ホームページ  >  記事  >  データベース  >  Redis で分散ロックを実装するときは何に注意する必要がありますか? 【注意事項まとめ】

Redis で分散ロックを実装するときは何に注意する必要がありますか? 【注意事項まとめ】

青灯夜游
青灯夜游転載
2022-03-04 16:21:583736ブラウズ

Redis で分散ロックを実装する場合、何に注意する必要がありますか?以下の記事では、Redis を分散ロックとして使用する際の注意点をまとめてお伝えしますので、ご参考になれば幸いです。

Redis で分散ロックを実装するときは何に注意する必要がありますか? 【注意事項まとめ】

Redis は分散ロックを実装します

分散ロックを読んでいるときに最近ある記事を目にしました特に良い記事です私自身の理解のために処理しました:

Redis 分散ロック実装の 3 つのコア要素:

1. ロック

最も簡単な方法は、setnx を使用することです指示。キーはロックの一意の識別子で、ビジネスに応じて名前が付けられます。値は現在のスレッドのスレッド ID です。 [関連する推奨事項: Redis ビデオ チュートリアル ]

たとえば、製品のフラッシュ セール アクティビティをロックする場合は、キーに「lock_sale_ID」という名前を付けることができます。また、値は何に設定されていますか?一時的に 1 に設定できます。ロックの疑似コードは次のとおりです。

setnx(key, 1)スレッドが setnx を実行して 1 を返した場合、キーが元々存在しなかったことを意味します。ロックの場合、他のスレッドが setnx を実行して 0 を返した場合、キーはすでに存在し、スレッドはロックの取得に失敗したことを意味します。

2. ロック解除

ロックしたい場合は、ロックを解除する必要があります。ロックを取得したスレッドがタスクを完了したら、他のスレッドが参加できるようにロックを解放する必要があります。ロックを解除する最も簡単な方法は、del 命令 # を実行することです。疑似コードは次のとおりです:

del(key)Afterロックを解放すると、他のスレッドは引き続き setnx コマンドを実行してロックを取得できます。

3. ロック タイムアウト

ロック タイムアウトとは何を意味しますか?ロックを取得したスレッドがタスクの実行中に停止し、明示的にロックを解放する時間がなかった場合、リソースは永久にロックされ、他のスレッドは二度とアクセスできなくなります。

したがって、setnx のキーにはタイムアウト期間を設定して、明示的に解放されなくても、一定時間が経過するとロックが自動的に解放されるようにする必要があります。 setnx はタイムアウト パラメーターをサポートしていないため、追加の命令が必要です。疑似コードは次のとおりです:

expire(key, 30)まとめると、3 番目のステップになります。分散ロック実装の最初のバージョンの疑似コードは次のとおりです:

if(setnx(key,1) == 1){
    expire(key,30)
    try {
        do something ......
    }catch()  {  }  finally {
       del(key)
    }

}

上記の疑似コードには 3 つの致命的な問題があるためです:

1. setnx と期限切れの非アトミック性

極端なシナリオを想像してください。スレッドが setnx を実行すると、ロックの取得に成功します。

setnx は実行に成功し、そして期限切れコマンドを実行する前に、ノード 1 Duang がハングアップします。

if(setnx(key,1) == 1){  //此处挂掉了.....
    expire(key,30)
    try {
        do something ......
    }catch()
  {
  }
  finally {
       del(key)
    }
 
}

この方法では、ロックには有効期限が設定されておらず、「不滅」になり、他のスレッドはロックを取得できなくなります。

どうすれば解決できますか? setnx 命令自体は、受信タイムアウト期間をサポートしていません。Redis 2.6.12 以降では、set 命令にオプションのパラメーターが追加されます。疑似コードは次のとおりです: set (key, 1, 30, NX), setnx 命令 を置き換えることができます。

#2. タイムアウト後に del を使用すると、他のスレッドのロックが誤って削除されてしまいます

##別の極端なシナリオでは、スレッドがロックの取得に成功し、タイムアウトが 30 秒に設定されているとします。

何らかの理由でスレッド A の実行が非常に遅く、30 秒経過しても実行が終了しない場合、ロックは有効期限が切れると自動的に解放され、スレッド B がロックを取得します。

その後、スレッド A がタスクを完了し、スレッド A は del 命令を実行してロックを解放します。ただし、この時点ではスレッド B の実行は完了していません。

スレッド A は、スレッド B によって追加されたロックを実際に削除します。

この状況を回避するにはどうすればよいですか? del がロックを解放する前に、現在のロックが自分で追加したロックかどうかを確認することができます。

具体的な実装としては、ロック時の値として現在のスレッドIDを使用し、キーに対応する値が自分のスレッドのIDであることを確認してから削除することができます。

加锁:
String threadId = Thread.currentThread().getId()
set(key,threadId ,30,NX)
doSomething.....
 
解锁:
if(threadId .equals(redisClient.get(key))){
    del(key)
}

ただし、これを行うと、

if 判定とロック解除が 2 つの独立した操作であり、アトミックではないという新たな問題が発生します。 私たちは皆、究極を追求するプログラマーであるため、この部分は Lua スクリプトを使用して実装する必要があります:

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

##redisClient.eval(luaScript , Collections.singletonList(key) , Collections.singletonList(threadId));このように、検証と削除のプロセスはアトミックな操作です。

3.

同時実行の可能性

还是刚才第二点所描述的场景,虽然我们避免了线程A误删掉key的情况,但是同一时间有A,B两个线程在访问代码块,仍然是不完美的。

怎么办呢?我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续航”

当过去了29秒,线程A还没执行完,这时候守护线程会执行expire指令,为这把锁“续命”20秒。守护线程从第29秒开始执行,每20秒执行一次。

当线程A执行完任务,会显式关掉守护线程。

另一种情况,如果节点1 忽然断电,由于线程A和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。

 memcache实现分布式锁

首页top 10, 由数据库加载到memcache缓存n分钟
微博中名人的content cache, 一旦不存在会大量请求不能命中并加载数据库
需要执行多个IO操作生成的数据存在cache中, 比如查询db多次
问题
在大并发的场合,当cache失效时,大量并发同时取不到cache,会同一瞬间去访问db并回设cache,可能会给系统带来潜在的超负荷风险。我们曾经在线上系统出现过类似故障。

解决方法

if (memcache.get(key) == null) {
// 3 min timeout to avoid mutex holder crash
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
value = db.get(key);
memcache.set(key, value);
memcache.delete(key_mutex);
} else {
 
sleep(50);
retry();
}
}

在load db之前先add一个mutex key, mutex key add成功之后再去做加载db, 如果add失败则sleep之后重试读取原cache数据。为了防止死锁,mutex key也需要设置过期时间。伪代码如下

Zookeeper实现分布式缓存

Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode

Znode分为四种类型:

  • 1.持久节点 (PERSISTENT)

默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在 。

  • 2.持久节点顺序节点(PERSISTENT_SEQUENTIAL)

所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号:

  • 3.临时节点(EPHEMERAL)

和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除:

  • 4.临时顺序节点(EPHEMERAL_SEQUENTIAL)

顾名思义,临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。

Zookeeper分布式锁恰恰应用了临时顺序节点。具体如何实现呢?让我们来看一看详细步骤:

  • 获取锁

首先,在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1

之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。

这时候,如果再有一个客户端 Client2 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock2

Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。

したがって、Client2 は、Lock1 を監視するために、Lock1 よりも上位にのみランク付けされたノードに Watcher を登録します。ノードが存在するかどうか。 これは、Client2 がロックの取得に失敗し、待機状態に入ったことを意味します。

この時点で、別のクライアント Client3 がロックを取得しに来た場合は、ParentLock シーケンスで一時的なロックをダウンロードして作成します。ノードロック3

Client3ParentLock の下にあるすべての一時シーケンス ノードを見つけて並べ替え、作成したノード Lock3# を決定します。 ## は順序の最初のものですか? ノード Lock3 が最小ではないことがわかります。

したがって、

Client3 は、Lock2 を監視するために、それよりも上位にのみランク付けされているノード Lock2Watcher を登録します。ノードが存在するかどうか。これは、Client3 もロックの取得に失敗し、待機状態に入ったことを意味します。

このようにして、

Client1 がロックを取得し、Client2Lock1 Client3 を監視します。 Lock2 を聴きました。これは、Java の ReentrantLock が依存する AQS (AbstractQueuedSynchronizer) とよく似た、待機キューを形成するだけです。

#ロックの解放
  • ロックを解放するには 2 つの状況があります:

1. タスクが完了すると、クライアントはリリースを表示します。

タスクが完了すると、

Client1

は削除ノード Lock1 を呼び出す指示を表示します。 。

2. タスクの実行中にクライアントがクラッシュします

ロックを取得しました

Client1

タスクの実行中に、Duang の A がクラッシュした場合Zookeeper サーバーを切断します。一時ノードの特性に従って、関連ノード Lock1 は自動的に削除されます。

Client2

Lock1 の存在状況を監視しているため、Lock1 ノードが削除されると、 ,Client2 にはすぐに通知されます。このとき、Client2 は、ParentLock 配下のすべてのノードを再度クエリして、自身が作成したノード Lock2 が現在の最小ノードであるかどうかを確認します。それが最小の場合、Client2 が自然にロックを取得します。

同様に、タスクの完了またはノードのクラッシュにより

Client2

がノード Lock2 も削除する場合、Cient3 通知されます。

最終的に、

Client3

はロックを正常に取得しました。

Zookeeper と Redis 分散ロックの比較

次の表に要約します。 Zookeeper と Redis 分散ロックの長所と短所:

##プログラミング関連の知識の詳細については、次を参照してください:プログラミングの概要

! !

以上がRedis で分散ロックを実装するときは何に注意する必要がありますか? 【注意事項まとめ】の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

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