Why Do Major Tech Companies Shun the Popular Delayed Double‑Delete Cache Strategy?
The article examines the delayed double‑delete cache invalidation technique, reveals its hidden risk of cache‑penetration‑induced database load, compares Facebook's lease‑based solution and Uber's version‑number approach, and advises when the method is appropriate for high‑traffic systems.
Problem with delayed double‑delete
Cache‑aside patterns often delete a cache entry after a DB write and add a delayed second delete (1–2 s) to reduce inconsistency windows. The two rapid deletions can both miss the cache, forcing the primary DB to serve the request. In low‑traffic services this extra load is tolerable, but in high‑traffic services the sudden DB surge can be intolerable.
Facebook lease mechanism
Facebook’s 2013 paper “Scaling Memcache at Facebook” proposes a lease (lock‑like) mechanism.
Lease acquisition
If a key is missing, Redis returns a 64‑bit token and stores it under a lease key.
The client must present the token on subsequent writes; Redis validates the token before storing data.
Other requests must wait for the lease to expire before acquiring a new one.
Lua script for get (lease acquisition):
local key = KEYS[1]
local token = ARGV[1]
local value = redis.call('get', key)
if not value then
redis.replicate_commands()
local lease_key = 'lease:'..key
redis.call('set', lease_key, token)
return {false, false}
else
return {value, true}
endLua script for set (lease validation):
local key = KEYS[1]
local token = ARGV[1]
local value = ARGV[2]
local lease_key = 'lease:'..key
local lease_value = redis.call('get', lease_key)
if lease_value == token then
redis.replicate_commands()
redis.call('set', key, value)
return {value, true}
else
return {false, false}
endApplication impact:
Operations must be performed via EVAL instead of raw Redis commands.
Redis returns an array; callers must parse it.
Clients generate a token per request and follow a three‑step workflow (get → optional DB load → set).
Java wrapper for the result:
public class EvalResult {
String value;
boolean effect;
public EvalResult(List<?> args) {
value = (String) args.get(0);
effect = args.get(1) != null && 1 == (long) args.get(1);
}
}Java lease‑aware client:
public class LeaseWrapper extends Jedis implements CacheCommands {
private final Jedis jedis;
private final TokenGenerator tokenGenerator = () -> UUID.randomUUID().toString();
private final ThreadLocal<String> tokenHolder = new ThreadLocal<>();
public LeaseWrapper(Jedis jedis) { this.jedis = jedis; }
@Override
public String get(String key) {
String token = tokenGenerator.get();
tokenHolder.set(token);
Object result = jedis.eval(LuaScripts.leaseGet(), List.of(key), List.of(token));
EvalResult er = new EvalResult((List<?>) result);
return er.effect ? er.value : null;
}
@Override
public String set(String key, String value) {
String token = tokenHolder.get();
tokenHolder.remove();
Object result = jedis.eval(LuaScripts.leaseSet(), List.of(key), List.of(token, value));
EvalResult er = new EvalResult((List<?>) result);
return er.effect ? er.value : null;
}
}Uber version‑number solution
Uber’s 2024 blog “How Uber Serves Over 40 Million Reads Per Second from Online Storage Using an Integrated Cache” stores a timestamp version alongside each record. Writes compare the incoming version with the stored version; only newer data overwrites the cache.
Lua script for versioned set (string value):
local key = KEYS[1]
local value = ARGV[1]
local current_version = ARGV[2]
local version_key = 'version:'..key
local version_value = redis.call('get', version_key)
if version_value == false or version_value < current_version then
redis.call('mset', version_key, current_version, key, value)
return {value, true}
else
return {false, false}
endJava wrapper for versioned writes:
public class VersionWrapper extends Jedis implements CacheCommands {
private final Jedis jedis;
public VersionWrapper(Jedis jedis) { this.jedis = jedis; }
public String set(String key, String value, String version) {
Object result = jedis.eval(LuaScripts.versionSet(),
List.of(key),
List.of(value, version));
EvalResult er = new EvalResult((List<?>) result);
return er.effect ? er.value : null;
}
}Uber combines this with an asynchronous Flux‑based component that introduces a second‑level delay of a few seconds, reducing cache‑penetration risk while keeping latency acceptable for most services.
Trade‑off analysis
Delayed double‑delete is simple but can cause a short‑term DB overload in high‑traffic scenarios.
Lease mechanism prevents concurrent cache misses at the cost of added client complexity (token management, Lua scripts, EVAL calls).
Version‑number approach avoids stale writes but requires storing an extra version key and performing version comparison on each write; it fits naturally with asynchronous update pipelines.
Conclusion
For low‑traffic services the delayed double‑delete may be acceptable. High‑traffic services should consider alternatives such as Facebook’s lease mechanism or Uber’s version‑based updates, selecting the approach that matches traffic profile, team expertise, and infrastructure capabilities.
dbaplus Community
Enterprise-level professional community for Database, BigData, and AIOps. Daily original articles, weekly online tech talks, monthly offline salons, and quarterly XCOPS&DAMS conferences—delivered by industry experts.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
