How to Achieve Near‑Perfect Cache Consistency: Double‑Check, Queues, and Advanced Strategies
This article walks through the fundamentals and advanced techniques for guaranteeing cache consistency, covering the Double‑Check pattern, root causes of inconsistency, interview‑ready questions, practical solutions like message queues, optimistic locking, multi‑level caching, and cutting‑edge schemes such as consistent hashing with singleflight.
Double‑Check Pattern
The double‑check (check‑lock‑check) pattern avoids expensive locking for the common fast path while guaranteeing correctness for the rare write path.
// Go example of double‑check
func doubleCheck() {
// first check under read lock
rlock()
if !checkSomething() {
runlock() // release read lock before acquiring write lock
lock() // exclusive write lock
if !checkSomething() { // second check – data may have been loaded by another goroutine
doHeavyBusinessLogic()
}
unlock() // release write lock
return
}
runlock() // fast path – data already present
}Root Causes of Cache Inconsistency
Partial operation failure – e.g., DB update succeeds but cache update fails due to network or service outage.
Race condition caused by concurrent updates – multiple threads modify the same key in different orders.
Partial Failure
Typical updates involve two independent network calls (MySQL then Redis). Because most cache systems do not support XA‑style distributed transactions, a failure between the two steps leaves the system in an inconsistent state (DB updated, cache stale). Compensation (retries, async MQ, periodic reconciliation) is required to achieve eventual consistency.
Race Condition
Consider two threads updating an order status:
Thread A updates DB to “Paid”.
Thread B updates DB to “Shipped”.
Thread B writes “Shipped” to cache first.
Thread A later writes its stale “Paid” value to cache.
Result: DB = “Shipped”, cache = “Paid”. The fix is to serialize the updates.
Core Solutions
Message‑Queue Serialization
Transform every write request into a message and let a single consumer process them sequentially.
Encapsulate the update as an MQ message instead of hitting the DB directly.
Route messages of the same business ID to the same partition (Kafka, RocketMQ, etc.).
The consumer pulls messages in order.
For each message, execute “update DB → update cache”.
This guarantees ordering but turns a synchronous write into an asynchronous one, which may not satisfy strict real‑time requirements.
Optimistic Lock & Versioning
Add a version (or last‑updated timestamp) column to the table. Increment it on each update and only overwrite the cache when the incoming version is greater than the cached version.
// Pseudo‑code for version check
if newVersion > cachedVersion {
cache.set(key, newData, newVersion)
}Redis Lua scripts can make the compare‑and‑set operation atomic.
Multi‑Level Cache Update Order
In a three‑layer stack (Local Cache → Redis → Database), the safe update sequence is:
Update the database (source of truth).
Update the local in‑process cache.
Update the remote Redis cache.
This order ensures high reliability, immediate “read‑your‑writes” experience, and a fallback when remote cache updates fail.
Advanced Techniques
Consistent Hashing + Singleflight
Route all requests for the same key to the same node using consistent hashing. Within the node, apply a Singleflight‑style request‑merging so that only one goroutine performs the expensive DB read while others wait for the result. This reduces a distributed concurrency problem to a single‑machine one.
Distributed‑Lock Variants
Variant 1 – Transaction → Lock → Delete Cache → Release
Begin a local DB transaction.
Execute the business update.
Acquire a distributed lock.
Delete the cache entry.
Commit the transaction.
Release the lock.
Read side must also acquire the lock and double‑check the cache to avoid stale reads.
Variant 2 – Delete‑Then‑Commit
Begin a local transaction.
Update the DB.
Acquire a distributed lock.
Delete the cache.
Commit the transaction.
Release the lock.
This keeps the cache empty while the transaction commits, providing near‑strong consistency, but holding the lock inside the transaction can increase latency and reduce throughput.
Summary
Cache consistency boils down to two fundamental problems: partial failures (mitigated by compensation/retry) and concurrent race conditions (mitigated by serialization). Conventional tools such as message queues, version numbers, and optimistic locks address most scenarios. Advanced schemes like consistent hashing with singleflight or carefully ordered distributed‑lock workflows push the consistency envelope further. Choose the solution that balances performance, complexity, and the business’s tolerance for stale data.
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.
IT Services Circle
Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.
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.
