Race condition during clearByTag execution in Redis storage adapter
0 reputation · 07 May 2024, 15:02 UTC
0 reputation · 07 May 2024, 15:02 UTC
When utilizing the laminas-cache component with a Redis backend, latency spikes occur during high-concurrency invalidation events. While the adapter provides a unified interface for tagging, the implementation of clearByTag lacks inherent atomicity across distributed nodes.
In a high-load environment, multiple processes may trigger tag-based invalidations simultaneously. Because Laminas-cache does not provide built-in distributed locking for its storage adapters, a race condition emerges where one process may refresh a cache key while another is clearing its associated tag. This results in inconsistent cache states or cache stampedes.
How can the Redis storage adapter ensure atomicity during tag-clearing operations without external synchronization? Additionally, is there a recommended configuration within Laminas to prevent race conditions when O(n) complexity key-mapping occurs?
35275 reputation · 07 May 2024, 22:58 UTC · edited 16 Sept 2026, 09:10 UTC
In Laminas‑Cache the clearByTag method is implemented as a sequence of Redis commands: read the tag set, delete each key, delete the tag set. Each command is atomic, but the whole sequence is not, so two concurrent calls can interleave and leave stale keys. The only way to guarantee indivisibility inside Redis is to wrap the whole operation in a Lua script that runs on the server side.
SMEMBERS for small sets or a SSCAN loop for very large sets).DEL for each key.DEL tagKey).SCRIPT LOAD and keep the returned SHA1.EVALSHA. For every tag invalidation call the adapter to run EVALSHA <sha1> 1 tagKey. The script runs atomically, so no other client can modify the tag set while it is being processed.The Redis adapter allows passing a custom script via the clearByTagScript option (or by subclassing the adapter). Example configuration:
$cache = new Laminas\Cache\Storage\Adapter\Redis([
'redis' => [
'host' => '127.0.0.1',
'port' => 6379,
],
'clearByTagScript' => 'sha1-of-your-lua-script',
]);
When clearByTag is called, the adapter will automatically invoke the script via EVALSHA. No external locking is required.
SSCAN loop inside the script to delete keys in batches.To fine‑tune the script you need to know the typical size of a tag set. If your tags regularly hold more than a few thousand keys, consider a chunking strategy or a background deletion job. If the set size is small, the simple SMEMBERS + DEL loop is sufficient.
Wrap the entire clearByTag logic in a Lua script executed with EVALSHA. Configure the Redis adapter to use that script. For large tag sets, employ SSCAN or batch deletions to stay within Redis limits. No external distributed lock is needed.
Use comments to ask for clarification. Post a solution as an answer.
6,810 reputation · 07 May 2024, 18:22 UTC
The Lua-script answer is correct, but worth flagging a trade-off: because Redis executes scripts atomically on its single command thread, a clearByTag script that iterates a very large tag set blocks every other client for the duration. On a busy instance this can look exactly like the latency spikes you're trying to fix.
Two practical mitigations:
UNLINK instead of DEL inside the script (Redis 4+) so the actual memory reclamation happens asynchronously, keeping the script's blocking portion short.clearByTag for the same tag just does redundant work rather than corrupting state.Also verify your adapter handles NOSCRIPT errors: after a Redis restart or SCRIPT FLUSH, the cached SHA1 is invalid and the client must fall back to EVAL with the full script body. Most clients (e.g., Lettuce, Predis) do this automatically, but a custom adapter subclass may not — worth testing with a restart in staging.