Redis 分散ロックの実装方法とその適用シナリオとは
#はじめに
ロックは開発プロセスで非常に一般的なツールです。悲観的ロック、楽観的ロック、排他的ロック、公正なロックに精通している必要があります。ロック、不公平なロックなど、多くの概念があります。Java のロックがわからない場合は、この記事を参照してください: 必ず言及しなければならない Java の「ロック」 この記事は非常に包括的ですが、初心者向けです。これらのロックの概念は、実務経験が不足しているため、ロック解除の実際の使用シナリオを理解できない可能性があります。Java では、スレッドの安全性は、Volatile、Synchronized、ReentrantLock の 3 つのキーワードによって実現できます。この部分の知識は次のとおりです。基本的な面接の最初のラウンドに含まれており、必ず尋ねられます (それに習熟している必要があります)。
分散システムでは、Java ロック テクノロジは 2 台のマシンのコードを同時にロックできないため、分散ロックを通じて実装する必要があります。分散ロックの上手な使用は、大手企業が習得する必要があるスキルでもあります。工場の開発者。
1. インタビュアー:
分散ロックを使用する必要があるシナリオに遭遇したことがありますか?
問題分析: この質問は主に導入として使用されます。最初に、分散ロックがどのようなシナリオで使用される必要があるのか、分散ロックが解決する必要がある問題は何かを理解する必要があります。この前提の下で、より深く理解するのに役立ちます。分散ロックの実装原理。
分散ロックを使用するシナリオは、通常、次のシナリオを満たす必要があります。
システムは分散システムであり、Java ロックをロックできなくなりました。
ライブラリ内の唯一のユーザー データなどの共有リソースを操作します。
同期アクセス、つまり、複数のプロセスが共有リソースを同時に操作します。
回答: プロジェクトで分散ロックを使用するシナリオの例をお話しします:
消費ポイントは、クレジット カード、 ECサイトなど。 ポイントはギフトなどと交換できます。 ここでの「ポイントを消費する」という操作は、ロックが必要となる典型的なシナリオです。
イベント A:
ギフトのポイント交換を例にとると、完全なポイント消費プロセスは 3 つのステップに単純に分割されます。
A1: ユーザーが商品を選択し、交換を開始し、注文を送信します。
A2: システムはユーザーの残りのポイントを読み取り、ユーザーの現在のポイントが十分であるかどうかを判断します。
A3: ユーザーポイントが減算されます。
イベント B:
システムは 3 つの簡単なステップでユーザーにポイントを配布します:
B1: その日にユーザーが受け取るべきポイントを計算します
B2 : ユーザーの元のポイントを読み取ります
B3: 元のポイントに今回付与されるポイントを加算します
そこで問題は、ユーザーの消費ポイントとユーザーの蓄積ポイントが同時に発生した場合です。 (ユーザーポイントも同時に運用されます)どうなるでしょうか?
仮定: ユーザーがポイントを消費している間、オフライン タスクはたまたまポイントを計算し、ユーザーにポイントを発行します (たとえば、その日のユーザーの消費量に基づいて)。これら 2 つのことは次の時点で行われます。次のロジックは少し複雑なので、辛抱強く理解してください。
ユーザー U は 1,000 ポイントを所有しており (ユーザー ポイントを記録するデータは共有リソースとして理解できます)、今回の交換では 999 ポイントが消費されます。
ロックが解除された状況: イベント A プログラムがポイントの読み取りの 2 番目のステップに到達したとき、A:2 操作によって読み取られた結果は 1000 ポイントであり、残りのポイントがこの引き換えに十分であると判断され、ステップ 3 A: 3 つの操作に対してポイントが差し引かれます (1000 - 999 = 1)。通常の結果はユーザーにとって 1 ポイントのままです。ただし、この時点でイベント B も実行されています。今回はユーザー U に 100 ポイントが発行されます。2 つのスレッドが同時に実行します (同期アクセス)。ロックがなければ、次の可能性があります: A:2 - > B :2 -> A:3 -> B:3、A:3 が完了する前に (減点、1000 ~ 999)、ユーザー U の合計ポイントがイベント B のスレッドによって読み取られ、最後にユーザー U の合計ポイントが読み取られます。合計ポイントは1100ポイントになり、999ポイントのギフトを無駄に交換しましたが、明らかに期待した結果を満たしていませんでした。
ユーザー ポイントを同時に操作できるのは偶然であり、CPU が高速であることを言う人がいます。十分なユーザーがいて同時実行性が十分に大きい限り、マーフィーの法則は有効です。遅かれ早かれ、上記のバグが発生するのは時間の問題であり、ハッキングされる可能性があります。業界はこのバグに悩まされており、このバグに夢中になっています。現時点では、開発者として、これを解決したい場合は、隠れた危険があるため、ロック解除の使用方法を理解する必要があります。
(コードの記述は非常に難しい問題です!)
Java 自体には 2 つの組み込みロック実装があり、1 つは JVM によって同期実装され、もう 1 つは JDK によって提供されます。アプリケーションがスタンドアロンまたは単一プロセス アプリケーションの場合、これら 2 つのロックを使用してロックを実装できます。
しかし、インターネット企業の現在のシステムのほとんどは分散されており、コードが複数のマシンにデプロイされるため、現時点では Java 独自の同期または Lock は分散環境でのロック要件を満たすことができなくなります。この問題を解決するために、分散ロックが登場しました。分散ロックの特徴は、マルチプロセスであり、複数の物理マシンでメモリを共有することができません。一般的な解決策は、メモリ層の干渉に基づいています。実装ソリューションは次のとおりです。 Redis に基づく分散ロック、または ZooKeeper 分散ロック。
(これ以上詳しく分析することはできません。面接官はもう満足していませんか?)
2. 面接官:
Redis 分散ロックの実装方法
分散ロックの問題を解決するには、現在 2 つの主な実装方法があります。1 つは Redis クラスター モードに基づくもので、もう 1 つは... 2. Zookeeper クラスター モードに基づきます。
この2つを優先的にマスターすれば、面接対策は基本的には困らないでしょう。 ######答え:###
1、基於Redis的分散式鎖定
方法一:使用setnx指令加鎖定
public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) { // 第一步:加锁 Long result = jedis.setnx(lockKey, requestId); if (result == 1) { // 第二步:设置过期时间 jedis.expire(lockKey, expireTime); } }
程式碼解釋:
setnx
指令,意思是set if not exist,如果lockKey不存在,把key存入Redis,保存成功後如果result回傳1,表示設定成功,如果非1,表示失敗,別的執行緒已經設定過了。
expire()
,設定過期時間,防止死鎖,假設,如果一個鎖set後,一直不刪掉,那這個鎖相當於一直存在,產生死鎖。
(講到這裡,我還要和麵試官強調一個「但是」)
思考,我上面的方法哪裡與缺陷?繼續給面試官解釋…
加鎖總共分兩步,第一步jedis.setnx,第二步jedis.expire設定過期時間,setnx與expire不是一個原子操作,如果程式執行完第一步後異常了,第二步jedis.expire(lockKey, expireTime)沒有執行,相當於這個鎖沒有過期時間,有產生死鎖的可能。正對這個問題如何改進?
改進:
public class RedisLockDemo { private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; /** * 获取分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @param expireTime 超期时间 * @return 是否获取成功 */ public static boolean getLock(Jedis jedis, String lockKey, String requestId, int expireTime) { // 两步合二为一,一行代码加锁并设置 + 过期时间。 if (1 == jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime)) { return true;//加锁成功 } return false;//加锁失败 } }
程式碼解釋:
將加鎖和設定過期時間合而為一,一行程式碼搞定,原子操作。
(沒等面試官開口追問,面試官很滿意了)
3、面試官: 那解鎖操作呢?
答案:
釋放鎖定就是刪除key
使用del指令解鎖
public static void unLock(Jedis jedis, String lockKey, String requestId) { // 第一步: 使用 requestId 判断加锁与解锁是不是同一个客户端 if (requestId.equals(jedis.get(lockKey))) { // 第二步: 若在此时,这把锁突然不是这个客户端的,则会误解锁 jedis.del(lockKey); } }
程式碼解釋: 透過requestId 判斷加鎖與解鎖是不是同一個客戶端和jedis.del(lockKey) 兩步不是原子操作,理論上會出現在執行完第一步if判斷操作後鎖其實已經過期,並且被其它線程獲取,這是時候在執行jedis.del(lockKey )操作,相當於把別人的鎖釋放了,這是不合理的。當然,這是非常極端的情況,如果unLock方法裡第一步和第二步沒有其它業務操作,把上面的程式碼扔到線上,可能也不會真的出現問題,原因第一是業務並發量不高,根本不會暴露這個缺陷,那麼問題還不大。
但是寫程式碼是嚴謹的工作,能完美則必須完美。針對上述程式碼中的問題,提出改進。
程式碼改進:
public class RedisTool { private static final Long RELEASE_SUCCESS = 1L; /** * 释放分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @return 是否释放成功 */ public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; } }
程式碼解釋:
使用 jedis 用戶端的 eval 方法和只需一行 script 腳本,即可解決方法一涉及的原子性問題。
3、面試官:
基於ZooKeeper 的分散式鎖定實現原理
答案:還是積分消費與積分累加的例子:事件A和事件B同時需要進行對積分的修改操作,兩台機器同時進行,正確的業務邏輯上讓一台機器先執行完後另外一個機器再執行,要么事件A先執行,要么事件B先執行,這樣才能保證不會出現A :2 -> B:2 -> A:3 -> B:3這種積分越花越多的情況(想到這種bug一旦上線,老闆要生氣了,我可能要哭了)。
怎麼辦?使用 zookeeper 分散式鎖定。
一個機器接收到了請求之後,先獲取zookeeper 上的一把分散式鎖(zk會創建一個znode),執行操作;然後另外一個機器也嘗試去創建那個znode,結果發現自己創建不了,因為被別人創建了,那隻能等待,等第一個機器執行完了方可拿到鎖。
使用ZooKeeper 的順序節點特性,假如我們在/lock/目錄下建立3個節點,ZK叢集會依照發起建立的順序來建立節點,節點分為/lock/0000000001、/lock/0000000002 、/lock/0000000003,最後一位數是依序遞增的,節點名由zk來完成。
ZK中還有一個名為臨時節點的節點,臨時節點由某個客戶端創建,當客戶端與ZK叢集斷開連接,則該節點自動被刪除。 EPHEMERAL_SEQUENTIAL為暫時順序節點。
分散式鎖定的基本邏輯是使用ZK中節點的存在與否作為鎖定狀態,以此實作分散式鎖定
客戶端呼叫create()方法建立名為「/dlm-locks/lockname/lock-」的臨時順序節點。
客戶端呼叫getChildren(“lockname”)方法來取得所有已經建立的子節點。
客戶端取得到所有子節點path之後,如果發現自己在步驟1中建立的節點是所有節點中序號最小的,就是看自己建立的序號是否排第一,如果是第一,那麼就認為這個客戶端獲得了鎖,在它前面沒有別的客戶端拿到鎖。
如果建立的節點不是所有節點中最小的,那麼就要監視比自己建立節點的序號小的最大的節點,然後進入等待狀態。在監視的子節點發生變更後,再取得子節點並判斷是否獲得鎖定。
儘管釋放鎖定的過程相對簡單,其實就是刪除已建立的子節點,但仍需考慮刪除節點失敗等異常情況。
額外補充
分散式鎖定還可以從資料庫下手解決問題
方法一:
利用 Mysql 的锁表,创建一张表,设置一个 UNIQUE KEY 这个 KEY 就是要锁的 KEY,所以同一个 KEY 在mysql表里只能插入一次了,这样对锁的竞争就交给了数据库,处理同一个 KEY 数据库保证了只有一个节点能插入成功,其他节点都会插入失败。
这样 lock 和 unlock 的思路就很简单了,伪代码:
def lock : exec sql: insert into locked—table (xxx) values (xxx) if result == true : return true else : return false def unlock : exec sql: delete from lockedOrder where order_id='order_id'
方法二:
使用流水号+时间戳做幂等操作,可以看作是一个不会释放的锁。
以上がRedis 分散ロックの実装方法とその適用シナリオとはの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ホットAIツール

Undresser.AI Undress
リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover
写真から衣服を削除するオンライン AI ツール。

Undress AI Tool
脱衣画像を無料で

Clothoff.io
AI衣類リムーバー

Video Face Swap
完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

人気の記事

ホットツール

メモ帳++7.3.1
使いやすく無料のコードエディター

SublimeText3 中国語版
中国語版、とても使いやすい

ゼンドスタジオ 13.0.1
強力な PHP 統合開発環境

ドリームウィーバー CS6
ビジュアル Web 開発ツール

SublimeText3 Mac版
神レベルのコード編集ソフト(SublimeText3)

ホットトピック











Redisクラスターモードは、シャードを介してRedisインスタンスを複数のサーバーに展開し、スケーラビリティと可用性を向上させます。構造の手順は次のとおりです。異なるポートで奇妙なRedisインスタンスを作成します。 3つのセンチネルインスタンスを作成し、Redisインスタンスを監視し、フェールオーバーを監視します。 Sentinel構成ファイルを構成し、Redisインスタンス情報とフェールオーバー設定の監視を追加します。 Redisインスタンス構成ファイルを構成し、クラスターモードを有効にし、クラスター情報ファイルパスを指定します。各Redisインスタンスの情報を含むnodes.confファイルを作成します。クラスターを起動し、CREATEコマンドを実行してクラスターを作成し、レプリカの数を指定します。クラスターにログインしてクラスター情報コマンドを実行して、クラスターステータスを確認します。作る

Redisデータをクリアする方法:Flushallコマンドを使用して、すべての重要な値をクリアします。 FlushDBコマンドを使用して、現在選択されているデータベースのキー値をクリアします。 [選択]を使用してデータベースを切り替え、FlushDBを使用して複数のデータベースをクリアします。 DELコマンドを使用して、特定のキーを削除します。 Redis-CLIツールを使用してデータをクリアします。

Redisのキューを読むには、キュー名を取得し、LPOPコマンドを使用して要素を読み、空のキューを処理する必要があります。特定の手順は次のとおりです。キュー名を取得します:「キュー:キュー」などの「キュー:」のプレフィックスで名前を付けます。 LPOPコマンドを使用します。キューのヘッドから要素を排出し、LPOP Queue:My-Queueなどの値を返します。空のキューの処理:キューが空の場合、LPOPはnilを返し、要素を読む前にキューが存在するかどうかを確認できます。

Redis指令を使用するには、次の手順が必要です。Redisクライアントを開きます。コマンド(動詞キー値)を入力します。必要なパラメーターを提供します(指示ごとに異なります)。 Enterを押してコマンドを実行します。 Redisは、操作の結果を示す応答を返します(通常はOKまたは-ERR)。

Redisを使用して操作をロックするには、setnxコマンドを介してロックを取得し、有効期限を設定するために有効期限コマンドを使用する必要があります。特定の手順は次のとおりです。(1)SETNXコマンドを使用して、キー価値ペアを設定しようとします。 (2)expireコマンドを使用して、ロックの有効期限を設定します。 (3)Delコマンドを使用して、ロックが不要になったときにロックを削除します。

Centosシステムでは、Redis構成ファイルを変更するか、Redisコマンドを使用して悪意のあるスクリプトがあまりにも多くのリソースを消費しないようにすることにより、LUAスクリプトの実行時間を制限できます。方法1:Redis構成ファイルを変更し、Redis構成ファイルを見つけます:Redis構成ファイルは通常/etc/redis/redis.confにあります。構成ファイルの編集:テキストエディター(VIやNANOなど)を使用して構成ファイルを開きます:sudovi/etc/redis/redis.conf luaスクリプト実行時間制限を設定します。

Redisコマンドラインツール(Redis-Cli)を使用して、次の手順を使用してRedisを管理および操作します。サーバーに接続し、アドレスとポートを指定します。コマンド名とパラメーターを使用して、コマンドをサーバーに送信します。ヘルプコマンドを使用して、特定のコマンドのヘルプ情報を表示します。 QUITコマンドを使用して、コマンドラインツールを終了します。

Debian Systemsでは、Directoryコンテンツを読み取るためにReadDirシステム呼び出しが使用されます。パフォーマンスが良くない場合は、次の最適化戦略を試してください。ディレクトリファイルの数を簡素化します。大きなディレクトリをできる限り複数の小さなディレクトリに分割し、Readdirコールごとに処理されたアイテムの数を減らします。ディレクトリコンテンツのキャッシュを有効にする:キャッシュメカニズムを構築し、定期的にキャッシュを更新するか、ディレクトリコンテンツが変更されたときに、頻繁な呼び出しをreaddirに削減します。メモリキャッシュ(memcachedやredisなど)またはローカルキャッシュ(ファイルやデータベースなど)を考慮することができます。効率的なデータ構造を採用する:ディレクトリトラバーサルを自分で実装する場合、より効率的なデータ構造(線形検索の代わりにハッシュテーブルなど)を選択してディレクトリ情報を保存およびアクセスする
