Welcome back! Previously, you learned how to build and execute basic transactions in Redis using pipelines in C++. In this lesson, we are going a step further by introducing the concept of monitoring keys to control transaction execution. This approach is essential for scenarios where you need to ensure operations are completed only when specific conditions are met.
Let's take a look at how you can implement key monitoring alongside transaction handling in your code using hiredis.
C++1#include <iostream> 2#include <hiredis/hiredis.h> 3#include <string> 4 5void updateBalance(redisContext* context, int user_id, int increment) { 6 while (true) { 7 redisReply* reply; 8 9 // WATCH command 10 reply = (redisReply*)redisCommand(context, "WATCH balance:%d", user_id); 11 freeReplyObject(reply); 12 13 // Get current balance 14 reply = (redisReply*)redisCommand(context, "GET balance:%d", user_id); 15 int balance = (reply->type == REDIS_REPLY_NIL) ? 0 : std::stoi(reply->str); 16 freeReplyObject(reply); 17 18 // MULTI command to start a transaction 19 reply = (redisReply*)redisCommand(context, "MULTI"); 20 freeReplyObject(reply); 21 22 // Set new balance 23 reply = (redisReply*)redisCommand(context, "SET balance:%d %d", user_id, balance + increment); 24 freeReplyObject(reply); 25 26 // EXEC command to execute transaction 27 reply = (redisReply*)redisCommand(context, "EXEC"); 28 29 if (reply->type == REDIS_REPLY_ARRAY && reply->elements == 1) { 30 freeReplyObject(reply); 31 break; // Transaction succeeded 32 } 33 34 std::cerr << "Retrying transaction due to an external modification." << std::endl; 35 freeReplyObject(reply); 36 } 37} 38 39int main() { 40 // Connect to the Redis server 41 redisContext* context = redisConnect("127.0.0.1", 6379); 42 if (context == nullptr || context->err) { 43 if (context) { 44 std::cerr << "Connection error: " << context->errstr << std::endl; 45 } else { 46 std::cerr << "Connection error: can't allocate Redis context" << std::endl; 47 } 48 return 1; 49 } 50 51 // Set initial balance for user 1 52 redisReply* reply = (redisReply*)redisCommand(context, "SET balance:1 100"); 53 freeReplyObject(reply); 54 55 // Update balance for user 1 56 updateBalance(context, 1, 50); 57 58 // Retrieve updated balance 59 reply = (redisReply*)redisCommand(context, "GET balance:1"); 60 if (reply->type == REDIS_REPLY_STRING) { 61 std::cout << "Updated balance for user 1: " << reply->str << std::endl; 62 } else { 63 std::cerr << "Failed to retrieve the updated balance." << std::endl; 64 } 65 freeReplyObject(reply); 66 67 // Free the context 68 redisFree(context); 69 70 return 0; 71}
Here are detailed explanations for the key Redis commands used for transactions and key monitoring:
-
WATCH Command:
- Purpose: Monitors one or more keys (
balance:<user_id>
) for changes before starting a transaction. - Operation: Alerts the client if changes occur to the keys by another Redis command, which invalidates the current transaction when
EXEC
is called. - In this code, it means keeping an eye on
balance:1
to ensure no other client modifies it while you prepare a transaction. - Why It's Needed: The WATCH command is crucial for maintaining data consistency and integrity when multiple clients interact with Redis concurrently. By watching a key, we can prevent race conditions where the key's value might be altered by others right before executing the transaction, leading to errors or incorrect results. This ensures that transactional logic operates on the most current data.
- Purpose: Monitors one or more keys (
-
MULTI Command:
- Purpose: Marks the start of a Redis transaction.
- Operation: Begins queuing subsequent commands as part of a transaction rather than executing them immediately.
- Sets the stage for atomic execution—either all commands within the transaction block are applied, or none are if conditions change (e.g., watched keys are modified).
-
EXEC Command:
- Purpose: Attempts to execute all queued commands in the transaction.
- Operation: Ensures atomicity; if watched keys were changed,
EXEC
will returnnil
, meaning the transaction didn't happen. - In this lesson, it enforces that the balance update happens only if no other client changed
balance:<user_id>
sinceWATCH
.
- Atomicity means all operations within a transaction are completed successfully or none at all. This property prevents inconsistencies that may arise from partial updates.
- For instance, in our code, if another client updates the balance while our transaction is pending, the entire operation will abort, ensuring we don't apply changes on outdated data. The
updateBalance
function keeps retrying until the transaction is successfully applied without other interferences.
In this lesson, you learned how to monitor a key with the watch
command in Redis using hiredis in C++. The above code ensures that if another client modifies the key being monitored, the transaction is retried until it successfully updates the balance.
Ready to explore further? In the next section, you'll practice building pipelines to execute and retrieve results efficiently in C++, enhancing your understanding of Redis pipelines in real-world applications.