Ensuring Cache‑Database Consistency: Strategies, Pitfalls, and Delayed Double‑Delete
This article examines common cache‑database consistency approaches, analyzes their drawbacks through concrete examples, compares them in a decision matrix, and proposes a reliable solution—delayed double‑delete with retry mechanisms—detailing implementation code, timing considerations, and practical deployment tips for high‑read, low‑write services.
Problem Statement
Ensuring consistency between a cache (e.g., Redis) and the underlying database under concurrent updates is difficult. The article analyses four typical update orders, shows how each can produce stale or dirty data, compares their risk levels, and proposes a robust solution.
Four Update Orders and Their Issues
1. Cache → Database
First writes the new value to the cache and then updates the database. If the cache write succeeds but the database write fails, the system ends up with inconsistent data because the cache reflects a state that is not persisted.
2. Database → Cache (Double Write)
Updates the database and immediately writes the same value to the cache. This pattern is vulnerable to race conditions: concurrent writers may interleave reads and writes, causing a later read to fetch a stale value that is then written back to the cache.
updateDB();
updateRedis();Example: after updateDB() but before updateRedis(), another request reads the old value from the database and populates the cache, so the subsequent cache write overwrites it with outdated data.
3. Delete Cache → Database
Deletes the cache entry first, then updates the database. If a read request arrives between the deletion and the database update, it will read the old value from the database and repopulate the cache with stale data.
redis.del(key);
updateDB();Step‑by‑step illustration: request A deletes the cache and writes the new value to the database; request B reads the cache (miss), queries the database (still old), and writes the old value back to the cache, creating inconsistency.
4. Database → Delete Cache
Updates the database first and then deletes the cache. If a read request occurs before the database update and the cache has already expired, the read will fetch the old value from the database, and the subsequent cache deletion will not prevent the stale value from being cached later.
updateDB();
redis.del(key);This scenario is less likely because reads are usually faster than writes, but it can still happen when read latency exceeds write latency.
Comparison of Strategies
Cache → Database : Highest inconsistency risk; not recommended.
Database → Cache (Double Write) : May write dirty data under concurrent writes; unsuitable for write‑heavy workloads.
Delete Cache → Database : High risk when reads are frequent; not recommended.
Database → Delete Cache : Lowest risk when read latency is greater than write latency; marginally recommended.
Recommended Approach: Delayed Double Delete
The safest pattern is to delete the cache both before and after the database update, inserting a short pause between the two deletions to allow in‑flight reads to complete.
public void write(String key, Object data) {
redis.del(key); // 1. Remove stale cache
db.update(data); // 2. Persist new value
Thread.sleep(1000); // 3. Wait (adjust to read latency)
redis.del(key); // 4. Remove any cache that might have been repopulated
}First delete the cache to prevent reads from hitting stale data.
Write the new value to the database.
Sleep for a duration roughly equal to the maximum read‑operation latency.
Delete the cache again to clean any entry that may have been written by a concurrent read.
Handling Throughput and Deletion Failures
Make the second deletion asynchronous (e.g., submit a delayed task to a thread‑pool or scheduler) to avoid blocking the write path.
If a deletion fails, enqueue the key for retry (e.g., via a message queue), though this introduces tighter coupling.
Delay Mechanisms
Various implementations can provide the pause: Thread.sleep(...) (simple blocking).
ScheduledThreadPoolExecutor or Quartz jobs.
Netty’s HashWheelTimer.
Message‑queue delayed tasks (e.g., RabbitMQ delayed exchange).
Practical Scenario
In a product‑center service with read‑heavy, write‑light traffic, writes trigger downstream MQ notifications. Strict cache‑DB consistency is required to avoid delivering stale product data.
Write‑Cache Strategies
Assign a TTL to cache keys.
Perform the DB operation first and rely on cache expiration.
Mark keys (via middleware) to force reads from the primary DB.
Listen to binlog changes with middleware and delete the cache as a fallback.
Read‑Cache Strategies
Determine whether the request must go to the primary DB.
If yes, read directly from the DB.
Otherwise, attempt to read from the cache.
If the cache hits, return the cached value.
If the cache misses, read from the DB and populate the cache.
Cache Expiration Note
Without an expiration time, inconsistencies persist until the next update. Always configure a TTL for cached entries to bound the window of potential staleness.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
ITPUB
Official ITPUB account sharing technical insights, community news, and exciting events.
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.
