Avance 3 min de lecture · 504 mots

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

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:

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

    Laisser un commentaire