Introduction
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.
Architecture de Caching Multi-Niveau
Vue d’Ensemble des Couches de Cache
┌─────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────────┘
Stratégie de Cache par Type de Contenu
| 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 |
Redis Cluster Configuration
Architecture Redis Cluster (Production)
┌──────────────────────────────────────┐
│ 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 │
└──────────┘ └───────────┘ └───────────┘
Redis Master Configuration
# /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 your_redis_strong_password
# 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
Redis Sentinel Configuration
# /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 your_redis_strong_password
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 your_redis_strong_password
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 your_redis_strong_password
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
WordPress Redis Object Cache Implementation
redis = new Redis();
if ( $this->sentinel_mode ) {
$this->connect_sentinel();
} else {
$this->connect_direct();
}
}
/**
* Connect to Redis via Sentinel for HA
*/
private function connect_sentinel() {
$sentinels = [
[ 'host' => '10.0.3.101', 'port' => 26379 ],
[ 'host' => '10.0.3.102', 'port' => 26379 ],
[ 'host' => '10.0.3.103', 'port' => 26379 ],
];
$master_name = 'mymaster-shard-a';
foreach ( $sentinels as $sentinel ) {
try {
$sentinel_conn = new Redis();
$sentinel_conn->connect(
$sentinel['host'],
$sentinel['port'],
1 // timeout
);
$master_info = $sentinel_conn->rawCommand(
'SENTINEL', 'get-master-addr-by-name', $master_name
);
if ( $master_info && count( $master_info ) === 2 ) {
$this->redis->connect(
$master_info[0],
$master_info[1],
1, // timeout
null,
0,
0,
[ 'stream' => [ 'verify_peer' => false ] ]
);
$this->redis->auth( WP_REDIS_PASSWORD );
$this->redis->select( WP_REDIS_DATABASE ?? 0 );
// Set client name for monitoring
$this->redis->client( 'SETNAME', 'wordpress:' . $*SERVER['SERVER_NAME'] ?? 'unknown' );
return true;
}
} catch ( Exception $e ) {
continue;
}
}
return false;
}
/**
* Direct connection (fallback)
*/
private function connect_direct() {
try {
$this->redis->connect(
WP_REDIS_HOST ?? '127.0.0.1',
WP_REDIS_PORT ?? 6379,
1
);
if ( defined( 'WP_REDIS_PASSWORD' ) ) {
$this->redis->auth( WP_REDIS_PASSWORD );
}
$this->redis->select( WP_REDIS_DATABASE ?? 0 );
return true;
} catch ( Exception $e ) {
error_log( 'Redis connection failed: ' . $e->getMessage() );
return false;
}
}
/**
* Get cached value
*/
public function get( $key, $group = 'default', $force = false, &$found = null ) {
$derived_key = $this->build_key( $key, $group );
// Check non-persistent groups
if ( in_array( $group, $this->non_persistent_groups ) ) {
$found = isset( $this->cache[ $derived_key ] );
if ( $found ) {
$this->cache_hits++;
return $this->cache[ $derived_key ];
}
$this->cache_misses++;
return false;
}
// Check local cache first
if ( ! $force && isset( $this->cache[ $derived_key ] ) ) {
$found = true;
$this->cache_hits++;
return $this->cache[ $derived_key ];
}
// Get from Redis
try {
$value = $this->redis->get( $derived_key );
if ( $value === false ) {
$found = false;
$this->cache_misses++;
return false;
}
$found = true;
$this->cache_hits++;
$value = maybe_unserialize( $value );
$this->cache[ $derived_key ] = $value;
return $value;
} catch ( Exception $e ) {
$found = false;
$this->cache_misses++;
error_log( 'Redis GET error: ' . $e->getMessage() );
return false;
}
}
/**
* Set cached value
*/
public function set( $key, $data, $group = 'default', $expire = 0 ) {
$derived_key = $this->build_key( $key, $group );
// Non-persistent groups stay in memory only
if ( in_array( $group, $this->non_persistent_groups ) ) {
$this->cache[ $derived_key ] = $data;
return true;
}
// Store in local cache
$this->cache[ $derived_key ] = $data;
// Store in Redis
try {
$value = maybe_serialize( $data );
if ( $expire > 0 ) {
$result = $this->redis->setex( $derived_key, $expire, $value );
} else {
$result = $this->redis->set( $derived_key, $value );
}
return $result;
} catch ( Exception $e ) {
error_log( 'Redis SET error: ' . $e->getMessage() );
return false;
}
}
/**
* Delete cached value
*/
public function delete( $key, $group = 'default' ) {
$derived_key = $this->build_key( $key, $group );
unset( $this->cache[ $derived_key ] );
if ( in_array( $group, $this->non_persistent_groups ) ) {
return true;
}
try {
return (bool) $this->redis->del( $derived_key );
} catch ( Exception $e ) {
error_log( 'Redis DELETE error: ' . $e->getMessage() );
return false;
}
}
/**
* Flush entire cache
*/
public function flush() {
$this->cache = [];
try {
return $this->redis->flushDB();
} catch ( Exception $e ) {
error_log( 'Redis FLUSH error: ' . $e->getMessage() );
return false;
}
}
/**
* Build cache key with blog ID prefix for multisite
*/
private function build_key( $key, $group = 'default' ) {
if ( empty( $group ) ) {
$group = 'default';
}
$blog_prefix = '';
if ( function_exists( 'is_multisite' ) && is_multisite() ) {
$blog_id = get_current_blog_id();
$blog_prefix = $blog_id . ':';
}
return $blog_prefix . $group . ':' . $key;
}
/**
* Add non-persistent groups
*/
public function add_non_persistent_groups( $groups ) {
$groups = (array) $groups;
$this->non_persistent_groups = array_unique(
array_merge( $this->non_persistent_groups, $groups )
);
}
/**
* Get cache stats
*/
public function get_stats() {
$total = $this->cache_hits + $this->cache_misses;
$hit_rate = $total > 0 ? ( $this->cache_hits / $total ) * 100 : 0;
try {
$info = $this->redis->info();
return [
'hits' => $this->cache_hits,
'misses' => $this->cache_misses,
'hit_rate' => round( $hit_rate, 2 ),
'uptime' => $info['uptime_in_seconds'] ?? 0,
'connected_clients' => $info['connected_clients'] ?? 0,
'used_memory' => $info['used_memory_human'] ?? '0',
'used_memory_peak' => $info['used_memory_peak_human'] ?? '0',
'evicted_keys' => $info['evicted_keys'] ?? 0,
'keyspace_hits' => $info['keyspace_hits'] ?? 0,
'keyspace_misses' => $info['keyspace_misses'] ?? 0,
];
} catch ( Exception $e ) {
return [
'hits' => $this->cache_hits,
'misses' => $this->cache_misses,
'hit_rate' => round( $hit_rate, 2 ),
'error' => $e->getMessage(),
];
}
}
/**
* Implement other WordPress Object Cache API methods
*/
public function add( $key, $data, $group = 'default', $expire = 0 ) {
$derived_key = $this->build_key( $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 ) {
$derived_key = $this->build_key( $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' ) {
$derived_key = $this->build_key( $key, $group );
try {
$value = $this->redis->incrBy( $derived_key, $offset );
$this->cache[ $derived_key ] = $value;
return $value;
} catch ( Exception $e ) {
return false;
}
}
public function decr( $key, $offset = 1, $group = 'default' ) {
$derived_key = $this->build_key( $key, $group );
try {
$value = $this->redis->decrBy( $derived_key, $offset );
$this->cache[ $derived_key ] = $value;
return $value;
} catch ( Exception $e ) {
return false;
}
}
}
// Initialize global cache object
$GLOBALS['wp_object_cache'] = new WP_Redis_Object_Cache();
/**
* WordPress Object Cache API wrapper functions
*/
function wp_cache_init() {
// Already initialized
}
function wp_cache_add( $key, $data, $group = '', $expire = 0 ) {
global $wp_object_cache;
return $wp_object_cache->add( $key, $data, $group, (int) $expire );
}
function wp_cache_set( $key, $data, $group = '', $expire = 0 ) {
global $wp_object_cache;
return $wp_object_cache->set( $key, $data, $group, (int) $expire );
}
function wp_cache_get( $key, $group = '', $force = false, &$found = null ) {
global $wp_object_cache;
return $wp_object_cache->get( $key, $group, $force, $found );
}
function wp_cache_delete( $key, $group = '' ) {
global $wp_object_cache;
return $wp_object_cache->delete( $key, $group );
}
function wp_cache_flush() {
global $wp_object_cache;
return $wp_object_cache->flush();
}
function wp_cache_replace( $key, $data, $group = '', $expire = 0 ) {
global $wp_object_cache;
return $wp_object_cache->replace( $key, $data, $group, (int) $expire );
}
function wp_cache_incr( $key, $offset = 1, $group = '' ) {
global $wp_object_cache;
return $wp_object_cache->incr( $key, $offset, $group );
}
function wp_cache_decr( $key, $offset = 1, $group = '' ) {
global $wp_object_cache;
return $wp_object_cache->decr( $key, $offset, $group );
}
function wp_cache_add_global_groups( $groups ) {
global $wp_object_cache;
$wp_object_cache->add_global_groups( $groups );
}
function wp_cache_add_non_persistent_groups( $groups ) {
global $wp_object_cache;
$wp_object_cache->add_non_persistent_groups( $groups );
}
Memcached Cluster Configuration
Memcached Deployment Architecture
┌──────────────────────────────┐
│ Consistent Hashing Ring │
│ (Client-side distribution) │
└───────────┬──────────────────┘
│
┌───────────────┼───────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│Memcached│ │Memcached│ │Memcached│
│ Node 1 │ │ Node 2 │ │ Node 3 │
│11211 │ │11211 │ │11211 │
│16GB RAM │ │16GB RAM │ │16GB RAM │
└─────────┘ └─────────┘ └─────────┘
Memcached Configuration
# /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 slab_reassign
-o slab_automove
-o lru_crawler
-o lru_maintainer
# Security
# Use firewall rules instead of SASL for better performance
WordPress Memcached Object Cache
memcached = new Memcached( 'wordpress' );
$this->memcached->setOption( Memcached::OPT_COMPRESSION, true );
$this->memcached->setOption( Memcached::OPT_COMPRESSION_TYPE, Memcached::COMPRESSION_FASTLZ );
$this->memcached->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_IGBINARY );
$this->memcached->setOption( Memcached::OPT_DISTRIBUTION, Memcached::DISTRIBUTION_CONSISTENT );
$this->memcached->setOption( Memcached::OPT_LIBKETAMA_COMPATIBLE, true );
$this->memcached->setOption( Memcached::OPT_BINARY_PROTOCOL, true );
$this->memcached->setOption( Memcached::OPT_TCP_NODELAY, true );
$this->memcached->setOption( Memcached::OPT_CONNECT_TIMEOUT, 1000 );
$this->memcached->setOption( Memcached::OPT_RETRY_TIMEOUT, 1 );
$this->memcached->setOption( Memcached::OPT_SERVER_FAILURE_LIMIT, 2 );
$this->memcached->setOption( Memcached::OPT_REMOVE_FAILED_SERVERS, 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 ) {
$derived_key = $this->build_key( $key, $group );
if ( in_array( $group, $this->non_persistent_groups ) ) {
$found = isset( $this->cache[ $derived_key ] );
if ( $found ) {
$this->cache_hits++;
return $this->cache[ $derived_key ];
}
$this->cache_misses++;
return false;
}
if ( ! $force && isset( $this->cache[ $derived_key ] ) ) {
$found = true;
$this->cache_hits++;
return $this->cache[ $derived_key ];
}
$value = $this->memcached->get( $derived_key );
$result_code = $this->memcached->getResultCode();
if ( $result_code === Memcached::RES_SUCCESS ) {
$found = true;
$this->cache_hits++;
$this->cache[ $derived_key ] = $value;
return $value;
}
$found = false;
$this->cache_misses++;
return false;
}
public function set( $key, $data, $group = 'default', $expire = 0 ) {
$derived_key = $this->build_key( $key, $group );
if ( in_array( $group, $this->non_persistent_groups ) ) {
$this->cache[ $derived_key ] = $data;
return true;
}
$this->cache[ $derived_key ] = $data;
$expire = ( $expire === 0 ) ? 0 : time() + $expire;
return $this->memcached->set( $derived_key, $data, $expire );
}
public function delete( $key, $group = 'default' ) {
$derived_key = $this->build_key( $key, $group );
unset( $this->cache[ $derived_key ] );
if ( in_array( $group, $this->non_persistent_groups ) ) {
return true;
}
return $this->memcached->delete( $derived_key );
}
private function build_key( $key, $group = 'default' ) {
if ( empty( $group ) ) {
$group = 'default';
}
$blog_prefix = '';
if ( function_exists( 'is_multisite' ) && is_multisite() ) {
$blog_id = get_current_blog_id();
$blog_prefix = $blog_id . ':';
}
return 'wp:' . $blog_prefix . $group . ':' . $key;
}
public function get_stats() {
$stats = $this->memcached->getStats();
$total_hits = 0;
$total_misses = 0;
$total_memory = 0;
$used_memory = 0;
foreach ( $stats as $server => $server_stats ) {
if ( ! $server_stats ) {
continue;
}
$total_hits += $server_stats['get_hits'] ?? 0;
$total_misses += $server_stats['get_misses'] ?? 0;
$total_memory += $server_stats['limit_maxbytes'] ?? 0;
$used_memory += $server_stats['bytes'] ?? 0;
}
$total = $total_hits + $total_misses;
$hit_rate = $total > 0 ? ( $total_hits / $total ) * 100 : 0;
return [
'hits' => $this->cache_hits,
'misses' => $this->cache_misses,
'hit_rate' => round( $hit_rate, 2 ),
'total_memory' => $this->format_bytes( $total_memory ),
'used_memory' => $this->format_bytes( $used_memory ),
'servers' => count( $stats ),
];
}
private function format_bytes( $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 ];
}
}
Fragment Caching Strategies
Advanced Fragment Caching Helper
$value ) {
switch ( $type ) {
case 'post':
$post = get_post( $value );
$hash_parts[] = $post ? $post->post_modified : '';
break;
case 'term':
$term = get_term( $value );
$hash_parts[] = $term ? $term->count : '';
break;
case 'option':
$hash_parts[] = get_option( $value );
break;
case 'user':
$user = get_user_by( 'id', $value );
$hash_parts[] = $user ? $user->user_email : '';
break;
}
}
return md5( serialize( $hash_parts ) );
}
/**
* Invalidate fragment cache
*/
public static function invalidate( $key ) {
$cache_key = 'fragment:' . $key;
$deps_key = 'fragment_deps:' . $key;
wp_cache_delete( $cache_key, 'fragments' );
wp_cache_delete( $deps_key, 'fragments' );
}
/**
* Invalidate by pattern (requires Redis)
*/
public static function invalidate_pattern( $pattern ) {
global $wp_object_cache;
if ( ! method_exists( $wp_object_cache->redis, 'scan' ) ) {
return false;
}
$iterator = null;
$prefix = 'fragment:' . $pattern . '*';
while ( false !== ( $keys = $wp_object_cache->redis->scan( $iterator, $prefix ) ) ) {
foreach ( $keys as $key ) {
wp_cache_delete( str_replace( 'fragment:', '', $key ), 'fragments' );
}
}
return true;
}
}
/**
* Helper functions
*/
function wp_fragment_cache( $key, $ttl, $callable, $args = [] ) {
WP_Fragment_Cache::cache( $key, $ttl, $callable, $args );
}
function wp_fragment_invalidate( $key ) {
WP_Fragment_Cache::invalidate( $key );
}
/**
* Usage examples in theme templates
*/
function example_usage() {
// Simple fragment cache
wp_fragment_cache( 'popular_posts', HOUR_IN_SECONDS, function() {
$query = new WP_Query( [ 'meta_key' => 'views', 'orderby' => 'meta_value_num', 'posts_per_page' => 5 ] );
while ( $query->have_posts() ) {
$query->the_post();
get_template_part( 'template-parts/content', 'popular' );
}
wp_reset_postdata();
} );
// Smart cache with dependencies
WP_Fragment_Cache::smart_cache(
'post*' . get_the_ID() . '*related',
DAY_IN_SECONDS,
[ 'post' => get_the_ID(), 'term' => get_the_category()[0]->term_id ],
function( $post_id ) {
$related = new WP_Query( [
'post__not_in' => [ $post_id ],
'category__in' => wp_get_post_categories( $post_id ),
'posts_per_page' => 4,
] );
while ( $related->have_posts() ) {
$related->the_post();
get_template_part( 'template-parts/content', 'related' );
}
wp_reset_postdata();
},
[ get_the_ID() ]
);
}
/**
* Auto-invalidation on content updates
*/
add_action( 'save_post', function( $post_id ) {
// Invalidate post fragment
wp_fragment_invalidate( 'post*' . $post_id . '*related' );
// Invalidate category archives
$categories = wp_get_post_categories( $post_id );
foreach ( $categories as $cat_id ) {
wp_fragment_invalidate( 'category*' . $cat_id . '*posts' );
}
// Invalidate homepage
wp_fragment_invalidate( 'popular_posts' );
wp_fragment_invalidate( 'recent_posts' );
}, 10, 1 );
Query Result Caching
Advanced Database Query Cache
should_cache_query( $query ) ) {
return $posts;
}
$cache_key = $this->get_cache_key( $query );
$cached = wp_cache_get( $cache_key, 'query_results' );
if ( $cached !== false ) {
// Restore found_posts for pagination
$query->found_posts = $cached['found_posts'];
$query->max_num_pages = $cached['max_num_pages'];
return $cached['posts'];
}
return $posts;
}
/**
* Cache query results after execution
*/
public function maybe_cache_results( $posts, $query ) {
if ( ! $this->should_cache_query( $query ) ) {
return $posts;
}
$cache_key = $this->get_cache_key( $query );
$cache_data = [
'posts' => $posts,
'found_posts' => $query->found_posts,
'max_num_pages' => $query->max_num_pages,
];
wp_cache_set( $cache_key, $cache_data, 'query_results', $this->ttl );
return $posts;
}
/**
* Determine if query should be cached
*/
private function should_cache_query( $query ) {
// Don't cache if explicitly disabled
if ( ! $this->enabled || ! empty( $query->query_vars['cache_results'] ) && $query->query_vars['cache_results'] === false ) {
return false;
}
// Don't cache admin queries
if ( is_admin() && ! wp_doing_ajax() ) {
return false;
}
// Don't cache if user is logged in (unless specifically allowed)
if ( is_user_logged_in() && ! apply_filters( 'wp_query_cache_logged_in', false ) ) {
return false;
}
// Don't cache meta queries (complex to invalidate)
if ( ! empty( $query->query_vars['meta_query'] ) ) {
return false;
}
return true;
}
/**
* Generate unique cache key for query
*/
private function get_cache_key( $query ) {
$key_parts = [
'sql' => $query->request,
'query_vars' => $query->query_vars,
];
return 'query:' . md5( serialize( $key_parts ) );
}
/**
* Invalidate query caches
*/
public static function invalidate_for_post( $post_id ) {
$post = get_post( $post_id );
if ( ! $post ) {
return;
}
// Invalidate by post type
self::invalidate_by_pattern( 'post_type:' . $post->post_type );
// Invalidate by taxonomy
$taxonomies = get_object_taxonomies( $post->post_type );
foreach ( $taxonomies as $taxonomy ) {
$terms = wp_get_object_terms( $post_id, $taxonomy, [ 'fields' => 'ids' ] );
foreach ( $terms as $term_id ) {
self::invalidate_by_pattern( 'taxonomy:' . $taxonomy . ':' . $term_id );
}
}
}
/**
* Invalidate caches by pattern
*/
private static function invalidate_by_pattern( $pattern ) {
// This requires Redis SCAN or similar
// For Memcached, you'll need to track keys separately
WP_Fragment_Cache::invalidate_pattern( $pattern );
}
}
// Initialize
WP_Query_Cache::get_instance();
// Invalidate on post save
add_action( 'save_post', [ 'WP_Query_Cache', 'invalidate_for_post' ] );
add_action( 'delete_post', [ 'WP_Query_Cache', 'invalidate_for_post' ] );
Performance Benchmarks
Test Infrastructure
- Cache Layer: Redis 7.0 Cluster (6 nodes, 8GB RAM each)
- Alternative: Memcached 1.6 (3 nodes, 16GB RAM each)
- Database: MySQL 8.0 (Primary + 2 Replicas)
- Web Tier: 4x PHP 8.2 + NGINX
- Test Tool: Apache Bench + custom scripts
Redis vs Memcached Performance
# 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
WordPress Query Performance
| 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% |
Cache Hit Rates (Production – 30 Days)
| 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 |
Monitoring and Optimization
Redis Monitoring Script
#!/bin/bash
# /usr/local/bin/redis-monitor.sh
REDIS_CLI="/usr/bin/redis-cli"
REDIS_HOST="10.0.3.10"
REDIS_PORT="6379"
REDIS_PASS="your_redis_password"
# Get Redis info
INFO=$($REDIS_CLI -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASS INFO)
# Parse metrics
USED_MEMORY=$(echo "$INFO" | grep "used_memory_human:" | cut -d: -f2 | tr -d 'r')
USED_MEMORY_PEAK=$(echo "$INFO" | grep "used_memory_peak_human:" | cut -d: -f2 | tr -d 'r')
CONNECTED_CLIENTS=$(echo "$INFO" | grep "connected_clients:" | cut -d: -f2 | tr -d 'r')
KEYSPACE_HITS=$(echo "$INFO" | grep "keyspace_hits:" | cut -d: -f2 | tr -d 'r')
KEYSPACE_MISSES=$(echo "$INFO" | grep "keyspace_misses:" | cut -d: -f2 | tr -d 'r')
EVICTED_KEYS=$(echo "$INFO" | grep "evicted_keys:" | cut -d: -f2 | tr -d 'r')
# Calculate hit rate
TOTAL=$((KEYSPACE_HITS + KEYSPACE_MISSES))
if [ $TOTAL -gt 0 ]; then
HIT_RATE=$(awk "BEGIN {printf "%.2f", ($KEYSPACE_HITS / $TOTAL) * 100}")
else
HIT_RATE=0
fi
# Output for Prometheus node exporter textfile collector
cat > /var/lib/node_exporter/textfile_collector/redis.prom <
Cache Warmer for Critical Pages
urls = array_merge( $this->urls, $urls );
}
/**
* Warm critical pages
*/
public function warm_cache() {
// Get critical URLs
$critical_urls = $this->get_critical_urls();
// Process in batches
$batches = array_chunk( $critical_urls, $this->batch_size );
foreach ( $batches as $batch ) {
$this->process_batch( $batch );
}
}
/**
* Get critical URLs to warm
*/
private function get_critical_urls() {
$urls = [];
// Homepage
$urls[] = home_url( '/' );
// Top 20 posts by views
$popular = new WP_Query( [
'post_type' => 'post',
'posts_per_page' => 20,
'meta_key' => 'views',
'orderby' => 'meta_value_num',
'order' => 'DESC',
] );
while ( $popular->have_posts() ) {
$popular->the_post();
$urls[] = get_permalink();
}
wp_reset_postdata();
// Main category archives
$categories = get_categories( [ 'number' => 10, 'orderby' => 'count', 'order' => 'DESC' ] );
foreach ( $categories as $category ) {
$urls[] = get_category_link( $category->term_id );
}
// Add custom URLs
$urls = array_merge( $urls, $this->urls );
return array_unique( $urls );
}
/**
* Process batch of URLs
*/
private function process_batch( $batch ) {
$multi = curl_multi_init();
$handles = [];
foreach ( $batch as $url ) {
$ch = curl_init( $url );
curl_setopt_array( $ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_TIMEOUT => 30,
CURLOPT_USERAGENT => 'WordPress Cache Warmer',
CURLOPT_HEADER => true,
] );
curl_multi_add_handle( $multi, $ch );
$handles[] = $ch;
}
// Execute all requests
$running = null;
do {
curl_multi_exec( $multi, $running );
curl_multi_select( $multi );
} while ( $running > 0 );
// Clean up
foreach ( $handles as $ch ) {
curl_multi_remove_handle( $multi, $ch );
curl_close( $ch );
}
curl_multi_close( $multi );
}
}
// Initialize
$cache_warmer = new WP_Cache_Warmer();
// Add custom URLs
$cache_warmer->add_urls( [
home_url( '/about' ),
home_url( '/contact' ),
home_url( '/services' ),
] );
Conclusion
Le caching distribué avec Redis et Memcached permet de scaler WordPress à des millions de requêtes par jour:
Choix Redis vs Memcached
Redis - Recommandé si:
Memcached - Recommandé si:
Résultats Attendus
La clé du succès: monitoring continu, invalidation intelligente, et tuning progressif basé sur les métriques réelles.