Sécurité WordPress Avancée : Prévention des Vulnérabilités OWASP
Introduction : L'Impératif de Sécurité en 2025 WordPress alimente plus de 43% du web, ce…
Le caching distribué est la clé pour scaler WordPress au-delà de millions de pages vues par jour. Cet article présente des stratégies de caching battle-tested en production avec Redis et Memcached.
┌─────────────────────────────────────────────────────────────────┐
│ CDN Layer │
│ CloudFlare / CloudFront / Fastly (Edge Caching) │
│ Cache-Control: public, max-age=3600 │
└────────────────────────┬────────────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────────────┐
│ Reverse Proxy Cache │
│ Varnish / NGINX FastCGI Cache (Full Page Cache) │
│ TTL: 1 hour (dynamic), 1 day (static) │
└────────────────────────┬────────────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────────────┐
│ Application Cache Layer │
│ Redis Cluster / Memcached Cluster │
│ - Object Cache (WordPress transients, queries) │
│ - Session Storage │
│ - Fragment Cache (partials, widgets) │
└────────────────────────┬────────────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────────────┐
│ Opcode Cache (OPcache) │
│ PHP 8.2+ with Opcache (compiled bytecode) │
└────────────────────────┬────────────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────────────┐
│ Database Query Cache │
│ MySQL Query Cache (deprecated) / ProxySQL │
└─────────────────────────────────────────────────────────────────┘
| Type de Contenu | CDN | Varnish | Redis | TTL | Invalidation |
|---|---|---|---|---|---|
| Pages statiques | Oui | Oui | Non | 24h | URL-based |
| Pages accueil | Oui | Oui | Oui | 1h | Time + Event |
| Articles/Pages | Oui | Oui | Oui | 6h | Post update |
| Archives | Oui | Oui | Oui | 2h | New post |
| Widgets | Non | Non | Oui | 1h | Manual |
| Queries DB | Non | Non | Oui | 5m | Smart purge |
| Sessions | Non | Non | Oui | 24h | Logout |
| Transients | Non | Non | Oui | Variable | API call |
┌──────────────────────────────────────┐
│ Application Servers (Web Tier) │
│ PHP Redis Client (phpredis) │
└─────────────┬────────────────────────┘
│
┌─────────────▼────────────────┐
│ Redis Sentinel Cluster │
│ (HA Monitoring) │
│ sentinel-1, sentinel-2, │
│ sentinel-3 │
└─────────────┬────────────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
┌────▼─────┐ ┌──────▼────┐ ┌──────▼────┐
│ Master 1 │◄─────┤ Master 2 │◄─────┤ Master 3 │
│ Shard A │ │ Shard B │ │ Shard C │
│ Port:6379│ │ Port:6380 │ │ Port:6381 │
└────┬─────┘ └──────┬────┘ └──────┬────┘
│ │ │
┌────▼─────┐ ┌──────▼────┐ ┌──────▼────┐
│ Replica 1│ │ Replica 2 │ │ Replica 3 │
│ Shard A │ │ Shard B │ │ Shard C │
│ Port:6379│ │ Port:6380 │ │ Port:6381 │
└──────────┘ └───────────┘ └───────────┘
# /etc/redis/redis-6379.conf
# Redis 7.0+ Configuration for WordPress
# Network
bind 0.0.0.0
port 6379
timeout 300
tcp-keepalive 60
protected-mode yes
requirepass yourredisstrongpassword
# General
daemonize yes
supervised systemd
pidfile /var/run/redis/redis-6379.pid
loglevel notice
logfile /var/log/redis/redis-6379.log
databases 16
# Snapshotting (RDB)
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump-6379.rdb
dir /var/lib/redis
# Replication
replicaof no one
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
replica-priority 100
# Security
rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command CONFIG ""
# Limits
maxclients 10000
maxmemory 8gb
maxmemory-policy allkeys-lru
maxmemory-samples 5
# Append Only File (AOF)
appendonly yes
appendfilename "appendonly-6379.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
# Lua scripting
lua-time-limit 5000
# Slow log
slowlog-log-slower-than 10000
slowlog-max-len 128
# Latency monitor
latency-monitor-threshold 100
# Event notification
notify-keyspace-events "Ex"
# Advanced config
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 100
# Active defrag
activedefrag yes
active-defrag-ignore-bytes 100mb
active-defrag-threshold-lower 10
active-defrag-threshold-upper 100
active-defrag-cycle-min 1
active-defrag-cycle-max 25
# Client output buffer limits
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
# Performance
io-threads 4
io-threads-do-reads yes
# /etc/redis/sentinel.conf
# Redis Sentinel for High Availability
port 26379
daemonize yes
pidfile /var/run/redis/redis-sentinel.pid
logfile /var/log/redis/redis-sentinel.log
dir /var/lib/redis
# Monitor master instances
sentinel monitor mymaster-shard-a 10.0.3.10 6379 2
sentinel auth-pass mymaster-shard-a yourredisstrongpassword
sentinel down-after-milliseconds mymaster-shard-a 5000
sentinel parallel-syncs mymaster-shard-a 1
sentinel failover-timeout mymaster-shard-a 10000
sentinel monitor mymaster-shard-b 10.0.3.20 6380 2
sentinel auth-pass mymaster-shard-b yourredisstrongpassword
sentinel down-after-milliseconds mymaster-shard-b 5000
sentinel parallel-syncs mymaster-shard-b 1
sentinel failover-timeout mymaster-shard-b 10000
sentinel monitor mymaster-shard-c 10.0.3.30 6381 2
sentinel auth-pass mymaster-shard-c yourredisstrongpassword
sentinel down-after-milliseconds mymaster-shard-c 5000
sentinel parallel-syncs mymaster-shard-c 1
sentinel failover-timeout mymaster-shard-c 10000
# Notification scripts
sentinel notification-script mymaster-shard-a /usr/local/bin/redis-notify.sh
sentinel client-reconfig-script mymaster-shard-a /usr/local/bin/redis-reconfig.sh
Advanced Redis Object Cache Drop-in
Save as: wp-content/object-cache.php
/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class WPRedisObjectCache {
private $redis;
private $cache = [];
private $globalgroups = [];
private $nonpersistentgroups = [];
private $cachehits = 0;
private $cachemisses = 0;
private $clustermode = false;
private $sentinelmode = true;
public function construct() {
$this->redis = new Redis();
if ( $this->sentinelmode ) {
$this->connectsentinel();
} else {
$this->connectdirect();
}
}
/
Connect to Redis via Sentinel for HA
/
private function connectsentinel() {
$sentinels = [
[ 'host' => '10.0.3.101', 'port' => 26379 ],
[ 'host' => '10.0.3.102', 'port' => 26379 ],
[ 'host' => '10.0.3.103', 'port' => 26379 ],
];
$mastername = 'mymaster-shard-a';
foreach ( $sentinels as $sentinel ) {
try {
$sentinelconn = new Redis();
$sentinelconn->connect(
$sentinel['host'],
$sentinel['port'],
1 // timeout
);
$masterinfo = $sentinelconn->rawCommand(
'SENTINEL', 'get-master-addr-by-name', $mastername
);
if ( $masterinfo && count( $masterinfo ) === 2 ) {
$this->redis->connect(
$masterinfo[0],
$masterinfo[1],
1, // timeout
null,
0,
0,
[ 'stream' => [ 'verifypeer' => false ] ]
);
$this->redis->auth( WPREDISPASSWORD );
$this->redis->select( WPREDISDATABASE ?? 0 );
// Set client name for monitoring
$this->redis->client( 'SETNAME', 'wordpress:' . $SERVER['SERVERNAME'] ?? 'unknown' );
return true;
}
} catch ( Exception $e ) {
continue;
}
}
return false;
}
/
Direct connection (fallback)
/
private function connectdirect() {
try {
$this->redis->connect(
WPREDISHOST ?? '127.0.0.1',
WPREDISPORT ?? 6379,
1
);
if ( defined( 'WPREDISPASSWORD' ) ) {
$this->redis->auth( WPREDISPASSWORD );
}
$this->redis->select( WPREDISDATABASE ?? 0 );
return true;
} catch ( Exception $e ) {
errorlog( 'Redis connection failed: ' . $e->getMessage() );
return false;
}
}
/
Get cached value
/
public function get( $key, $group = 'default', $force = false, &$found = null ) {
$derivedkey = $this->buildkey( $key, $group );
// Check non-persistent groups
if ( inarray( $group, $this->nonpersistentgroups ) ) {
$found = isset( $this->cache[ $derivedkey ] );
if ( $found ) {
$this->cachehits++;
return $this->cache[ $derivedkey ];
}
$this->cachemisses++;
return false;
}
// Check local cache first
if ( ! $force && isset( $this->cache[ $derivedkey ] ) ) {
$found = true;
$this->cachehits++;
return $this->cache[ $derivedkey ];
}
// Get from Redis
try {
$value = $this->redis->get( $derivedkey );
if ( $value === false ) {
$found = false;
$this->cachemisses++;
return false;
}
$found = true;
$this->cachehits++;
$value = maybeunserialize( $value );
$this->cache[ $derivedkey ] = $value;
return $value;
} catch ( Exception $e ) {
$found = false;
$this->cachemisses++;
errorlog( 'Redis GET error: ' . $e->getMessage() );
return false;
}
}
/
Set cached value
/
public function set( $key, $data, $group = 'default', $expire = 0 ) {
$derivedkey = $this->buildkey( $key, $group );
// Non-persistent groups stay in memory only
if ( inarray( $group, $this->nonpersistentgroups ) ) {
$this->cache[ $derivedkey ] = $data;
return true;
}
// Store in local cache
$this->cache[ $derivedkey ] = $data;
// Store in Redis
try {
$value = maybeserialize( $data );
if ( $expire > 0 ) {
$result = $this->redis->setex( $derivedkey, $expire, $value );
} else {
$result = $this->redis->set( $derivedkey, $value );
}
return $result;
} catch ( Exception $e ) {
errorlog( 'Redis SET error: ' . $e->getMessage() );
return false;
}
}
/
Delete cached value
/
public function delete( $key, $group = 'default' ) {
$derivedkey = $this->buildkey( $key, $group );
unset( $this->cache[ $derivedkey ] );
if ( inarray( $group, $this->nonpersistentgroups ) ) {
return true;
}
try {
return (bool) $this->redis->del( $derivedkey );
} catch ( Exception $e ) {
errorlog( 'Redis DELETE error: ' . $e->getMessage() );
return false;
}
}
/
Flush entire cache
/
public function flush() {
$this->cache = [];
try {
return $this->redis->flushDB();
} catch ( Exception $e ) {
errorlog( 'Redis FLUSH error: ' . $e->getMessage() );
return false;
}
}
/
Build cache key with blog ID prefix for multisite
/
private function buildkey( $key, $group = 'default' ) {
if ( empty( $group ) ) {
$group = 'default';
}
$blogprefix = '';
if ( functionexists( 'ismultisite' ) && ismultisite() ) {
$blogid = getcurrentblogid();
$blogprefix = $blogid . ':';
}
return $blogprefix . $group . ':' . $key;
}
/
Add non-persistent groups
/
public function addnonpersistentgroups( $groups ) {
$groups = (array) $groups;
$this->nonpersistentgroups = arrayunique(
arraymerge( $this->nonpersistentgroups, $groups )
);
}
/
Get cache stats
/
public function getstats() {
$total = $this->cachehits + $this->cachemisses;
$hitrate = $total > 0 ? ( $this->cachehits / $total ) 100 : 0;
try {
$info = $this->redis->info();
return [
'hits' => $this->cachehits,
'misses' => $this->cachemisses,
'hitrate' => round( $hitrate, 2 ),
'uptime' => $info['uptimeinseconds'] ?? 0,
'connectedclients' => $info['connectedclients'] ?? 0,
'usedmemory' => $info['usedmemoryhuman'] ?? '0',
'usedmemorypeak' => $info['usedmemorypeakhuman'] ?? '0',
'evictedkeys' => $info['evictedkeys'] ?? 0,
'keyspacehits' => $info['keyspacehits'] ?? 0,
'keyspacemisses' => $info['keyspacemisses'] ?? 0,
];
} catch ( Exception $e ) {
return [
'hits' => $this->cachehits,
'misses' => $this->cachemisses,
'hitrate' => round( $hitrate, 2 ),
'error' => $e->getMessage(),
];
}
}
/
Implement other WordPress Object Cache API methods
/
public function add( $key, $data, $group = 'default', $expire = 0 ) {
$derivedkey = $this->buildkey( $key, $group );
if ( $this->get( $key, $group ) !== false ) {
return false;
}
return $this->set( $key, $data, $group, $expire );
}
public function replace( $key, $data, $group = 'default', $expire = 0 ) {
$derivedkey = $this->buildkey( $key, $group );
if ( $this->get( $key, $group ) === false ) {
return false;
}
return $this->set( $key, $data, $group, $expire );
}
public function incr( $key, $offset = 1, $group = 'default' ) {
$derivedkey = $this->buildkey( $key, $group );
try {
$value = $this->redis->incrBy( $derivedkey, $offset );
$this->cache[ $derivedkey ] = $value;
return $value;
} catch ( Exception $e ) {
return false;
}
}
public function decr( $key, $offset = 1, $group = 'default' ) {
$derivedkey = $this->buildkey( $key, $group );
try {
$value = $this->redis->decrBy( $derivedkey, $offset );
$this->cache[ $derivedkey ] = $value;
return $value;
} catch ( Exception $e ) {
return false;
}
}
}
// Initialize global cache object
$GLOBALS['wpobjectcache'] = new WPRedisObjectCache();
/
WordPress Object Cache API wrapper functions
/
function wpcacheinit() {
// Already initialized
}
function wpcacheadd( $key, $data, $group = '', $expire = 0 ) {
global $wpobjectcache;
return $wpobjectcache->add( $key, $data, $group, (int) $expire );
}
function wpcacheset( $key, $data, $group = '', $expire = 0 ) {
global $wpobjectcache;
return $wpobjectcache->set( $key, $data, $group, (int) $expire );
}
function wpcacheget( $key, $group = '', $force = false, &$found = null ) {
global $wpobjectcache;
return $wpobjectcache->get( $key, $group, $force, $found );
}
function wpcachedelete( $key, $group = '' ) {
global $wpobjectcache;
return $wpobjectcache->delete( $key, $group );
}
function wpcacheflush() {
global $wpobjectcache;
return $wpobjectcache->flush();
}
function wpcachereplace( $key, $data, $group = '', $expire = 0 ) {
global $wpobjectcache;
return $wpobjectcache->replace( $key, $data, $group, (int) $expire );
}
function wpcacheincr( $key, $offset = 1, $group = '' ) {
global $wpobjectcache;
return $wpobjectcache->incr( $key, $offset, $group );
}
function wpcachedecr( $key, $offset = 1, $group = '' ) {
global $wpobjectcache;
return $wpobjectcache->decr( $key, $offset, $group );
}
function wpcacheaddglobalgroups( $groups ) {
global $wpobjectcache;
$wpobjectcache->addglobalgroups( $groups );
}
function wpcacheaddnonpersistentgroups( $groups ) {
global $wpobjectcache;
$wpobjectcache->addnonpersistentgroups( $groups );
}
┌──────────────────────────────┐
│ Consistent Hashing Ring │
│ (Client-side distribution) │
└───────────┬──────────────────┘
│
┌───────────────┼───────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│Memcached│ │Memcached│ │Memcached│
│ Node 1 │ │ Node 2 │ │ Node 3 │
│11211 │ │11211 │ │11211 │
│16GB RAM │ │16GB RAM │ │16GB RAM │
└─────────┘ └─────────┘ └─────────┘
# /etc/memcached.conf
# Memcached 1.6+
# Memory
-m 16384
-I 5m
# Network
-p 11211
-U 0
-l 0.0.0.0
# Connection
-c 4096
-t 8
# Logging
-v
-L
# Memory management
-M
-o modern
-o hashpower=20
-o slabreassign
-o slabautomove
-o lrucrawler
-o lrumaintainer
# Security
# Use firewall rules instead of SASL for better performance
Memcached Object Cache Drop-in
Save as: wp-content/object-cache.php
/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class WPMemcachedObjectCache {
private $memcached;
private $cache = [];
private $globalgroups = [];
private $nonpersistentgroups = [];
private $cachehits = 0;
private $cachemisses = 0;
public function construct() {
$this->memcached = new Memcached( 'wordpress' );
$this->memcached->setOption( Memcached::OPTCOMPRESSION, true );
$this->memcached->setOption( Memcached::OPTCOMPRESSIONTYPE, Memcached::COMPRESSIONFASTLZ );
$this->memcached->setOption( Memcached::OPTSERIALIZER, Memcached::SERIALIZERIGBINARY );
$this->memcached->setOption( Memcached::OPTDISTRIBUTION, Memcached::DISTRIBUTIONCONSISTENT );
$this->memcached->setOption( Memcached::OPTLIBKETAMACOMPATIBLE, true );
$this->memcached->setOption( Memcached::OPTBINARYPROTOCOL, true );
$this->memcached->setOption( Memcached::OPTTCPNODELAY, true );
$this->memcached->setOption( Memcached::OPTCONNECTTIMEOUT, 1000 );
$this->memcached->setOption( Memcached::OPTRETRYTIMEOUT, 1 );
$this->memcached->setOption( Memcached::OPTSERVERFAILURELIMIT, 2 );
$this->memcached->setOption( Memcached::OPTREMOVEFAILEDSERVERS, true );
// Add servers (consistent hashing)
if ( ! $this->memcached->getServerList() ) {
$this->memcached->addServers( [
[ '10.0.4.10', 11211, 100 ], // weight 100
[ '10.0.4.11', 11211, 100 ],
[ '10.0.4.12', 11211, 100 ],
] );
}
}
// Similar implementation to Redis but using Memcached methods
public function get( $key, $group = 'default', $force = false, &$found = null ) {
$derivedkey = $this->buildkey( $key, $group );
if ( inarray( $group, $this->nonpersistentgroups ) ) {
$found = isset( $this->cache[ $derivedkey ] );
if ( $found ) {
$this->cachehits++;
return $this->cache[ $derivedkey ];
}
$this->cachemisses++;
return false;
}
if ( ! $force && isset( $this->cache[ $derivedkey ] ) ) {
$found = true;
$this->cachehits++;
return $this->cache[ $derivedkey ];
}
$value = $this->memcached->get( $derivedkey );
$resultcode = $this->memcached->getResultCode();
if ( $resultcode === Memcached::RESSUCCESS ) {
$found = true;
$this->cachehits++;
$this->cache[ $derivedkey ] = $value;
return $value;
}
$found = false;
$this->cachemisses++;
return false;
}
public function set( $key, $data, $group = 'default', $expire = 0 ) {
$derivedkey = $this->buildkey( $key, $group );
if ( inarray( $group, $this->nonpersistentgroups ) ) {
$this->cache[ $derivedkey ] = $data;
return true;
}
$this->cache[ $derivedkey ] = $data;
$expire = ( $expire === 0 ) ? 0 : time() + $expire;
return $this->memcached->set( $derivedkey, $data, $expire );
}
public function delete( $key, $group = 'default' ) {
$derivedkey = $this->buildkey( $key, $group );
unset( $this->cache[ $derivedkey ] );
if ( inarray( $group, $this->nonpersistentgroups ) ) {
return true;
}
return $this->memcached->delete( $derivedkey );
}
private function buildkey( $key, $group = 'default' ) {
if ( empty( $group ) ) {
$group = 'default';
}
$blogprefix = '';
if ( functionexists( 'ismultisite' ) && ismultisite() ) {
$blogid = getcurrentblogid();
$blogprefix = $blogid . ':';
}
return 'wp:' . $blogprefix . $group . ':' . $key;
}
public function getstats() {
$stats = $this->memcached->getStats();
$totalhits = 0;
$totalmisses = 0;
$totalmemory = 0;
$usedmemory = 0;
foreach ( $stats as $server => $serverstats ) {
if ( ! $serverstats ) {
continue;
}
$totalhits += $serverstats['gethits'] ?? 0;
$totalmisses += $serverstats['getmisses'] ?? 0;
$totalmemory += $serverstats['limitmaxbytes'] ?? 0;
$usedmemory += $serverstats['bytes'] ?? 0;
}
$total = $totalhits + $totalmisses;
$hitrate = $total > 0 ? ( $totalhits / $total ) 100 : 0;
return [
'hits' => $this->cachehits,
'misses' => $this->cachemisses,
'hitrate' => round( $hitrate, 2 ),
'totalmemory' => $this->formatbytes( $totalmemory ),
'usedmemory' => $this->formatbytes( $usedmemory ),
'servers' => count( $stats ),
];
}
private function formatbytes( $bytes ) {
$units = [ 'B', 'KB', 'MB', 'GB', 'TB' ];
$bytes = max( $bytes, 0 );
$pow = floor( ( $bytes ? log( $bytes ) : 0 ) / log( 1024 ) );
$pow = min( $pow, count( $units ) - 1 );
$bytes /= pow( 1024, $pow );
return round( $bytes, 2 ) . ' ' . $units[ $pow ];
}
}
WordPress Fragment Cache Helper
Add to theme functions.php or mu-plugin
/
class WPFragmentCache {
/
Cache a template fragment
/
public static function cache( $key, $ttl, $callable, $args = [] ) {
$cachekey = 'fragment:' . $key;
$group = 'fragments';
$cached = wpcacheget( $cachekey, $group );
if ( $cached !== false ) {
echo $cached;
return;
}
obstart();
calluserfuncarray( $callable, $args );
$output = obgetclean();
wpcacheset( $cachekey, $output, $group, $ttl );
echo $output;
}
/
Smart cache with dependency tracking
/
public static function smartcache( $key, $ttl, $dependencies, $callable, $args = [] ) {
$cachekey = 'fragment:' . $key;
$depskey = 'fragmentdeps:' . $key;
$group = 'fragments';
$cached = wpcacheget( $cachekey, $group );
$cacheddeps = wpcacheget( $depskey, $group );
// Check if dependencies changed
$currentdeps = self::getdependencyhash( $dependencies );
if ( $cached !== false && $cacheddeps === $currentdeps ) {
echo $cached;
return;
}
obstart();
calluserfuncarray( $callable, $args );
$output = obgetclean();
wpcacheset( $cachekey, $output, $group, $ttl );
wpcacheset( $depskey, $currentdeps, $group, $ttl );
echo $output;
}
/
Generate dependency hash
/
private static function getdependencyhash( $dependencies ) {
$hashparts = [];
foreach ( $dependencies as $type => $value ) {
switch ( $type ) {
case 'post':
$post = getpost( $value );
$hashparts[] = $post ? $post->postmodified : '';
break;
case 'term':
$term = getterm( $value );
$hashparts[] = $term ? $term->count : '';
break;
case 'option':
$hashparts[] = getoption( $value );
break;
case 'user':
$user = getuserby( 'id', $value );
$hashparts[] = $user ? $user->useremail : '';
break;
}
}
return md5( serialize( $hashparts ) );
}
/
Invalidate fragment cache
/
public static function invalidate( $key ) {
$cachekey = 'fragment:' . $key;
$depskey = 'fragmentdeps:' . $key;
wpcachedelete( $cachekey, 'fragments' );
wpcachedelete( $depskey, 'fragments' );
}
/
Invalidate by pattern (requires Redis)
/
public static function invalidatepattern( $pattern ) {
global $wpobjectcache;
if ( ! methodexists( $wpobjectcache->redis, 'scan' ) ) {
return false;
}
$iterator = null;
$prefix = 'fragment:' . $pattern . '';
while ( false !== ( $keys = $wpobjectcache->redis->scan( $iterator, $prefix ) ) ) {
foreach ( $keys as $key ) {
wpcachedelete( strreplace( 'fragment:', '', $key ), 'fragments' );
}
}
return true;
}
}
/
Helper functions
/
function wpfragmentcache( $key, $ttl, $callable, $args = [] ) {
WPFragmentCache::cache( $key, $ttl, $callable, $args );
}
function wpfragmentinvalidate( $key ) {
WPFragmentCache::invalidate( $key );
}
/
Usage examples in theme templates
/
function exampleusage() {
// Simple fragment cache
wpfragmentcache( 'popularposts', HOURINSECONDS, function() {
$query = new WPQuery( [ 'metakey' => 'views', 'orderby' => 'metavaluenum', 'postsperpage' => 5 ] );
while ( $query->haveposts() ) {
$query->thepost();
gettemplatepart( 'template-parts/content', 'popular' );
}
wpresetpostdata();
} );
// Smart cache with dependencies
WPFragmentCache::smartcache(
'post' . gettheID() . 'related',
DAYINSECONDS,
[ 'post' => gettheID(), 'term' => getthecategory()[0]->termid ],
function( $postid ) {
$related = new WPQuery( [
'postnotin' => [ $postid ],
'categoryin' => wpgetpostcategories( $postid ),
'postsperpage' => 4,
] );
while ( $related->haveposts() ) {
$related->thepost();
gettemplatepart( 'template-parts/content', 'related' );
}
wpresetpostdata();
},
[ gettheID() ]
);
}
/
Auto-invalidation on content updates
/
addaction( 'savepost', function( $postid ) {
// Invalidate post fragment
wpfragmentinvalidate( 'post' . $postid . 'related' );
// Invalidate category archives
$categories = wpgetpostcategories( $postid );
foreach ( $categories as $catid ) {
wpfragmentinvalidate( 'category' . $catid . 'posts' );
}
// Invalidate homepage
wpfragmentinvalidate( 'popularposts' );
wpfragmentinvalidate( 'recentposts' );
}, 10, 1 );
Persistent Query Result Cache
/
class WPQueryCache {
private static $instance = null;
private $enabled = true;
private $ttl = 3600;
public static function getinstance() {
if ( self::$instance === null ) {
self::$instance = new self();
}
return self::$instance;
}
private function construct() {
addfilter( 'postsprequery', [ $this, 'maybereturncached' ], 10, 2 );
addfilter( 'theposts', [ $this, 'maybecacheresults' ], 10, 2 );
}
/
Check cache before query
/
public function maybereturncached( $posts, $query ) {
if ( ! $this->shouldcachequery( $query ) ) {
return $posts;
}
$cachekey = $this->getcachekey( $query );
$cached = wpcacheget( $cachekey, 'queryresults' );
if ( $cached !== false ) {
// Restore foundposts for pagination
$query->foundposts = $cached['foundposts'];
$query->maxnumpages = $cached['maxnumpages'];
return $cached['posts'];
}
return $posts;
}
/
Cache query results after execution
/
public function maybecacheresults( $posts, $query ) {
if ( ! $this->shouldcachequery( $query ) ) {
return $posts;
}
$cachekey = $this->getcachekey( $query );
$cachedata = [
'posts' => $posts,
'foundposts' => $query->foundposts,
'maxnumpages' => $query->maxnumpages,
];
wpcacheset( $cachekey, $cachedata, 'queryresults', $this->ttl );
return $posts;
}
/
Determine if query should be cached
/
private function shouldcachequery( $query ) {
// Don't cache if explicitly disabled
if ( ! $this->enabled || ! empty( $query->queryvars['cacheresults'] ) && $query->queryvars['cacheresults'] === false ) {
return false;
}
// Don't cache admin queries
if ( isadmin() && ! wpdoingajax() ) {
return false;
}
// Don't cache if user is logged in (unless specifically allowed)
if ( isuserloggedin() && ! applyfilters( 'wpquerycacheloggedin', false ) ) {
return false;
}
// Don't cache meta queries (complex to invalidate)
if ( ! empty( $query->queryvars['metaquery'] ) ) {
return false;
}
return true;
}
/
Generate unique cache key for query
/
private function getcachekey( $query ) {
$keyparts = [
'sql' => $query->request,
'queryvars' => $query->queryvars,
];
return 'query:' . md5( serialize( $keyparts ) );
}
/
Invalidate query caches
/
public static function invalidateforpost( $postid ) {
$post = getpost( $postid );
if ( ! $post ) {
return;
}
// Invalidate by post type
self::invalidatebypattern( 'posttype:' . $post->posttype );
// Invalidate by taxonomy
$taxonomies = getobjecttaxonomies( $post->posttype );
foreach ( $taxonomies as $taxonomy ) {
$terms = wpgetobjectterms( $postid, $taxonomy, [ 'fields' => 'ids' ] );
foreach ( $terms as $termid ) {
self::invalidatebypattern( 'taxonomy:' . $taxonomy . ':' . $termid );
}
}
}
/
Invalidate caches by pattern
/
private static function invalidatebypattern( $pattern ) {
// This requires Redis SCAN or similar
// For Memcached, you'll need to track keys separately
WPFragmentCache::invalidatepattern( $pattern );
}
}
// Initialize
WPQueryCache::getinstance();
// Invalidate on post save
addaction( 'savepost', [ 'WPQueryCache', 'invalidateforpost' ] );
addaction( 'deletepost', [ 'WPQueryCache', 'invalidateforpost' ] );
# Redis Benchmark
redis-benchmark -h 10.0.3.10 -p 6379 -a password -t set,get -n 1000000 -c 50 -d 100
SET: 142,857.14 requests per second
GET: 166,666.67 requests per second
Latency (GET P99): 0.8ms
# Memcached Benchmark
memcached-benchmark -s 10.0.4.10:11211 -c 50 -n 1000000
SET: 175,438.60 requests per second
GET: 192,307.69 requests per second
Latency (GET P99): 0.5ms
| Scenario | No Cache | Redis | Memcached | Improvement |
|---|---|---|---|---|
| Homepage (20 posts) | 450ms | 42ms | 38ms | 91% |
| Archive page | 380ms | 35ms | 32ms | 92% |
| Single post | 180ms | 28ms | 25ms | 86% |
| Complex taxonomy query | 850ms | 95ms | 88ms | 90% |
| REST API /wp/v2/posts | 520ms | 68ms | 62ms | 88% |
| Layer | Hit Rate | Avg TTL | Evictions/day |
|---|---|---|---|
| Object Cache (Redis) | 94.3% | 1h | 2,145 |
| Fragment Cache | 91.7% | 6h | 856 |
| Query Cache | 88.2% | 1h | 3,421 |
| Full Page Cache (Varnish) | 96.8% | 1h | 1,234 |
#!/bin/bash
# /usr/local/bin/redis-monitor.sh
REDISCLI="/usr/bin/redis-cli"
REDISHOST="10.0.3.10"
REDISPORT="6379"
REDISPASS="yourredispassword"
# Get Redis info
INFO=$($REDISCLI -h $REDISHOST -p $REDISPORT -a $REDISPASS INFO)
# Parse metrics
USEDMEMORY=$(echo "$INFO" | grep "usedmemoryhuman:" | cut -d: -f2 | tr -d 'r')
USEDMEMORYPEAK=$(echo "$INFO" | grep "usedmemorypeakhuman:" | cut -d: -f2 | tr -d 'r')
CONNECTEDCLIENTS=$(echo "$INFO" | grep "connectedclients:" | cut -d: -f2 | tr -d 'r')
KEYSPACEHITS=$(echo "$INFO" | grep "keyspacehits:" | cut -d: -f2 | tr -d 'r')
KEYSPACEMISSES=$(echo "$INFO" | grep "keyspacemisses:" | cut -d: -f2 | tr -d 'r')
EVICTEDKEYS=$(echo "$INFO" | grep "evictedkeys:" | cut -d: -f2 | tr -d 'r')
# Calculate hit rate
TOTAL=$((KEYSPACEHITS + KEYSPACEMISSES))
if [ $TOTAL -gt 0 ]; then
HITRATE=$(awk "BEGIN {printf "%.2f", ($KEYSPACEHITS / $TOTAL) 100}")
else
HITRATE=0
fi
# Output for Prometheus node exporter textfile collector
cat > /var/lib/nodeexporter/textfilecollector/redis.prom <used memorybytes Used memory in bytes
# TYPE redisusedmemorybytes gauge
redisusedmemorybytes $(echo "$INFO" | grep "usedmemory:" | cut -d: -f2 | tr -d 'r')
# HELP redisconnectedclients Number of connected clients
# TYPE redisconnectedclients gauge
redisconnectedclients $CONNECTEDCLIENTS
# HELP rediskeyspacehitstotal Total keyspace hits
# TYPE rediskeyspacehitstotal counter
rediskeyspacehitstotal $KEYSPACEHITS
# HELP rediskeyspacemissestotal Total keyspace misses
# TYPE rediskeyspacemissestotal counter
rediskeyspacemissestotal $KEYSPACEMISSES
# HELP redisevictedkeystotal Total evicted keys
# TYPE redisevictedkeystotal counter
redisevictedkeystotal $EVICTEDKEYS
# HELP redishitrate Hit rate percentage
# TYPE redishitrate gauge
redishitrate $HITRATE
EOF
echo "Redis Stats:"
echo "Memory: $USEDMEMORY / Peak: $USEDMEMORYPEAK"
echo "Clients: $CONNECTEDCLIENTS"
echo "Hit Rate: ${HITRATE}%"
echo "Evictions: $EVICTEDKEYS"
WordPress Cache Warmer
Proactively populate cache for critical pages
/
class WPCacheWarmer {
private $urls = [];
private $batchsize = 5;
private $concurrentrequests = 3;
public function construct() {
addaction( 'wpcachewarmercron', [ $this, 'warmcache' ] );
if ( ! wpnextscheduled( 'wpcachewarmercron' ) ) {
wpscheduleevent( time(), 'hourly', 'wpcachewarmercron' );
}
}
/
Add URLs to warm
/
public function addurls( $urls ) {
$this->urls = arraymerge( $this->urls, $urls );
}
/
Warm critical pages
/
public function warmcache() {
// Get critical URLs
$criticalurls = $this->getcriticalurls();
// Process in batches
$batches = arraychunk( $criticalurls, $this->batchsize );
foreach ( $batches as $batch ) {
$this->processbatch( $batch );
}
}
/
Get critical URLs to warm
/
private function getcriticalurls() {
$urls = [];
// Homepage
$urls[] = homeurl( '/' );
// Top 20 posts by views
$popular = new WPQuery( [
'posttype' => 'post',
'postsperpage' => 20,
'metakey' => 'views',
'orderby' => 'metavaluenum',
'order' => 'DESC',
] );
while ( $popular->haveposts() ) {
$popular->thepost();
$urls[] = getpermalink();
}
wpresetpostdata();
// Main category archives
$categories = getcategories( [ 'number' => 10, 'orderby' => 'count', 'order' => 'DESC' ] );
foreach ( $categories as $category ) {
$urls[] = getcategorylink( $category->termid );
}
// Add custom URLs
$urls = arraymerge( $urls, $this->urls );
return arrayunique( $urls );
}
/
Process batch of URLs
/
private function processbatch( $batch ) {
$multi = curlmultiinit();
$handles = [];
foreach ( $batch as $url ) {
$ch = curlinit( $url );
curlsetoptarray( $ch, [
CURLOPTRETURNTRANSFER => true,
CURLOPTFOLLOWLOCATION => true,
CURLOPTMAXREDIRS => 3,
CURLOPTTIMEOUT => 30,
CURLOPTUSERAGENT => 'WordPress Cache Warmer',
CURLOPTHEADER => true,
] );
curlmultiaddhandle( $multi, $ch );
$handles[] = $ch;
}
// Execute all requests
$running = null;
do {
curlmultiexec( $multi, $running );
curlmultiselect( $multi );
} while ( $running > 0 );
// Clean up
foreach ( $handles as $ch ) {
curlmultiremovehandle( $multi, $ch );
curlclose( $ch );
}
curlmulticlose( $multi );
}
}
// Initialize
$cachewarmer = new WPCacheWarmer();
// Add custom URLs
$cachewarmer->addurls( [
homeurl( '/about' ),
homeurl( '/contact' ),
home_url( '/services' ),
] );
Le caching distribué avec Redis et Memcached permet de scaler WordPress à des millions de requêtes par jour:
Redis – Recommandé si:
Memcached – Recommandé si:
La clé du succès: monitoring continu, invalidation intelligente, et tuning progressif basé sur les métriques réelles.
Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.