In this lesson, you will learn how to prevent stale data updates in distributed systems by implementing optimistic locking directly within your cache layer.
When two processes read the same cache entry simultaneously and both attempt to update it, you face a "lost update" race condition. If process A reads value and process B reads the same value , they might both calculate and try to set the value to , effectively overwriting each other’s progress.
The Optimistic Locking Approach
Optimistic locking assumes conflicts are rare. Instead of locking the record (which is slow and complex in distributed caches), we attach a version number to the cached data. Before writing an update back to the cache, we verify that the version hasn't changed since we last read it.
In Redis, we achieve this using the WATCH, MULTI, and EXEC command sequence. The WATCH command monitors the key; if another process modifies it before our EXEC, the transaction fails, allowing us to retry.
async function updateProductStock(productId, decrementBy) {
const key = `product:${productId}`;
// Start tracking the key for changes
await redis.watch(key);
const currentStock = await redis.get(key);
const newStock = parseInt(currentStock) - decrementBy;
// Use MULTI to execute update atomically
const result = await redis
.multi()
.set(key, newStock)
.exec();
// result is null if another process changed the key during WATCH
if (result === null) {
console.log("Race condition detected, retrying...");
return updateProductStock(productId, decrementBy); // Recursive retry
}
}
Why This Works
By using the cache as a gatekeeper, we ensure that the write operation only succeeds if our view of the data remains consistent with the current state of the store. If the WATCH is triggered, it signals that our logic was based on "stale" information, and the EXEC will return null, preventing the corrupted update. This keeps your application logic clean while ensuring high data integrity under concurrent traffic.
Key Takeaway: Optimistic locking with versioning or watch-commit patterns prevents race conditions by ensuring writes only succeed if the underlying data has remained unchanged since it was read.