#この記事の主な内容は次のとおりです:
synchronzied または
lock ) を通じてロックされます。
キャッシュの故障を防ぐために、独自のスレッド リソースをロックします。
ローカル ロックメソッドであり、
分散状況でデータの不整合を引き起こします。たとえば、サービス A がデータを取得した後、キャッシュ キー =100 を更新します。 、サービス B はサービス A のロック制限を受けず、同時にキャッシュ キー = 99 を更新します。最終結果は 99 または 100 になる可能性がありますが、これは不明な状態です。
は期待される結果と矛盾します 。フローチャートは次のとおりです。
上記のローカル ロックの問題に基づいて、サポートが必要です分散クラスター環境ロック の下: DB にクエリを実行する場合、DB にアクセスできるのは 1 つのスレッドだけであり、他のスレッドは実行を続行する前に、最初のスレッドがロック リソースを解放するまで待つ必要があります。
人生の事例: ロックはドアの外の ロック
と見なされ、すべての同時スレッドが people
と比較されます。誰もがその部屋に入りたいのですが、部屋に入ることができるのは 1 人だけです。誰かが入ってきたら、ドアを施錠し、他の人は入ってきた人が出てくるまで待たなければなりません。
次の図に示すように、分散ロックの基本原理を見てみましょう。分析してください。上の図の分散ロック:
現地語での説明: 要求されたすべてのスレッドは同じ場所に移動します「ピットを占有します」
。ピットがある場合、ビジネス ロジックが実行されます。ピットはありません。「ピット」を解放するには別のスレッドが必要です。このピットはすべてのスレッドに表示されます。このピットを Redis キャッシュまたはデータベースに置くことができます。この記事では、Redis を使用して 「分散ピット」
を作成する方法について説明します。
Redis はパブリックにアクセスできる場所であるため、「活用する」場所として使用できます。
Redis を使用して分散ロックを実装するためのいくつかのソリューションでは、すべて SETNX コマンド (キーを特定の値に設定) を使用します。ハイエンドスキームで渡されるパラメータの数のみが異なり、異常事態が考慮されます。
このコマンドを見てみましょう。SETNX
は、set If not存在
の略です。これは、キーが存在しない場合はキーの値を設定し、キーが存在する場合は何もしないことを意味します。
Redis コマンド ラインでの実行方法は次のとおりです:
set <key> <value> NX
Redis コンテナーに入って、SETNX
コマンドを試すことができます。
最初にコンテナを入力してください:
docker exec -it <容器 id> redis-cli
然后执行 SETNX 命令:将 wukong
这个 key 对应的 value 设置成 1111
。
set wukong 1111 NX
返回 OK
,表示设置成功。重复执行该命令,返回 nil
表示设置失败。
我们先用 Redis 的 SETNX 命令来实现最简单的分布式锁。
我们来看下流程图:
代码示例如下,Java 中 setnx 命令对应的代码为 setIfAbsent
。
setIfAbsent 方法的第一个参数代表 key,第二个参数代表值。
// 1.先抢占锁 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "123"); if(lock) { // 2.抢占成功,执行业务 List<TypeEntity> typeEntityListFromDb = getDataFromDB(); // 3.解锁 redisTemplate.delete("lock"); return typeEntityListFromDb; } else { // 4.休眠一段时间 sleep(100); // 5.抢占失败,等待锁释放 return getTypeEntityListByRedisDistributedLock(); }
一个小问题:那为什么需要休眠一段时间?
因为该程序存在递归调用,可能会导致栈空间溢出。
ブロンズがブロンズと呼ばれる理由は、それが最も基本的であり、間違いなく多くの問題を引き起こすためです。
家族の風景を想像してみてください: 夜、シャオコンが一人でドアの鍵を開けて部屋に入り、電気をつけますか?その後、突然 停電になりました、シャオコンはドアを開けて外に出ようとしますが、ドアのロック位置が見つからないと、シャオミンも中に入ることができず、外にいる人も入ることができません。
デッドロックが発生します。
ロックの
自動有効期限を設定します。一定時間が経過すると、ロックは自動的に削除され、他のスレッドがロックを取得できるようになります。4. シルバー ソリューション
4.2 技術回路図
清理 redis key 的代码如下
// 在 10s 以后,自动清理 lock redisTemplate.expire("lock", 10, TimeUnit.SECONDS);
完整代码如下:
// 1.先抢占锁 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "123"); if(lock) { // 2.在 10s 以后,自动清理 lock redisTemplate.expire("lock", 10, TimeUnit.SECONDS); // 3.抢占成功,执行业务 List<TypeEntity> typeEntityListFromDb = getDataFromDB(); // 4.解锁 redisTemplate.delete("lock"); return typeEntityListFromDb; }
白银方案看似解决了线程异常或服务器宕机造成的锁未释放的问题,但还是存在其他问题:
因为占锁和设置过期时间是分两步执行的,所以如果在这两步之间发生了异常,则锁的过期时间根本就没有设置成功。
所以和青铜方案有一样的问题:锁永远不能过期。
上面的白银方案中,占锁和设置锁过期时间是分步两步执行的,这个时候,我们可以联想到什么:事务的原子性(Atom)。
原子性:多条命令要么都成功执行,要么都不执行。
将两步放在一步中执行:占锁+设置锁过期时间。
Redis 正好支持这种操作:
# 设置某个 key 的值并设置多少毫秒或秒 过期。 set <key> <value> PX <多少毫秒> NX 或 set <key> <value> EX <多少秒> NX
然后可以通过如下命令查看 key 的变化
ttl <key>
下面演示下如何设置 key 并设置过期时间。注意:执行命令之前需要先删除 key,可以通过客户端或命令删除。
# 设置 key=wukong,value=1111,过期时间=5000ms set wukong 1111 PX 5000 NX # 查看 key 的状态 ttl wukong
执行结果如下图所示:每运行一次 ttl 命令,就可以看到 wukong 的过期时间就会减少。最后会变为 -2(已过期)。
黄金方案和白银方案的不同之处:获取锁的时候,也需要设置锁的过期时间,这是一个原子操作,要么都成功执行,要么都不执行。如下图所示:
设置 lock
的值等于 123
,过期时间为 10 秒。如果 10
秒 以后,lock 还存在,则清理 lock。
setIfAbsent("lock", "123", 10, TimeUnit.SECONDS);
我们还是举生活中的例子来看下黄金方案的缺陷。
123
です。 123
に設定され、有効期限が 10 秒
に設定されました。 結果、競合が発生しました。
後にタスクを完了しましたが、ユーザー B はまだタスクを実行していました。
の錠を開けました。
上記のケースから、ユーザー A がタスク を処理するのに必要な時間は、自動ロックのクリーニング (ロック解除) の時間 よりも長いため、その後、別のユーザーがロックを奪取しました。ユーザー A がタスクを完了すると、他のユーザーが占有したロックを積極的に開けます。
なぜここで他の人の鍵が開いているのですか? ロック番号はすべて "123"
と呼ばれるため、ユーザー A はロック番号のみを認識し、"123"
という番号のロックを見たときにロックを開きます。その結果、 、ユーザー B のロックが開いています。、ユーザー B は現時点ではタスクを完了していません。もちろん、彼は怒っています。
上記のゴールド プランの欠点も簡単に解決できます。 各ロックに異なる番号を設定すると良いのではないでしょうか~
下の図に示すように、B によってプリエンプトされたロックは青色であり、B によってプリエンプトされた緑色のロックとは異なります。 A.こうすればAさんには開けられなくなります。 わかりやすいようにアニメーション画像を作成しました: 静止画像はより高精細で、撮影できます。外観:// 1.生成唯一 id String uuid = UUID.randomUUID().toString(); // 2. 抢占锁 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS); if(lock) { System.out.println("抢占成功:" + uuid); // 3.抢占成功,执行业务 List<TypeEntity> typeEntityListFromDb = getDataFromDB(); // 4.获取当前锁的值 String lockValue = redisTemplate.opsForValue().get("lock"); // 5.如果锁的值和设置的值相等,则清理自己的锁 if(uuid.equals(lockValue)) { System.out.println("清理锁:" + lockValue); redisTemplate.delete("lock"); } return typeEntityListFromDb; } else { System.out.println("抢占失败,等待锁释放"); // 4.休眠一段时间 sleep(100); // 5.抢占失败,等待锁释放 return getTypeEntityListByRedisDistributedLock(); }
上面的方案看似很完美,但还是存在问题:第 4 步和第 5 步并不是原子性的。
时刻:0s。线程 A 抢占到了锁。
时刻:9.5s。线程 A 向 Redis 查询当前 key 的值。
时刻:10s。锁自动过期。
时刻:11s。线程 B 抢占到锁。
时刻:12s。线程 A 在查询途中耗时长,终于拿多锁的值。
时刻:13s。线程 A 还是拿自己设置的锁的值和返回的值进行比较,值是相等的,清理锁,但是这个锁其实是线程 B 抢占的锁。
那如何规避这个风险呢?钻石方案登场。
上面的线程 A 查询锁和删除锁的逻辑不是原子性
的,所以将查询锁和删除锁这两步作为原子指令操作就可以了。
如下图所示,红色圈出来的部分是钻石方案的不同之处。用脚本进行删除,达到原子操作。
那如何用脚本进行删除呢?
我们先来看一下这段 Redis 专属脚本:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
这段脚本和铂金方案的获取key,删除key的方式很像。先获取 KEYS[1] 的 value,判断 KEYS[1] 的 value 是否和 ARGV[1] 的值相等,如果相等,则删除 KEYS[1]。
那么这段脚本怎么在 Java 项目中执行呢?
分两步:先定义脚本;用 redisTemplate.execute 方法执行脚本。
// 脚本解锁 String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"; redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
上面的代码中,KEYS[1] 对应“lock”
,ARGV[1] 对应 “uuid”
,含义就是如果 lock 的 value 等于 uuid 则删除 lock。
而这段 Redis 脚本是由 Redis 内嵌的 Lua 环境执行的,所以又称作 Lua 脚本。
那钻石方案是不是就完美了呢?有没有更好的方案呢?
下篇,我们再来介绍另外一种分布式锁的王者方案:Redisson。
この記事では、分散ロックの問題からローカル ロックの問題までを紹介します。次に、5 つの分散ロック ソリューションを紹介し、浅いものから深いものまでさまざまなソリューションの改善について説明します。
上記のソリューションの継続的な進化から、システムのどこに異常な状況が存在する可能性があるか、そしてそれらをより適切に処理する方法がわかりました。
1 つの例から推論すると、この進化する思考モデルは他のテクノロジーにも適用できます。
以下は、上記の 5 つのソリューションの欠点と改善点をまとめたものです。
ブロンズ ソリューション:
#シルバー ソリューション:
ゴールデンソリューション:
プラチナ ソリューション:
ダイヤモンドプラン:
王様の計画、次の記事でお会いしましょう~
以上がRedis分散ロック|ブロンズからダイヤモンドへの5つの進化プランの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。