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
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 スクリプトを実行してから業務処理を実行します。
概要
SET resource_name my_random_value NX PX 30000 を使用してロック
ロックが失敗した場合は、
defer に直接戻り、ロック解除ロジックを追加して、ロックが解除されたときに
が実行されるようにします。 function exits ビジネス ロジック
複数の 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 を使用して、ロックの取得操作が終了するのを待ちます。ロックが成功すると、次のステップはビジネス処理を実行することです。
概要
そして使用するために各 Redis インスタンスに送信します
SET resource_name my_random_value NX PX 30000すべてのロック取得操作が完了するまで待機します
defer はロック解除ロジックを追加して、確実にロックが解除されるようにします。関数終了時に実行されますが、ここでは Redis インスタンスの一部のロックは取得できているため Defer してから判定しますが、半分を超えていないためやはりロック失敗と判定されます。
以上がGo と Redis を組み合わせて分散ロックを実装する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。