Avance 3 min de lecture · 573 mots

WordPress à Grande Échelle : Caching Distribué avec Redis et Memcached

Estimated reading time: 3 minutes

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 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

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 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

WordPress Redis Object Cache Implementation


  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 );
}

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 slabreassign
-o slabautomove
-o lrucrawler
-o lrumaintainer

# Security
# Use firewall rules instead of SASL for better performance

WordPress Memcached Object Cache


  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 ];
    }
}

Fragment Caching Strategies

Advanced Fragment Caching Helper


  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 );

Query Result Caching

Advanced Database Query Cache


  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' ] );

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

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 <usedmemorybytes 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"

Cache Warmer for Critical Pages


  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' ),
] );

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:

  • Besoin de persistence des données
  • Structures de données avancées (Lists, Sets, Sorted Sets)
  • Pub/Sub messaging
  • Support Lua scripting
  • Replication et Sentinel pour HA
  • Memcached – Recommandé si:

  • Performance brute maximale (légèrement plus rapide)
  • Simplicité de déploiement
  • Multi-threading natif
  • Utilisation mémoire optimale
  • Résultats Attendus

  • Hit Rate: >90% avec une stratégie bien configurée
  • Latence: <5ms pour les opérations de cache
  • Réduction charge DB: 70-90%
  • Throughput: 10x-50x amélioration
  • Coût: ROI positif dès 100,000 pages vues/jour
  • La clé du succès: monitoring continu, invalidation intelligente, et tuning progressif basé sur les métriques réelles.

    Une remarque, un retour ?

    Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.