Implementing Conditional Transactions in Go with Redis `Watch`

Introduction to Watch in Redis

Welcome back! You’ve learned how to build and execute basic transactions in Redis using Go. This lesson will introduce you to the watch functionality in go-redis, enabling conditional and controlled transactions. Such functionality is vital for scenarios where you need to monitor specific keys and ensure operations only execute when certain conditions are met.

What You'll Learn

In this unit, you will explore the Watch feature in Redis using the go-redis library in Go. Here's a quick overview of your learning objectives:

  1. Setting Up Watch: Understanding how to monitor keys to control transaction execution in Go.
  2. Implementing Conditional Updates: Crafting functions that use Watch to deliver safer and more conditional updates to your Redis data with Go.

Let's take a look at a practical example of how to use Watch in your code.

Go
package main

import (
    "context"
    "fmt"

    "github.com/redis/go-redis/v9"
)

func updateBalance(rdb *redis.Client, ctx context.Context, userID string, increment int) {
    redisPrefix := "balance:"
    key := redisPrefix + userID

    err := rdb.Watch(ctx, func(tx *redis.Tx) error {
        currentBalance, err := tx.Get(ctx, key).Int()
        if err != nil {
            if err == redis.Nil {
                currentBalance = 0
            } else {
                return fmt.Errorf("failed to get balance: %w", err)
            }
        }

        newBalance := currentBalance + increment

        _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
            pipe.Set(ctx, key, newBalance, 0)
            return nil
        })

        return err
    }, key)

    if err != nil {
        if err == redis.TxFailedErr {
            fmt.Println("Retrying transaction due to Watch mismatch.")
            updateBalance(rdb, ctx, userID, increment)
        } else {
            fmt.Printf("Fatal error: %v\n", err)
        }
    }
}

func main() {
    ctx := context.Background()

    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
    defer rdb.Close()

    // Set initial balance for user "1" to 100
    err := rdb.Set(ctx, "balance:1", 100, 0).Err()
    if err != nil {
        fmt.Printf("Fatal error: %v\n", err)
        return
    }

    updateBalance(rdb, ctx, "1", 50)

    updatedValue, err := rdb.Get(ctx, "balance:1").Int()
    if err != nil {
        fmt.Printf("Fatal error: %v\n", err)
        return
    }
    fmt.Printf("Updated balance for user 1: %d\n", updatedValue)
}

In this Go example, we watch the balance:id key to catch changes. If another client alters the value before executing the transaction, the transaction will fail, and we retry. This ensures balance updates are consistent.

Let's break down each step in the code snippet:

  • We define a function updateBalance that takes the Redis client, context, userID, and increment as arguments.
    • We begin by using Watch to keep an eye on the balance:id key, ensuring no modifications during the transaction.
    • The current balance value is retrieved using Get, and defaults to 0 if nonexistent.
    • We use TxPipelined to run commands in a transaction safely.
    • We update the balance by adding the increment value.
    • If the operation fails due to a change in the balance key, the transaction is retried.

By calling updateBalance, we adjust userID=1's balance by 50 and print the updated value.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal