Repository navigation
Use Redis Hash Field Expiration to keep cache tag indexes always consistent and eliminate stale-tag maintenance (Redis >= 7.4) #61364
Replies: 3 comments
|
I think HFE is a very good fit for this, but there are a few implementation details worth addressing before replacing the current sorted-set approach. The biggest one is that this would eliminate TTL-induced stale references, but not necessarily make the tag index universally "always consistent". For example, a cache entry may disappear before its TTL because of an explicit There are also a few important compatibility/implementation details: 1. Existing tag keys cannot simply change from ZSET to HASHToday Laravel uses keys such as: as sorted sets. If an upgraded application starts issuing So the HFE implementation would need either:
This is probably the most important upgrade-path issue. 2.
|
|
@mostafanajee-coder Thanks for your feedback! All points are pretty much valid and I agree we need: backward compatibility, clear migration path, consistency between data and metadata |
|
Related: #61328 Recently fixed the write/flush race in the same Redis tagged-cache path. This proposal addresses the separate accumulation of expired finite-TTL references. |
Uh oh!
There was an error while loading. Please reload this page.
Problem
RedisTagSettracks which cache keys belong to a tag using a sorted set per tag (tag:{name}:entries), where the member is the cache key and the score is its absolute expiration timestamp (RedisTagSet::addEntry(),src/Illuminate/Cache/RedisTagSet.php:19-29). Redis sorted sets have no concept of per-member expiration, so this score is purely bookkeeping data that Laravel itself must act on.When a tagged cache entry's TTL elapses, the entry's own key disappears — but its reference inside the tag's sorted set does not. Nothing removes it automatically. The only cleanup mechanism is:
RedisTagSet::flushStaleEntries()(RedisTagSet.php:86-101), which issuesZREMRANGEBYSCOREto prune members whose score is in the pastcache:prune-stale-tagsartisan command (src/Illuminate/Cache/Console/PruneStaleTagsCommand.php), which application owners must remember to add to their own scheduler.If that command is never scheduled — which is easy to forget, since nothing fails or warns when it isn't — tag sorted sets grow unbounded with references to cache entries that no longer exist. This has two visible costs as it grows:
RedisTaggedCache::flush()andRedisTagSet::entries()(RedisTagSet.php:37-79) iterate every member viaZSCAN, including all the dead ones, issuing wastedDELcalls against keys that vanished long ago.This is a workaround for something Redis didn't support when this code was written: per-member expiration on a collection type.
Proposal
Redis 7.4 introduced Hash Field Expiration (
HEXPIRE,HPEXPIRE,HPERSIST,HTTL, plusHSCANover fields) — TTLs scoped to individual hash fields rather than the whole key. This is the primitive that was missing.For servers that support it, back each tag with a hash instead of a sorted set: field = cache key, and the field's own TTL (set via
HEXPIRE, using the same relative$ttlalready passed intoaddEntry()) mirrors the underlying cache entry's expiration. Redis then expires the field itself — no score bookkeeping, no scheduled prune job, no possibility of unbounded ghost accumulation.flushStaleEntries()andcache:prune-stale-tagsbecome unnecessary for correctness on these servers.Benefits
flushStaleEntries()ran — between prunes, it's a mix of live and dead references, and on a long-running or unscheduled app it trends toward mostly dead. With HFE, a field is gone the instant it expires:entries()andflush()are always operating on the true live set, not a snapshot that's stale by however long it's been since the last cron run.flush()/entries()as a direct consequence, not a side effect. Because the index never accumulates dead references,ZSCAN-then-DEL-against-already-gone-keys work disappears entirely — this scales with how long a tag has been alive and how much traffic has churned through it, so the win compounds over the life of a long-running app rather than being a one-time fixed saving.cache:prune-stale-tags— a command that's easy to forget to schedule, and whose omission silently degrades the two points above over time rather than failing loudly.addEntry()— no need to compute an absolute expiry timestamp as a sorted-set score; pass the relative TTL straight toHEXPIRE.Would be happy to contribute a PR for this!
All reactions