ホームページ  >  記事  >  データベース  >  Go と Redis を使用して分散ミューテックス ロックとレッド ロックを実装する方法

Go と Redis を使用して分散ミューテックス ロックとレッド ロックを実装する方法

WBOY
WBOY転載
2023-05-28 08:54:441326ブラウズ

ミューテックス ロック

Redis には 設定があります。 コマンドが存在しない場合は、このコマンドを使用してミューテックス ロック機能を実装できます。標準実装は、 Redis 公式ドキュメント。メソッドは SET resource_name my_random_value NX PX 30000 この一連のコマンドです。

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

  • NX は、存在しない場合は設定することを意味します。

  • PX 30000 は、有効期限は 30000 ミリ秒、つまり 30 秒です。

  • my_random_valueこの値はすべてのクライアント間で一意である必要があり、この値はすべてのロック競合相手で同じであってはなりません。同じキーです。

主にロックをより安全に解放するために、値は乱数である必要があります。ロックを解放するときは、スクリプトを使用して Redis に次のように指示します: キーのみが存在し、格納されている値は同じである私はそれを正常に削除し、他の競合他社のロックを誤って解放することを避けました。

2 つの操作が関係しているため、Lua スクリプトを通じて操作のアトミック性を確保する必要があります。

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
Lua スクリプトを使用しない例を挙げます。クライアント 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 サイトの他の関連記事を参照してください。

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