Redis には 設定があります。
コマンドが存在しない場合は、このコマンドを使用してミューテックス ロック機能を実装できます。標準実装は、 Redis 公式ドキュメント。メソッドは SET resource_name my_random_value NX PX 30000
この一連のコマンドです。
##resource_name は、ロックされるリソースを表します
NX は、存在しない場合は設定することを意味します。
PX 30000 は、有効期限は 30000 ミリ秒、つまり 30 秒です。
my_random_valueこの値はすべてのクライアント間で一意である必要があり、この値はすべてのロック競合相手で同じであってはなりません。同じキーです。
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 endLua スクリプトを使用しない例を挙げます。クライアント A はリソース ロックを取得しましたが、その後、別の操作によってブロックされました。クライアント A が他の操作を実行した後にロックを解放しようとしたとき、元のロックはすでにタイムアウトしていて、Redis によって自動的に解放されました。この期間中に、リソース ロックはクライアント B によって再度取得されました。 判定と削除は 2 つの操作であるため、A が判定するとすぐに期限切れでロックを自動的に解放し、その後 B がロックを取得し、その後 A が Del を呼び出して、B のエラーが発生する可能性があります。ロックが解除されます。 TryLock と Unlock の実装
TryLock は実際に
SET resource_name my_random_value NX PX 30000 を使用してロックします。ここでは
UUID を使用します。ランダム値として、ロックが成功するとランダム値が返され、このランダム値は
Unlock;
Unlockロック解除ロジックが次の場合に使用されます。前述の
lua スクリプト を実行します。
func (l *Lock) TryLock(ctx context.Context) error { success, err := l.client.SetNX(ctx, l.resource, l.randomValue, ttl).Result() if err != nil { return err } // 加锁失败 if !success { return ErrLockFailed } // 加锁成功 l.randomValue = randomValue return nil } func (l *Lock) Unlock(ctx context.Context) error { return l.script.Run(ctx, l.client, []string{l.resource}, l.randomValue).Err() }ロックの実装
Lock はブロッキング取得ロックであるため、ロックが失敗した場合は再試行する必要があります。もちろん、他の異常な状況 (ネットワークの問題、リクエストのタイムアウトなど) が発生する可能性があり、そのような状況では
error が直接返されます。
func (l *Lock) Lock(ctx context.Context) error { // 尝试加锁 err := l.TryLock(ctx) if err == nil { return nil } if !errors.Is(err, ErrLockFailed) { return err } // 加锁失败,不断尝试 ticker := time.NewTicker(l.tryLockInterval) defer ticker.Stop() for { select { case <-ctx.Done(): // 超时 return ErrTimeout case <-ticker.C: // 重新尝试加锁 err := l.TryLock(ctx) if err == nil { return nil } if !errors.Is(err, ErrLockFailed) { return err } } } }ウォッチドッグ メカニズムの実装ミューテックス ロック前の例で述べたものには小さな問題があります。つまり、ロックが保持されているクライアント A がブロックされている場合、タイムアウト後に A のロックが自動的に解放され、クライアント B が事前にロックを取得する可能性があります。 この状況の発生を減らすために、A がロックを保持している間はロックの有効期限を継続的に延長し、クライアント B が事前にロックを取得する状況を減らすことができます。これがウォッチドッグ機構です。 。 もちろん、クライアント A がロックを取得した後にたまたま Redis との接続を閉じた場合、タイムアウトを延長する方法がないため、上記の状況を完全に回避する方法はありません。 ウォッチドッグの実装ロックが成功するとスレッドを開始し、ロックの有効期限を継続的に延長し、ロック解除が実行されるとウォッチドッグ スレッドを閉じます。 ウォッチドッグ プロセスは次のとおりです。
func (l *Lock) startWatchDog() { ticker := time.NewTicker(l.ttl / 3) defer ticker.Stop() for { select { case <-ticker.C: // 延长锁的过期时间 ctx, cancel := context.WithTimeout(context.Background(), l.ttl/3*2) ok, err := l.client.Expire(ctx, l.resource, l.ttl).Result() cancel() // 异常或锁已经不存在则不再续期 if err != nil || !ok { return } case <-l.watchDog: // 已经解锁 return } } }TryLock: ウォッチドッグを開始
func (l *Lock) TryLock(ctx context.Context) error { success, err := l.client.SetNX(ctx, l.resource, l.randomValue, l.ttl).Result() if err != nil { return err } // 加锁失败 if !success { return ErrLockFailed } // 加锁成功,启动看门狗 go l.startWatchDog() return nil }Unlock: オフにしますウォッチドッグ
func (l *Lock) Unlock(ctx context.Context) error { err := l.script.Run(ctx, l.client, []string{l.resource}, l.randomValue).Err() // 关闭看门狗 close(l.watchDog) return err }レッドロック上記の実装は単一のRedisインスタンスに基づいているため、このインスタンスのみがハングすると、ロックを取得できないためすべてのリクエストが失敗します。フォールト トレランスでは、異なるマシンに分散された複数の Redis インスタンスを使用でき、ほとんどのノードのロックを取得している限り、正常にロックできます。これがレッド ロック アルゴリズムです。実際には、複数の Redis インスタンスのロックを同時に取得する必要がある点を除いて、上記の単一インスタンスのアルゴリズムに基づいています。
在加锁逻辑里,我们主要是对每个Redis实例执行SET resource_name my_random_value NX PX 30000
获取锁,然后把成功获取锁的客户端放到一个channel
里(这里因为是多线程并发获取锁,使用slice可能有并发问题),同时使用sync.WaitGroup
等待所有获取锁操作结束。
然后判断成功获取到的锁的数量是否大于一半,如果没有得到一半以上的锁,说明加锁失败,释放已经获得的锁。
如果加锁成功,则启动看门狗延长锁的过期时间。
func (l *RedLock) TryLock(ctx context.Context) error { randomValue := gofakeit.UUID() var wg sync.WaitGroup wg.Add(len(l.clients)) // 成功获得锁的Redis实例的客户端 successClients := make(chan *redis.Client, len(l.clients)) for _, client := range l.clients { go func(client *redis.Client) { defer wg.Done() success, err := client.SetNX(ctx, l.resource, randomValue, ttl).Result() if err != nil { return } // 加锁失败 if !success { return } // 加锁成功,启动看门狗 go l.startWatchDog() successClients <- client }(client) } // 等待所有获取锁操作完成 wg.Wait() close(successClients) // 如果成功加锁得客户端少于客户端数量的一半+1,表示加锁失败 if len(successClients) < len(l.clients)/2+1 { // 就算加锁失败,也要把已经获得的锁给释放掉 for client := range successClients { go func(client *redis.Client) { ctx, cancel := context.WithTimeout(context.Background(), ttl) l.script.Run(ctx, client, []string{l.resource}, randomValue) cancel() }(client) } return ErrLockFailed } // 加锁成功,启动看门狗 l.randomValue = randomValue l.successClients = nil for successClient := range successClients { l.successClients = append(l.successClients, successClient) } return nil }
我们需要延长所有成功获取到的锁的过期时间。
func (l *RedLock) startWatchDog() { l.watchDog = make(chan struct{}) ticker := time.NewTicker(resetTTLInterval) defer ticker.Stop() for { select { case <-ticker.C: // 延长锁的过期时间 for _, client := range l.successClients { go func(client *redis.Client) { ctx, cancel := context.WithTimeout(context.Background(), ttl-resetTTLInterval) client.Expire(ctx, l.resource, ttl) cancel() }(client) } case <-l.watchDog: // 已经解锁 return } } }
我们需要解锁所有成功获取到的锁。
func (l *RedLock) Unlock(ctx context.Context) error { for _, client := range l.successClients { go func(client *redis.Client) { l.script.Run(ctx, client, []string{l.resource}, l.randomValue) }(client) } // 关闭看门狗 close(l.watchDog) return nil }
以上がGo と Redis を使用して分散ミューテックス ロックとレッド ロックを実装する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。