MySQL and Redis Data Consistency: Why You Delete, Not Update, the Cache
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.
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.
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.