Transactions, Scripting, and PipelinesLesson 5.2
WATCH and optimistic locking in Redis: how to handle concurrent updates
WATCH command, optimistic locking, CAS pattern, MULTI after WATCH, EXEC returning nil on conflict, retry loop, WATCH vs pessimistic locking
Optimistic locking with WATCH
WATCH implements optimistic locking. It monitors one or more keys. If any watched key is modified by another client before your EXEC, the transaction is aborted — EXEC returns nil instead of the results array.
Pattern: safe balance transfer
async function transfer(from, to, amount) {
while (true) {
await client.watch(`balance:${from}`);
const balance = parseInt(await client.get(`balance:${from}`));
if (balance < amount) {
await client.unwatch();
throw new Error('Insufficient funds');
}
const result = await client
.multi()
.decrBy(`balance:${from}`, amount)
.incrBy(`balance:${to}`, amount)
.exec();
if (result !== null) return result; // success
// null means conflict — retry
}
}When WATCH is appropriate
Use WATCH when you need to read a value, make a decision based on it, and write — and the decision must be based on fresh data. Under high contention the retry loop can spin, so WATCH is best for low-to-medium contention. For high contention, a Lua script (lesson 5.4) is faster because it runs entirely server-side without round trips.
