@@ -36,8 +36,11 @@ class Redis extends Cache implements IMemcacheTTL {
3636
3737 private const MAX_TTL = 30 * 24 * 60 * 60 ; // 1 month
3838
39+ /** Number of keys to request per SCAN iteration in {@see self::clear()} (only a hint to Redis) */
40+ private const SCAN_COUNT = 1000 ;
41+
3942 /**
40- * @var \Redis|\RedisCluster $cache
43+ * @var \Redis|\RedisCluster|null $cache
4144 */
4245 private static $ cache = null ;
4346
@@ -88,12 +91,45 @@ public function remove($key) {
8891 }
8992
9093 public function clear ($ prefix = '' ) {
91- // TODO: this is slow and would fail with Redis cluster
92- $ prefix = $ this ->getPrefix () . $ prefix . '* ' ;
93- $ keys = $ this ->getCache ()->keys ($ prefix );
94- $ deleted = $ this ->getCache ()->del ($ keys );
94+ $ pattern = $ this ->getPrefix () . $ prefix . '* ' ;
95+ $ cache = $ this ->getCache ();
96+
97+ // Iterate with SCAN and remove with UNLINK rather than KEYS + DEL:
98+ // KEYS walks the whole keyspace and blocks the server, while a
99+ // multi-key DEL/UNLINK is not cluster-safe (keys spanning hash slots
100+ // raise a CROSSSLOT error). SCAN is non-blocking and UNLINK reclaims
101+ // memory in the background.
102+ if ($ cache instanceof \RedisCluster) {
103+ // On a cluster SCAN must be run against each master node, and keys
104+ // are unlinked one at a time so each command stays within a slot.
105+ foreach ($ cache ->_masters () as $ master ) {
106+ $ iterator = null ;
107+ do {
108+ /** @psalm-suppress NullArgument, PossiblyNullArgument the SCAN cursor must start as null (the phpredis stub types it as int) */
109+ $ keys = $ cache ->scan ($ iterator , $ master , $ pattern , self ::SCAN_COUNT );
110+ if ($ keys === false ) {
111+ break ;
112+ }
113+ foreach ($ keys as $ key ) {
114+ $ cache ->unlink ($ key );
115+ }
116+ } while ($ iterator > 0 );
117+ }
118+ } else {
119+ $ iterator = null ;
120+ do {
121+ /** @psalm-suppress NullArgument, PossiblyNullArgument the SCAN cursor must start as null (the phpredis stub types it as int) */
122+ $ keys = $ cache ->scan ($ iterator , $ pattern , self ::SCAN_COUNT );
123+ if ($ keys === false ) {
124+ break ;
125+ }
126+ if ($ keys !== []) {
127+ $ cache ->unlink ($ keys );
128+ }
129+ } while ($ iterator > 0 );
130+ }
95131
96- return ( is_array ( $ keys ) && ( count ( $ keys ) === $ deleted )) ;
132+ return true ;
97133 }
98134
99135 /**
0 commit comments