Backend Development 23 min read

Ensuring Cache and Database Consistency: Strategies and Best Practices

This article explains why cache‑database consistency is a classic problem, analyzes various update‑order and deletion strategies, discusses concurrency and replication delays, and recommends using the "update database then delete cache" approach combined with asynchronous retries via message queues or binlog subscription to achieve reliable eventual consistency.

Full-Stack Internet Architecture
Full-Stack Internet Architecture
Full-Stack Internet Architecture
Ensuring Cache and Database Consistency: Strategies and Best Practices

Reading this article takes about 10 minutes.

Hello, I am Kaito.

How to guarantee cache and database consistency is a long‑standing topic, yet many still have doubts:

Should we update the cache or delete it?

Do we update the database first then delete the cache, or delete the cache first then update the database?

Why introduce a message queue to ensure consistency?

What problems does delayed double‑delete cause, and should we use it?

We will clarify these questions.

Introducing Cache to Improve Performance

We start with the simplest scenario.

If your service is at an early stage with very low traffic, you can read and write directly to the database.

As traffic grows, reading every request from the database becomes a performance bottleneck.

The usual solution is to introduce a cache to improve read performance, changing the architecture to:

Redis is a popular cache middleware with high performance and rich data types.

After adding a cache, a key question appears: how to store data that previously existed only in the database into the cache?

The simplest solution is to "bulk load" data into the cache:

Push the entire database data into the cache (no expiration).

Write requests only update the database, not the cache.

Run a scheduled task that periodically syncs database data to the cache.

This approach gives very high read performance because all reads hit the cache.

However, it has two obvious drawbacks:

Low cache utilization – rarely accessed data stays in the cache.

Data inconsistency – because the cache is refreshed on a schedule, the cache and database can diverge depending on the task interval.

Therefore this scheme suits small‑scale services with low consistency requirements.

When the service scale is large, we need to solve the above two problems.

Cache Utilization and Consistency Issues

First, how to improve cache utilization?

To maximize utilization, we usually keep only the "hot" data (recently accessed) in the cache. The optimization is:

Write requests still write only to the database.

Read requests first try the cache; if a miss occurs, read from the database and rebuild the cache.

Set an expiration time for data written into the cache.

With expiration, stale data gradually disappears, leaving only hot data in the cache.

Next, data consistency.

To keep cache and database "real‑time" consistent, we cannot rely on scheduled refreshes any more.

When data is updated, we must update both the database and the cache. Two possible orders exist:

Update the cache first, then the database.

Update the database first, then the cache.

Both orders work under normal conditions, but if the second step fails, inconsistency occurs.

Consider the failure scenarios:

1) Cache first, then database: If the cache update succeeds but the database update fails, the cache holds the new value while the database holds the old one. After the cache expires, the system will read the old value from the database, causing the data to revert.

2) Database first, then cache: If the database update succeeds but the cache update fails, the database has the new value while the cache still returns the old value until it expires.

Thus, any failure in the second step can break consistency.

Concurrency adds another layer of difficulty. Example with two threads updating the same record:

Thread A updates the database (X=1).

Thread B updates the database (X=2).

Thread B updates the cache (X=2).

Thread A updates the cache (X=1).

The final state is X=1 in the cache and X=2 in the database – a classic inconsistency caused by out‑of‑order execution.

Therefore, simply "update both" is not enough.

Deleting Cache to Ensure Consistency

Another approach is to delete the cache instead of updating it. Two orders exist:

Delete the cache first, then update the database.

Update the database first, then delete the cache.

Both suffer from the same "second step failure" problem.

When concurrency is involved, the "delete‑first" order can still lead to inconsistency, as shown by the earlier read‑write interleaving example.

The "update‑first‑then‑delete" order is safer, but we still need to guarantee that the delete operation eventually succeeds.

Asynchronous Retry with Message Queues

To guarantee the second step succeeds, we can offload the cache operation to a message queue and let a dedicated consumer retry until success.

This decouples the critical path from the request thread, avoiding thread blockage and making the system more resilient to transient failures.

Subscribe to Database Change Logs

Instead of writing to a queue manually, we can subscribe to the database's change log (e.g., MySQL binlog) using tools like Alibaba Canal. When a row changes, the listener deletes the corresponding cache entry.

Advantages:

No need to handle queue‑write failures – if the DB write succeeds, the binlog is guaranteed.

Canal automatically pushes the change events to downstream queues.

However, this requires maintaining the Canal service.

Delayed Double Delete Strategy

In some scenarios, especially with read‑write splitting and master‑slave replication lag, a single delete may not be enough because another thread might read stale data from the slave and write it back to the cache.

The delayed double‑delete strategy works as follows:

After deleting the cache and updating the database, wait for a short period, then delete the cache again.

Alternatively, send a delayed message to a queue that performs the second delete.

The delay must be longer than the replication lag and longer than the time a concurrent read‑write thread needs to read from the slave and write back to the cache, which is hard to estimate in high‑concurrency environments.

Because of this uncertainty, the recommended practice is to use the "update database then delete cache" pattern together with asynchronous retries (message queue or binlog subscription).

Can We Achieve Strong Consistency?

Strong consistency usually requires protocols like 2PC, 3PC, Paxos, or Raft, which bring significant performance overhead and complexity.

Since the primary purpose of caching is performance, we typically accept eventual consistency and aim to minimize inconsistency windows.

Techniques such as distributed locks can enforce strong consistency but often negate the performance benefits of caching.

Summary

Introduce a cache to improve performance.

After adding a cache, address utilization and consistency; options include "update‑database + update‑cache" and "update‑database + delete‑cache".

The "update‑database + update‑cache" approach fails under concurrency and wastes cache resources.

For "update‑database + delete‑cache", the "delete‑first" order still has concurrency issues; the "update‑first‑then‑delete" order with delayed double delete is recommended.

To ensure both steps succeed, combine the update‑first‑then‑delete pattern with asynchronous retries via a message queue or binlog subscription.

Read‑write splitting and master‑slave lag also cause inconsistency; delayed double delete and controlling replication delay help mitigate the problem.

Afterword

I am Kaito, a senior backend engineer who not only explains what a technology does but also why it is done, extracting generic methodologies that can be applied elsewhere.

If this article helped you, please like, view, and share it – your support motivates me to produce higher‑quality content.

Recommended reading:

Understanding Multi‑Active Geo‑Distributed Systems

Redis Transactions and ACID

Distributed Locks: Redis vs Redisson

backenddistributed systemsCacheRedisMessage Queuedatabase consistency
Full-Stack Internet Architecture
Written by

Full-Stack Internet Architecture

Introducing full-stack Internet architecture technologies centered on Java

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.