首頁  >  文章  >  資料庫  >  Redis分散式鎖怎麼實現及應用場景是什麼

Redis分散式鎖怎麼實現及應用場景是什麼

PHPz
PHPz轉載
2023-05-30 17:55:511591瀏覽

    引言

    鎖是開發過程中十分常見的工具,你一定不陌生,悲觀鎖,樂觀鎖,排它鎖,公平鎖,非公平鎖等等,很多概念,如果你對java裡的鎖還不了解,可以參考這篇:不可不說的Java「鎖」事,這篇寫的很全面了,但是對於初學者,知道這些鎖的概念,由於缺乏實際工作經驗,可能並不了解鎖的實際使用場景,Java中可以透過Volatile、Synchronized、ReentrantLock 三個關鍵字來實現線程的安全,這部分知識在第一輪基礎面試裡一定會問(要熟練哦)。

    在分散式系統中Java這些鎖技術是無法同時鎖住兩台機器上的程式碼,所以要透過分散式鎖來實現,熟練使用分散式鎖也是大廠開發必會的技能。

    1、面試官:

    你有遇到需要使用分散式鎖定的場景嗎?

    問題分析:這個問題主要作為引子,先要了解什麼場景下需要使用分散式鎖,分散式鎖要解決什麼問題,在此前提下有助於你更好的理解分散式鎖的實現原理。

    使用分散式鎖定的場景一般需要滿足以下場景:

    • 系統是一個分散式系統,java的鎖已經鎖不住了。

    • 操作共享資源,例如庫裡唯一的使用者資料。

    • 同步訪問,即多個行程同時操作共享資源。

    答案:說一個我在專案中使用分散式鎖定場景的例子:

    消費積分在很多系統裡都有,信用卡,電商網站,透過積分換禮品等,這裡「消費積分」這個操作是需要使用鎖的典型場景。

    事件A:

     以積分兌換禮品為例來講,完整的積分消費過程簡單分成3步驟:

    A1:使用者選取商品,發起兌換提交訂單。

    A2:系統讀取使用者剩餘積分:判斷使用者目前積分是否足夠。

    A3:扣掉用戶積分。

    事件B: 

    系統發給使用者積分也簡單分成3步驟:

    B1:計算使用者當天應得積分

    B2:讀取用戶原有積分

    B3:在原有積分上增加本次應得積分

    那麼問題來了,如果用戶消費積分和用戶累加積分同時發生(同時用戶積分進行操作)會怎樣?

    假設:用戶在消費積分的同時恰好離線任務在計算積分給用戶發放積分(如根據用戶當天的消費額),這兩件事同時進行,下面的邏輯有點繞,耐心理解。

    用戶U有1000點(記錄用戶積分的資料可以理解為共享資源),本次兌換要消耗掉999點。

    不加鎖的情況:事件A程式在執行到第2步讀取積分時,A:2操作讀到的結果是1000分,判斷剩餘積分夠本次兌換,緊接著要執行第3步驟A:3操作扣積分(1000 - 999 = 1),正常結果應該是使用者還是1分。但是這個時候事件B也在執行,這次給用戶U發放100積分,兩個線程同時進行(同步訪問),不加鎖的情況,就會有下面這種可能,A:2 -> B :2 -> A:3 -> B:3 ,在A:3尚未完成前(扣積分,1000 - 999),用戶U總積分被事件B的線程讀取了,最後用戶U的總積分變成了1100分,還白白兌換了一個999積分的禮物,這顯然不符合預期結果。

    有人說怎麼可能這麼巧同時操作用戶積分,cpu那麼快,只要用戶夠多,並發量夠大,墨菲定律遲早生效,出現上述bug只是時間問題,還有可能被黑產業界卡住這個bug瘋狂薅羊毛,這個時候作為開發人員要解決這個隱患就必須了解鎖的使用。

    (寫程式碼是一項嚴謹的事兒!)

    Java本身提供了兩種內建的鎖的實現,一種是由JVM實現的synchronized 和JDK 提供的Lock,而很多原子操作類別都是線程安全的,當你的應用是單機或說單進程應用時,可以使用這兩種鎖來實現鎖。

    但是當下網路公司的系統幾乎都是分散式的,這時候Java自帶的synchronized 或Lock 已經無法滿足分散式環境下鎖的要求了,因為程式碼會部署在多台機器上,為了解決這個問題,分散式鎖應運而生,分散式鎖的特點是多進程,多個實體機器上無法共享內存,常見的解決方法是基於內存層的干涉,落地方案就是基於Redis的分散式鎖or ZooKeeper分散式鎖定。

    (我分析的不能更詳細了,面試官再不滿意?)

    2、面試官:

    Redis分散式鎖定實作方法

    目前有兩種主要的實作方式來解決分散式鎖定問題:一種是基於Redis Cluster模式,另一種則是…。 2.基於Zookeeper 集群模式。

    優先掌握這兩種,應付面試基本上沒問題了。

    答:

    1. Distributed lock based on Redis

    Method 1: Use the setnx command to lock

    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);
        }
    }

    Code explanation:

    setnx command, It means set if not exist. If the lockKey does not exist, store the key in Redis. If the result returns 1 after successful saving, it means the setting is successful. If it is not 1, it means failure. Other threads have already set it.

    expire(), set the expiration time to prevent deadlock. Assume that if a lock is not deleted after being set, then the lock is equivalent to always existing, resulting in a deadlock.

    (At this point, I would like to emphasize a "but" to the interviewer)

    Think about it, what are the flaws in my method above? Continue to explain to the interviewer...

    There are two steps to locking. The first step is jedis.setnx, and the second step is jedis.expire to set the expiration time. setnx and expire are not an atomic operation. If the program is executed after the An exception occurred after the first step. In the second step, jedis.expire(lockKey, expireTime) was not executed, which means that the lock has no expiration time, and a deadlock may occur. How to improve this problem?

    Improvement:

    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;//加锁失败
        }
    }

    Code explanation:

    Combine locking and setting expiration time into one, one line of code, atomic operation.

    (The interviewer was very satisfied before asking further questions)

    3. Interviewer: What about the unlocking operation?

    Answer:

    Releasing the lock means deleting the key

    Use the del command to unlock

    public static void unLock(Jedis jedis, String lockKey, String requestId) {
        // 第一步: 使用 requestId 判断加锁与解锁是不是同一个客户端
        if (requestId.equals(jedis.get(lockKey))) {
            // 第二步: 若在此时,这把锁突然不是这个客户端的,则会误解锁
            jedis.del(lockKey);
        }
    }

    Code explanation: Use the requestId to determine whether the lock and unlock are the same The two steps of client and jedis.del(lockKey) are not atomic operations. In theory, it will appear after executing the first if judgment operation that the lock has actually expired and been acquired by other threads. This is the time to execute jedis.del(lockKey ) operation is equivalent to releasing someone else's lock, which is unreasonable. Of course, this is a very extreme situation. If there are no other business operations in the first and second steps of the unLock method, throwing the above code online may not really cause problems. The first reason is the business concurrency. If it is not high, this flaw will not be exposed at all, so the problem is not big.

    But writing code is rigorous work, and to be perfect, you must be perfect. Improvements are proposed to address the problems in the above code.

    Code improvement:

    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;
        }
    }

    Code explanation:

    Use the eval method of the jedis client and just one line of script to solve the atomicity problem involved in method one.

    3. Interviewer:

    Distributed lock implementation principle based on ZooKeeper

    Answer: This is still an example of point consumption and point accumulation: event A and event B need to be carried out at the same time The points modification operation is performed on two machines at the same time. The correct business logic is to let one machine execute it first and then the other machine. Either event A is executed first, or event B is executed first, so as to ensure that A will not occur. :2 -> B:2 -> A:3 -> B:3 The more points you spend, the more points you spend (thinking that once this kind of bug goes online, the boss will be angry, I may cry).

    what to do? Use zookeeper distributed locks.

    After a machine receives the request, it first obtains a distributed lock on zookeeper (zk will create a znode) and performs the operation; then another machine also tries to create the znode, but finds that it cannot create it. , because it was created by someone else, you can only wait until the first machine finishes executing before you can get the lock.

    Using the sequential node feature of ZooKeeper, if we create 3 nodes in the /lock/ directory, the ZK cluster will create the nodes in the order in which they are created. The nodes are divided into /lock/0000000001, /lock/0000000002 , /lock/0000000003, the last digit is incremented in sequence, and the node name is completed by zk.

    ZK also has a node called a temporary node. The temporary node is created by a client. When the client disconnects from the ZK cluster, the node is automatically deleted. EPHEMERAL_SEQUENTIAL is a temporary sequence node.

    The basic logic of distributed locks is to use the presence or absence of nodes in ZK as the lock status to implement distributed locks

    • The client calls the create() method Create a temporary sequence node named "/dlm-locks/lockname/lock-".

    • The client calls the getChildren("lockname") method to obtain all created child nodes.

    • After the client obtains the paths of all child nodes, if it finds that the node it created in step 1 has the smallest sequence number among all nodes, it will check whether the sequence number it created ranks first. First, if it is first, then it is considered that this client has obtained the lock, and no other client has obtained the lock before it.

    • If the created node is not the smallest of all nodes, then it is necessary to monitor the largest node with a smaller sequence number than the node it created, and then enter the waiting state. After the monitored child node changes, obtain the child node and determine whether to obtain the lock.

    Although the process of releasing the lock is relatively simple, which is actually deleting the created child node, you still need to consider abnormal situations such as failure to delete the node.

    Additional supplement

    Distributed locks can also solve the problem from the database

    方法一:

    利用 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中文網其他相關文章!

    陳述:
    本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除