ホームページ  >  記事  >  データベース  >  Redis 分散ロックの実装方法とその適用シナリオとは

Redis 分散ロックの実装方法とその適用シナリオとは

PHPz
PHPz転載
2023-05-30 17:55:511590ブラウズ

    #はじめに

    ロックは開発プロセスで非常に一般的なツールです。悲観的ロック、楽観的ロック、排他的ロック、公正なロックに精通している必要があります。ロック、不公平なロックなど、多くの概念があります。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 サイトの他の関連記事を参照してください。

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