Optimistic Transactions with BoostRedis
Introduction
Welcome back! In the previous lesson, you learned how to batch multiple commands into a single request using Boost.Redis to reduce network round trips. Batching improves performance, but it is not atomic — other clients may still change data between your batched reads and writes.
This lesson introduces WATCH together with MULTI/EXEC, which you use to build conditional, optimistic-locking transactions. The idea is:
MULTI/EXECmakes a group of writes execute atomically.WATCHmonitors one or more keys and causesEXECto abort if any of those keys change afterWATCHand beforeEXEC.
In short: batching is non-atomic; atomicity requires MULTI/EXEC. WATCH ensures the keys stay unchanged between your read and your EXEC.
What You'll Learn
By the end of this lesson, you will be able to:
- Send
WATCHfor one or more keys usingrequest.push(). - Combine
WATCHwithMULTI/EXECto perform conditional updates. - Detect conflicts via
EXEC's reply (nil/aborted), not via exceptions. - Follow the typical optimistic sequence (on the same connection):
WATCH- read
- compute
MULTI- write
EXEC(retry if aborted)
Remember: request batching by itself is not atomic. Transactions require MULTI/EXEC, and WATCH ensures the keys you care about didn't change.
The Problem: Race Conditions in Read-Modify-Write
Imagine you're building a banking application. You want to update a user's balance by adding money to their account. The naive approach looks like this:
GET balance:1→ returns "100"- Calculate in C++: 100 + 50 = 150
SET balance:1 150
What's the problem? Between steps 1 and 3, another client might also read the balance, compute a new value, and write it back. You might lose updates:
- Client A reads 100, adds 50 → wants to write 150
- Client B reads 100, adds 30 → wants to write 130
- Client B writes 130 first
- Client A writes 150 second
- Result: Client B's +30 is lost! The balance should be 180, not 150.
This is called a race condition. Even if you batch the commands, they're not atomic — other clients can still interleave their operations.
How WATCH Solves This
WATCH is Redis's optimistic locking mechanism. Here's how it works:
- Before reading, you tell Redis: "Watch this key for changes"
- Redis remembers which keys you're watching
- You read the key and compute your new value
- When you execute
MULTI/EXEC, Redis checks: "Did any watched key change since WATCH?"- If yes:
EXECreturnsnil(aborted), and nothing is written - If no: Your transaction executes atomically
- If yes:
This is called optimistic locking because you optimistically assume no conflict will occur. If one does, you detect it and retry.
Key insight: WATCH doesn't lock the key or block other clients. It just monitors for changes. If a change happens, you find out during EXEC.
Connection-specific (reliability) insight: WATCH is stored on the server per connection. That means:
- You must run the whole sequence (
WATCH→ read →MULTI/EXEC) on the same Redis connection. - If the connection drops or is reset, the watched state is lost. In practice, you must treat a disconnect as “transaction attempt failed” and retry from the beginning after reconnecting.
- Don’t share one connection across concurrent optimistic transactions unless you strictly serialize them; otherwise one transaction’s
WATCHcan interfere with another’s.
