Cache Consistency Strategies: From Simple Write‑Through to Binlog Subscription
This article explains why caching is essential for high‑concurrency systems, analyzes the challenges of keeping database and cache data consistent, and compares five practical cache‑invalidation strategies—including write‑after‑DB, delete‑before‑write, delayed double delete, queue‑based deletion, and binlog subscription—highlighting their trade‑offs and suitable scenarios.
In modern system architectures, cache occupies a very high position because, in the Internet era, request concurrency can be extremely high, and relational databases are not strong at handling such concurrency. Since cache operates in memory without disk I/O, it is ideal for high‑concurrency processing and has become indispensable in many systems.
However, this introduces many problems, the most prominent being how to ensure data consistency between the database and the cache.
Because database operations and cache operations cannot be performed within a single transaction, situations such as database write failure while cache update succeeds, or cache write failure while database update succeeds, can occur. How should we handle these cases?
We first look at a common read‑cache example
The diagram below shows the most widely used read‑through cache pattern. Reading itself is straightforward, but writing to the cache is the key to guaranteeing data consistency.
We do not consider periodic cache refresh methods such as the one shown below.
In this approach, writing to the database and writing to the cache are independent; after the database write, a scheduled service updates the cache. This leads to a long period of inconsistency and unnecessary cache reloads, which is inefficient except for some scenarios like caching system configuration.
Below we focus on double‑write consistency methods and discuss the various ways to write data and refresh the cache.
Method 1 – Update Database First, Then Update Cache
This is the simplest double‑write scheme. After a successful database write, the cache is refreshed. The flowchart is shown below.
While the code is simple and easy to maintain, it suffers from serious concurrency issues.
First, thread safety cannot be guaranteed. Consider two concurrent requests A and B operating on the same record. If A should execute before B, the database will contain B’s result, but the cache update order may be reversed.
The possible execution order is illustrated below.
Request A updates DB → Request B updates DB → Request B updates cache → Request A updates cache.
This leads to:
Inconsistent data between the database and cache, resulting in dirty cache data.
More write operations than reads, causing frequent cache refreshes of data that may never be read, wasting resources.
Therefore, this double‑write method is hard to guarantee consistency and is not recommended.
Method 2 – Delete Cache Before Updating Database
To avoid the above issue, we can delete the cache first, then update the database. When the database is updated, the cache is empty, so subsequent reads will bypass the cache and fetch fresh data from the database, which is then written back to the cache.
However, a new problem appears under high concurrency. Suppose request A writes data while request B reads data at the same time. The sequence may be:
Request A deletes cache → Request B finds cache missing → Request B reads stale value from DB → Request B writes stale value to cache → Request A writes new value to DB.
This again produces dirty data. If the cache has no expiration time, the stale data persists until the next write.
To mitigate this, we introduce a delayed second delete, leading to Method 3.
Method 3 – Delayed Double Delete
By deleting the cache twice—once immediately and once after a short delay—we can reduce the chance of stale reads caused by concurrent operations.
Nevertheless, this approach is not flawless. In a read‑write split scenario, the delayed delete may still cause inconsistency, as shown below.
Sequence: Request A deletes cache and writes to DB → Request B reads cache (empty) and queries the replica, which still holds the old value → Request B writes the old value to cache → The replica later synchronizes and holds the new value, leaving the cache with stale data.
Moreover, the delay hurts performance; making the second delete asynchronous improves speed but introduces the risk of asynchronous failure, which again leaves stale data.
Thus, a more reliable solution is needed.
Method 4 – Queue‑Based Cache Deletion
After updating the database, a message to delete the cache is placed into a reliable queue. If the queue execution fails, the message is re‑queued until it succeeds, ensuring eventual cache consistency even under read‑write split or other complex scenarios.
This decouples cache invalidation from business logic, improving maintainability.
Method 5 – Binlog Subscription for Cache Deletion
To further separate business code from cache refresh logic, we can subscribe to MySQL binlog events. By listening to data‑change logs, we trigger cache invalidation automatically.
This approach reduces code volume, simplifies maintenance, and frees developers from worrying about when to refresh the cache.
In practice, different business scenarios may require different consistency solutions; the methods above provide a reference framework.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.