Home  >  Article  >  Database  >  How to implement distributed locks in Go combined with Redis

How to implement distributed locks in Go combined with Redis

PHPz
PHPzforward
2023-05-27 21:55:241193browse

    Single Redis instance scenario

    If you are familiar with Redis commands, you may immediately think of using Redis's set if not exists operation to implement it, and it is now standard The implementation method is the SET resource_name my_random_value NX PX 30000 series of commands, where:

    • resource_name represents the resource to be locked

    • NX represents if it does not exist Then set

    • PX 30000 to indicate that the expiration time is 30000 milliseconds, which is 30 seconds

    • my_random_value This value must be unique among all clients , all acquirers (competitors) of the same key cannot have the same value.

    The value of value must be a random number mainly to release the lock more safely. When releasing the lock, use a script to tell Redis: only the key exists and the stored value is the same as the value I specified. Only then can I be told that the deletion was successful. This can be achieved through the following Lua script:

    if redis.call("get",KEYS[1]) == ARGV[1] then
        return redis.call("del",KEYS[1])
    else
        return 0
    end

    For example: Client A obtains the resource lock, but is then blocked by another operation. When Client A wants to release the lock after running other operations, it turns out that The lock has already timed out and was automatically released by Redis, and during this period the resource lock was acquired again by client B.

    The Lua script is used because judgment and deletion are two operations, so it is possible that A will automatically release the lock upon expiration as soon as it judges it, and then B will acquire the lock, and then A will call Del, causing B to The lock is released.

    Add and Unlock Example

    package main
    
    import (
       "context"
       "errors"
       "fmt"
       "github.com/brianvoe/gofakeit/v6"
       "github.com/go-redis/redis/v8"
       "sync"
       "time"
    )
    
    var client *redis.Client
    
    const unlockScript = `
    if redis.call("get",KEYS[1]) == ARGV[1] then
        return redis.call("del",KEYS[1])
    else
        return 0
    end`
    
    func lottery(ctx context.Context) error {
       // 加锁
       myRandomValue := gofakeit.UUID()
       resourceName := "resource_name"
       ok, err := client.SetNX(ctx, resourceName, myRandomValue, time.Second*30).Result()
       if err != nil {
          return err
       }
       if !ok {
          return errors.New("系统繁忙,请重试")
       }
       // 解锁
       defer func() {
          script := redis.NewScript(unlockScript)
          script.Run(ctx, client, []string{resourceName}, myRandomValue)
       }()
    
       // 业务处理
       time.Sleep(time.Second)
       return nil
    }
    
    func main() {
       client = redis.NewClient(&redis.Options{
          Addr: "127.0.0.1:6379",
       })
       var wg sync.WaitGroup
       wg.Add(2)
       go func() {
          defer wg.Done()
          ctx, _ := context.WithTimeout(context.Background(), time.Second*3)
          err := lottery(ctx)
          if err != nil {
             fmt.Println(err)
          }
       }()
       go func() {
          defer wg.Done()
          ctx, _ := context.WithTimeout(context.Background(), time.Second*3)
          err := lottery(ctx)
          if err != nil {
             fmt.Println(err)
          }
       }()
       wg.Wait()
    }

    Let’s first look at the lottery() function, which simulates a lottery operation. When entering the function, first use SET resource_name my_random_value NX PX 30000 to lock, here use UUID as Random value. If the operation fails, it returns directly and allows the user to try again. If the unlocking logic is successfully executed in defer, the unlocking logic is to execute the Lua script mentioned above and then perform business processing.

    We executed two goroutines in the main() function to concurrently call the lottery() function. One of the operations will fail directly because the lock cannot be obtained.

    Summary

    • Generate random value

    • Use SET resource_name my_random_value NX PX 30000 to lock

    • If the lock fails, return directly to

    • defer to add unlocking logic to ensure that

    • will be executed when the function exits Business logic

    Multiple Redis instance scenario

    In the case of a single instance, if this instance hangs, all requests will fail because the lock cannot be obtained, so we You need multiple Redis instances distributed on different machines, and you need to get the locks of most of the nodes to successfully lock. This is the RedLock algorithm. We need to acquire locks on multiple Redis instances at the same time, but it is actually based on a single instance algorithm.

    Add and Unlock Example

    package main
    
    import (
       "context"
       "errors"
       "fmt"
       "github.com/brianvoe/gofakeit/v6"
       "github.com/go-redis/redis/v8"
       "sync"
       "time"
    )
    
    var clients []*redis.Client
    
    const unlockScript = `
    if redis.call("get",KEYS[1]) == ARGV[1] then
        return redis.call("del",KEYS[1])
    else
        return 0
    end`
    
    func lottery(ctx context.Context) error {
       // 加锁
       myRandomValue := gofakeit.UUID()
       resourceName := "resource_name"
       var wg sync.WaitGroup
       wg.Add(len(clients))
       // 这里主要是确保不要加锁太久,这样会导致业务处理的时间变少
       lockCtx, _ := context.WithTimeout(ctx, time.Millisecond*5)
       // 成功获得锁的Redis实例的客户端
       successClients := make(chan *redis.Client, len(clients))
       for _, client := range clients {
          go func(client *redis.Client) {
             defer wg.Done()
             ok, err := client.SetNX(lockCtx, resourceName, myRandomValue, time.Second*30).Result()
             if err != nil {
                return
             }
             if !ok {
                return
             }
             successClients <- client
          }(client)
       }
       wg.Wait() // 等待所有获取锁操作完成
       close(successClients)
       // 解锁,不管加锁是否成功,最后都要把已经获得的锁给释放掉
       defer func() {
          script := redis.NewScript(unlockScript)
          for client := range successClients {
             go func(client *redis.Client) {
                script.Run(ctx, client, []string{resourceName}, myRandomValue)
             }(client)
          }
       }()
       // 如果成功加锁得客户端少于客户端数量的一半+1,表示加锁失败
       if len(successClients) < len(clients)/2+1 {
          return errors.New("系统繁忙,请重试")
       }
    
       // 业务处理
       time.Sleep(time.Second)
       return nil
    }
    
    func main() {
       clients = append(clients, redis.NewClient(&redis.Options{
          Addr: "127.0.0.1:6379",
          DB:   0,
       }), redis.NewClient(&redis.Options{
          Addr: "127.0.0.1:6379",
          DB:   1,
       }), redis.NewClient(&redis.Options{
          Addr: "127.0.0.1:6379",
          DB:   2,
       }), redis.NewClient(&redis.Options{
          Addr: "127.0.0.1:6379",
          DB:   3,
       }), redis.NewClient(&redis.Options{
          Addr: "127.0.0.1:6379",
          DB:   4,
       }))
       var wg sync.WaitGroup
       wg.Add(2)
       go func() {
          defer wg.Done()
          ctx, _ := context.WithTimeout(context.Background(), time.Second*3)
          err := lottery(ctx)
          if err != nil {
             fmt.Println(err)
          }
       }()
       go func() {
          defer wg.Done()
          ctx, _ := context.WithTimeout(context.Background(), time.Second*3)
          err := lottery(ctx)
          if err != nil {
             fmt.Println(err)
          }
       }()
       wg.Wait()
       time.Sleep(time.Second) 
    }

    In the above code, we use Redis's multi-database to simulate multiple Redis master instances. Generally, we will choose 5 Redis instances. In the real environment, these instances should They are distributed on different machines to avoid simultaneous failures.
    In the locking logic, we mainly execute SET resource_name my_random_value NX PX 30000 for each Redis instance to obtain the lock, and then put the client that successfully obtained the lock into a channel (there may be concurrency issues when using slice here). At the same time, use sync.WaitGroup to wait for the lock acquisition operation to end.
    Then add defer to release the lock logic. The lock release logic is very simple, just release the successfully obtained lock.
    Finally determine whether the number of successfully acquired locks is greater than half. If more than half of the locks are not acquired, the locking fails.
    If the locking is successful, the next step is to perform business processing.

    Summary

    • Generate a random value

    • And send it to each Redis instance for useSET resource_name my_random_value NX PX 30000 Lock

    • Wait for all lock acquisition operations to be completed

    • defer adds unlocking logic to ensure that it will be executed when the function exits, here Defer first and then judge because it is possible to obtain the lock of a part of the Redis instance, but because it does not exceed half, it will still be judged as a lock failure.

    • Determine whether more than half of the Redis instance has been obtained Lock, if there is no explanation of lock failure, directly return

    • Execute business logic

    The above is the detailed content of How to implement distributed locks in Go combined with Redis. For more information, please follow other related articles on the PHP Chinese website!

    Statement:
    This article is reproduced at:yisu.com. If there is any infringement, please contact admin@php.cn delete