Redis optimistic lock with golang demo

redis transaction command

  • MULTI: Open a transaction
  • EXEC: Transaction execution, which executes all commands within a transaction at once
  • DISCARD: Cancel Transaction

Optimistic locking using WATCH+MULTI

WATCH: Monitors one or more keys, and if a key changes before the transaction executes, the transaction is interrupted
UNWATCH: Suppress WATCH command from monitoring all keys

Use go-redis package to simulate user ticket-grabbing process

  • Open multiple goroutine simulations and preempt invoices
  • go-redis TxPipelined Execute Transaction
  • Go-redisClient.WatchMonitor a key
package main

import (
    "errors"
    "fmt"
    "sync"

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

func main() {
    client := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "", // no password set
        DB:       0,  // use default DB
    })

    key := "ticket_count"
    client.Set(key, "5", 0).Err()
    val, _ := client.Get(key).Result()
    fmt.Println("current ticket_count key val: ", val)

    getTicket(client, key)
}

func runTx(key string, id int) func(tx *redis.Tx) error {
    txf := func(tx *redis.Tx) error {
        n, err := tx.Get(key).Int()
        if err != nil && err != redis.Nil {
            return err
        }

        if n == 0 {
            return errors.New("No tickets left")
        }

        // actual opperation (local in optimistic lock)
        n = n - 1

        // runs only if the watched keys remain unchanged
        _, err = tx.TxPipelined(func(pipe redis.Pipeliner) error {
            // pipe handles the error case
            pipe.Set(key, n, 0)
            return nil
        })
        return err
    }
    return txf
}

func getTicket(client *redis.Client, key string) {
    routineCount := 8
    var wg sync.WaitGroup
    wg.Add(routineCount)

    for i := 0; i < routineCount; i++ {
        go func(id int) {
            defer wg.Done()

            for {
                err := client.Watch(runTx(key, id), key)
                if err == nil {
                    fmt.Println(id, "Success")
                    return
                } else if err.Error() == "No tickets left" {
                    fmt.Println(id, "No tickets left")
                    return
                } else {
                    fmt.Println(err, "retry")
                }
            }
        }(i)
    }
    wg.Wait()
}

Github code: https://github.com/defp/redis...

current ticket_count key val:  5
7 Success
6 Success
redis: transaction failed retry
redis: transaction failed retry
redis: transaction failed retry
redis: transaction failed retry
redis: transaction failed retry
redis: transaction failed retry
redis: transaction failed retry
3 Success
redis: transaction failed retry
redis: transaction failed retry
redis: transaction failed retry
redis: transaction failed retry
2 Success
redis: transaction failed retry
redis: transaction failed retry
redis: transaction failed retry
redis: transaction failed retry
5 Success
redis: transaction failed retry
redis: transaction failed retry
redis: transaction failed retry
4 No tickets left
1 No tickets left
0 No tickets left

links

Tags: Go Redis github

Posted on Mon, 08 Jun 2020 12:23:08 -0400 by socalnate