跪拜 Guibai
← All articles
Backend

MySQL and Redis Data Consistency: Why You Delete, Not Update, the Cache

By 神奇小汤圆 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Cache inconsistency bugs are silent data corruptors that surface as wrong balances, stale permissions, or phantom inventory. This breakdown gives engineers a clear decision matrix—from a simple TTL to a full CDC pipeline—so they can match the complexity of the solution to the actual business risk, rather than cargo-culting a delayed double-delete and hoping it works.

Summary

Keeping a Redis cache consistent with MySQL is a problem of managing failure windows, not finding a perfect atomic write. The baseline strategy—commit the database transaction, then delete the cache key—works for most read-heavy workloads, but it still has edge cases: a slow concurrent read can backfill a stale value, and a deletion failure after a successful commit leaves the cache dirty indefinitely. A mandatory TTL acts as the final safety net. For important data, a single DEL is not enough. A deletion failure must enter a persistent retry queue with exponential backoff and dead-letter alerting. Cross-service scenarios demand a transactional Outbox pattern paired with a message queue, ensuring the cache invalidation event is published atomically with the database change. When multiple services or background jobs write to the database, binlog-based CDC tools like Canal capture all changes and produce centralized invalidation events, though this adds latency and schema governance costs. The article systematically eliminates alternative ordering strategies—updating the cache first, or deleting the cache before the database commit—by showing their wider failure windows under concurrency. It also stresses that not all data belongs in this pattern: balances, inventory deductions, and permission revocations must be decided by a conditional database UPDATE, not a cached value. The goal is not instantaneous consistency but a measurable, recoverable stale-read window where the database remains the single source of truth.

Takeaways
Commit the database transaction first, then delete the cache; this keeps the database as the source of truth and avoids the write-ordering bugs of updating the cache directly.
A mandatory TTL on every cache key is the non-negotiable upper-bound fallback when the deletion pipeline fails completely.
A deletion failure after a successful database commit is a production bug, not a log line; it requires a persistent retry queue with exponential backoff, dead-lettering, and age monitoring.
The transactional Outbox pattern solves the crash window between a database commit and an event publish by writing the invalidation event inside the same local database transaction.
Delayed double-delete can shorten the dirty-read window from a concurrent slow read, but the delay must be tuned to actual P99 read and replication latencies, not a fixed 500ms guess.
Binlog CDC covers all write paths—including background scripts—but introduces latency, schema evolution risk, and the governance problem of mapping one row change to multiple cache keys.
Redis Pub/Sub is at-most-once and cannot serve as a reliable invalidation bus; use a persistent queue or Redis Streams with acknowledgements for critical cache invalidations.
Replica lag can cause a read to backfill a stale value even after a correct primary-side deletion; route critical post-write reads to the primary or use a version token to detect lag.
Balances, inventory, and permissions must not be decided by a cached value; use a conditional UPDATE with a WHERE clause and check the affected row count against the database directly.
Reconciliation jobs should sample by update time and risk, delete stale cache keys rather than overwriting the database, and be rate-limited to avoid hammering both systems.
Conclusions

The core engineering mistake is treating cache consistency as an ordering puzzle rather than a failure-recovery problem. Most teams fixate on the sequence of operations under perfect conditions and ignore the far more common case: the deletion call times out or the process crashes right after commit.

Fixed delays like 'sleep 500ms' for double-delete are a guess disguised as a configuration. The correct delay is a function of measured P99 read latency and replica lag, and it changes as the system evolves, which means it must be monitored, not hardcoded.

Version numbers on database rows are useful for detecting staleness and ordering events, but they do not create an atomic boundary across two independent systems. A slow read can still backfill V1 after V2 is committed unless a separate version fence or lock is implemented.

The decision to delete rather than update the cache is fundamentally about reducing state space: a delete is idempotent and forces a single read-path rebuild, while an update creates a second, divergent serialization path that drifts from the read model.

A reconciliation task that does a full table scan and then hits Redis for every row is itself a denial-of-service risk. The design constraint is that the fix must be cheaper than the inconsistency it repairs.

Concepts & terms
Cache-Aside
An application-level caching pattern where the application first checks the cache; on a miss, it loads data from the database, populates the cache, and returns the result. On writes, the application updates the database and invalidates the cache, letting the next read rebuild it.
Transactional Outbox
A pattern that ensures atomicity between a database write and an event publication by writing the event into an outbox table within the same local database transaction. A separate relay or CDC process then reads the outbox and publishes the event to a message queue.
Delayed Double-Delete
A strategy where, after updating the database and deleting the cache, a second deletion is scheduled after a short delay. It aims to clean up stale values that a concurrent slow read might have backfilled into the cache between the first delete and the database commit becoming visible.
CDC (Change Data Capture)
A technique that monitors a database's transaction log (binlog in MySQL) to capture row-level changes as they happen. Tools like Canal or Debezium read this log and emit events, allowing cache invalidation to cover writes from any source, including background jobs and direct SQL.
Read-Your-Writes
A consistency guarantee that a user who makes a change will always see that change in subsequent reads. In a cached system, this can be achieved by returning the post-commit object directly in the write response or by routing the user's short-term reads to the primary database.
Single-flight (Mutex Rebuild)
A concurrency control pattern where, after a cache miss, only one request is allowed to query the database and rebuild the cache. All other concurrent requests either wait for that result or are served a potentially stale value, preventing a thundering herd from overwhelming the database.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗