ホームページ  >  記事  >  データベース  >  Go と Redis を組み合わせて分散ロックを実装する方法

Go と Redis を組み合わせて分散ロックを実装する方法

PHPz
PHPz転載
2023-05-27 21:55:241192ブラウズ

    単一 Redis インスタンスのシナリオ

    Redis コマンドに精通している場合は、Redis の「存在しない場合に設定」操作を使用して実装することをすぐに思いつくかもしれません。現在では標準となっています。実装方法は、SET resource_name my_random_value NX PX 30000 シリーズのコマンドです。ここで、

    • resource_name は、ロックされるリソースを表します。

    • NX は存在しない場合を表します。次に、有効期限が 30000 ミリ秒、つまり 30 秒であることを示すために

    • PX 30000 を設定します。 #my_random_value この値はすべてのクライアント間で一意である必要があり、同じキーのすべての取得者 (競合者) が同じ値を持つことはできません。

    • value の値は、主にロックをより安全に解放するために乱数である必要があります。ロックを解放するときは、スクリプトを使用して Redis に、キーのみが存在し、格納された値は次のように指示します。指定した値と同じである場合にのみ、削除が成功したことがわかります。これは、次の Lua スクリプトを通じて実現できます:

      if redis.call("get",KEYS[1]) == ARGV[1] then
          return redis.call("del",KEYS[1])
      else
          return 0
      end
    • 例: クライアント A はリソース ロックを取得しますが、その後、別の操作によってブロックされます。クライアント A が他の操作を実行した後にロックを解放したい場合、ロックはすでにタイムアウトしており、Redis によって自動的に解放されており、この期間中にクライアント B によってリソース ロックが再度取得されたことがわかります。

    Lua スクリプトを使用しているのは、判定と削除が 2 つの操作であるため、A が判定したらすぐに期限切れで自動的にロックを解放し、次に B がロックを取得し、次に A がロックを取得するという順序が考えられます。 Del を呼び出して B を呼び出します。ロックが解放されます。

    追加とロック解除の例

    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()
    }

    まず、宝くじ操作をシミュレートする Lotto() 関数を見てみましょう。関数に入るときは、まず SET resource_name my_random_value NX PX 30000 を使用してロックします。 UUID をランダム値として使用します。操作が失敗した場合は、直接戻り、ユーザーは再試行できます。defer でロック解除ロジックが正常に実行された場合、ロック解除ロジックは、上記の Lua スクリプトを実行してから業務処理を実行します。

    main() 関数で 2 つのゴルーチンを実行して、lottery() 関数を同時に呼び出しましたが、ロックを取得できないため、操作の 1 つが直接失敗します。

    概要

    ランダム値の生成

    • SET resource_name my_random_value NX PX 30000 を使用してロック

    • ロックが失敗した場合は、

    • defer に直接戻り、ロック解除ロジックを追加して、ロックが解除されたときに

    • が実行されるようにします。 function exits ビジネス ロジック

    • 複数の Redis インスタンスのシナリオ

    • 単一インスタンスの場合、このインスタンスがハングすると、ロックを取得できないため、すべてのリクエストが失敗します。複数の Redis インスタンスが異なるマシンに分散されている必要があり、正常にロックするにはほとんどのノードのロックを取得する必要があります。これが RedLock アルゴリズムです。複数の Redis インスタンスのロックを同時に取得する必要がありますが、実際には単一インスタンスのアルゴリズムに基づいています。

    追加とロック解除の例

    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) 
    }

    上記のコードでは、Redis のマルチデータベースを使用して複数の Redis マスター インスタンスをシミュレートします。通常、5 つの Redis インスタンスを選択します。実際の環境では、これらはインスタンスは、同時障害を避けるために異なるマシンに分散される必要があります。

    ロック ロジックでは、主に各 Redis インスタンスに対して SET resource_name my_random_value NX PX 30000 を実行してロックを取得し、ロックを正常に取得したクライアントをチャネルに配置します (ここでスライスを使用すると同時実行の問題が発生する可能性があります)。同時に、sync.WaitGroup を使用して、ロックの取得操作が終了するのを待ちます。

    次に、ロック ロジックを解放するために defer を追加します。ロック リリース ロジックは非常に単純で、正常に取得されたロックを解放するだけです。

    最後に、取得に成功したロックの数が半分より多いかどうかを確認し、半分以上のロックが取得できなかった場合、ロックは失敗します。

    ロックが成功すると、次のステップはビジネス処理を実行することです。

    概要

    ランダム値を生成します

    • そして使用するために各 Redis インスタンスに送信します

      SET resource_name my_random_value NX PX 30000
    • Lock
    • すべてのロック取得操作が完了するまで待機します

    • defer はロック解除ロジックを追加して、確実にロックが解除されるようにします。関数終了時に実行されますが、ここでは Redis インスタンスの一部のロックは取得できているため Defer してから判定しますが、半分を超えていないためやはりロック失敗と判定されます。

    • ## Redis インスタンスの半分以上がロックを取得したかどうかを判断します。ロック失敗の説明がない場合は、直接返します。
    • #ビジネス ロジックを実行

    以上がGo と Redis を組み合わせて分散ロックを実装する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

    声明:
    この記事はyisu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。