How to Ensure MySQL‑Redis Cache Consistency: Problems, Scenarios, and Solutions
This article analyses why cache consistency between MySQL and Redis breaks under high concurrency, explains the CAP perspective, walks through five typical failure scenarios, and presents concrete solutions such as delayed double‑delete, retry with MQ, binlog listening, expiration fallback, and mutex locks, ending with a decision guide for selecting the most suitable strategy.
Cache Consistency Problem
Updating the database and updating/deleting the cache are not atomic; in high‑concurrency situations the order and timing of these two operations can cause data inconsistency.
CAP view: Cache systems belong to the AP class, providing eventual consistency rather than strong consistency. Scenarios that require absolute consistency should avoid using a cache.
Key Concurrent Scenarios and Solutions
Scenario 1: Delete cache first, then update database
Problem description:
Thread A deletes a Redis key.
Thread B reads a cache miss, queries MySQL (old value), and writes the old value back to Redis.
Thread A updates MySQL (new value).
Result: Redis stores the old value, MySQL stores the new value → inconsistency.
Why dangerous: A read request that occurs after the cache deletion but before the database update can write stale data back to the cache; the probability is high under high concurrency.
Solution: Delayed Double‑Delete
Delete the Redis key.
Update MySQL.
Delay for a period estimated from the average read latency.
Delete the Redis key again.
Effectiveness: Guarantees that all requests which might have read stale data and written it back have finished before the second deletion.
Delay recommendation: “average read latency + 100 ms”. Example: if average read latency is 300 ms, set the delay to 400‑500 ms.
Scenario 2: Update database first, then delete cache (recommended)
Problem description:
Cache expires exactly when a read occurs.
Thread A reads a cache miss, queries MySQL (old value).
Thread B updates MySQL (new value) and deletes the Redis key.
Thread A writes the old value into Redis.
Result: Redis stores the old value, MySQL stores the new value → inconsistency.
Why recommended: The conditions for inconsistency are extremely strict (cache must expire at the exact moment of the read and the read latency must exceed the combined write‑and‑delete latency); in practice the probability is very low.
Why not 100 % safe: Theoretical inconsistency cannot be fully avoided.
Scenario 3: Cache deletion failure
Problem description: Database update succeeds but Redis deletion fails, leaving the database with the new value and the cache with the old value.
Solution: Cache‑Deletion Retry with MQ
Update MySQL.
Attempt to delete the Redis key.
If deletion fails, place a delete request into a message queue.
A consumer fetches the request from the queue and retries the deletion.
Using MQ for asynchronous retry avoids blocking the main thread and improves system throughput.
Scenario 4: MySQL master‑slave replication lag
Problem description: In a read‑write split architecture, after the master updates, the slave lags; reads from the slave may return stale data.
Solution
Set delayed double‑delete time = average read latency + replication lag + 100 ms.
Or force cache‑updating queries to read from the master directly to avoid slave lag.
Best practice for high‑consistency requirements: read from the master.
Scenario 5: Cache breakdown (stampede)
Problem description: Many concurrent requests access a key that is absent in the cache but present in the database, causing all requests to hit the database.
Solution
Mutex lock on cache miss so that only one request queries the database and repopulates the cache.
Set a reasonable TTL to avoid many keys expiring simultaneously.
Lock granularity should be at the key level to avoid performance impact of a global lock.
General Solution Comparison
Cache‑Aside + delayed double‑delete : eventual consistency with a short inconsistency window; low implementation difficulty; suitable for general scenarios; tiny probability of inconsistency.
Delete‑retry + MQ : eventual consistency; medium difficulty; suited for high‑reliability cases; introduces MQ complexity.
Binlog listening (Canal + Kafka) : eventual consistency; high difficulty; fits large‑scale multi‑service systems; requires maintaining middleware.
Cache expiration fallback : simple; low difficulty; applicable to all scenarios; may reduce cache hit rate.
Delete‑first‑then‑update : weak consistency; low difficulty; only for low‑concurrency cases; high risk under heavy load.
Core Implementation Examples
Cache‑Aside + Delayed Double‑Delete (Java)
public void updateData(Long id, String newValue) {
// 1. Update MySQL
mysql.update(id, newValue);
// 2. Delete Redis cache
redis.delete("data:" + id);
// 3. After a delay, delete again to avoid stale writes
new Thread(() -> {
try {
Thread.sleep(500); // delay in ms (e.g., avg latency 300 ms + 100 ms)
redis.delete("data:" + id);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}The second deletion runs asynchronously to avoid blocking the main flow.
Cache‑Deletion Retry with MQ (Java)
public void updateDataWithRetry(Long id, String newValue) {
mysql.update(id, newValue);
boolean deleteSuccess = false;
for (int i = 0; i < 3; i++) {
try {
redis.delete("data:" + id);
deleteSuccess = true;
break;
} catch (Exception e) {
if (i == 2) {
// After three failures, enqueue for async retry
mq.send("delete-cache", id);
}
}
}
}Binlog Listening Scheme (Complex Systems)
Architecture: Application → MySQL → Canal → Kafka → Consumer → Redis
Canal listens to MySQL binlog.
Kafka provides a reliable message queue.
Consumer processes messages and deletes the corresponding Redis entries.
Advantage: decouples synchronization from business logic, suitable for large‑scale systems.
Ultimate Consistency Safeguards
Cache expiration (must set) : typical TTL 5‑30 minutes based on business needs.
Data verification mechanism : periodically (e.g., daily) compare cache and database and alert via monitoring tools such as Prometheus + Grafana.
Version/timestamp control : add a version field to data; on read, compare cache version with DB version and reload if mismatched.
public Data getData(Long id) {
Data data = redis.get("data:" + id);
if (data != null) {
// Compare version
if (data.getVersion() != mysql.getVersion(id)) {
data = mysql.get(id);
redis.set("data:" + id, data);
}
return data;
}
// Cache miss, load from DB
data = mysql.get(id);
redis.set("data:" + id, data);
return data;
}Selection Guidance
General read‑heavy, write‑light scenarios: use Cache‑Aside + delayed double‑delete (≈500 ms) and set TTL 10‑30 minutes.
High‑reliability scenarios (finance, transaction systems): adopt the Binlog listening scheme (Canal + Kafka) together with delete‑retry via MQ and version verification.
High‑concurrency write scenarios (likes, view counts): prefer Write‑Behind (write to Redis first, then asynchronously persist to MySQL), tolerate short‑term inconsistency, and use a short TTL (5‑10 minutes).
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.
Programmer1970
Formerly called 'Code to 35'. Add our main WeChat ID to access a wealth of shared resources (algorithms, interview prep, tech stacks: Java, Python, Go, big data). We mainly share serious development techniques, focusing on output-driven input. Occasionally we post life snippets and gossip. Our aim is to attract precise traffic and test advertising opportunities.
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.
