目錄
引言
2、面試官:
你有遇到需要使用分散式鎖定的場景嗎?
事件A:
事件B: 
Redis分散式鎖定實作方法
1. Distributed lock based on Redis
3. Interviewer: What about the unlocking operation?
Use the del command to unlock
3. Interviewer:
Distributed lock implementation principle based on ZooKeeper
Additional supplement
方法一:
方法二:
首頁 資料庫 Redis Redis分散式鎖怎麼實現及應用場景是什麼

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

May 30, 2023 pm 05:55 PM
redis

    引言

    鎖是開發過程中十分常見的工具,你一定不陌生,悲觀鎖,樂觀鎖,排它鎖,公平鎖,非公平鎖等等,很多概念,如果你對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中文網其他相關文章!

    本網站聲明
    本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

    熱AI工具

    Undresser.AI Undress

    Undresser.AI Undress

    人工智慧驅動的應用程序,用於創建逼真的裸體照片

    AI Clothes Remover

    AI Clothes Remover

    用於從照片中去除衣服的線上人工智慧工具。

    Undress AI Tool

    Undress AI Tool

    免費脫衣圖片

    Clothoff.io

    Clothoff.io

    AI脫衣器

    Video Face Swap

    Video Face Swap

    使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

    熱門文章

    <🎜>:泡泡膠模擬器無窮大 - 如何獲取和使用皇家鑰匙
    3 週前 By 尊渡假赌尊渡假赌尊渡假赌
    北端:融合系統,解釋
    3 週前 By 尊渡假赌尊渡假赌尊渡假赌
    Mandragora:巫婆樹的耳語 - 如何解鎖抓鉤
    3 週前 By 尊渡假赌尊渡假赌尊渡假赌

    熱工具

    記事本++7.3.1

    記事本++7.3.1

    好用且免費的程式碼編輯器

    SublimeText3漢化版

    SublimeText3漢化版

    中文版,非常好用

    禪工作室 13.0.1

    禪工作室 13.0.1

    強大的PHP整合開發環境

    Dreamweaver CS6

    Dreamweaver CS6

    視覺化網頁開發工具

    SublimeText3 Mac版

    SublimeText3 Mac版

    神級程式碼編輯軟體(SublimeText3)

    熱門話題

    Java教學
    1666
    14
    CakePHP 教程
    1425
    52
    Laravel 教程
    1327
    25
    PHP教程
    1273
    29
    C# 教程
    1253
    24
    redis集群模式怎麼搭建 redis集群模式怎麼搭建 Apr 10, 2025 pm 10:15 PM

    Redis集群模式通過分片將Redis實例部署到多個服務器,提高可擴展性和可用性。搭建步驟如下:創建奇數個Redis實例,端口不同;創建3個sentinel實例,監控Redis實例並進行故障轉移;配置sentinel配置文件,添加監控Redis實例信息和故障轉移設置;配置Redis實例配置文件,啟用集群模式並指定集群信息文件路徑;創建nodes.conf文件,包含各Redis實例的信息;啟動集群,執行create命令創建集群並指定副本數量;登錄集群執行CLUSTER INFO命令驗證集群狀態;使

    redis數據怎麼清空 redis數據怎麼清空 Apr 10, 2025 pm 10:06 PM

    如何清空 Redis 數據:使用 FLUSHALL 命令清除所有鍵值。使用 FLUSHDB 命令清除當前選定數據庫的鍵值。使用 SELECT 切換數據庫,再使用 FLUSHDB 清除多個數據庫。使用 DEL 命令刪除特定鍵。使用 redis-cli 工具清空數據。

    redis怎麼讀取隊列 redis怎麼讀取隊列 Apr 10, 2025 pm 10:12 PM

    要從 Redis 讀取隊列,需要獲取隊列名稱、使用 LPOP 命令讀取元素,並處理空隊列。具體步驟如下:獲取隊列名稱:以 "queue:" 前綴命名,如 "queue:my-queue"。使用 LPOP 命令:從隊列頭部彈出元素並返回其值,如 LPOP queue:my-queue。處理空隊列:如果隊列為空,LPOP 返回 nil,可先檢查隊列是否存在再讀取元素。

    centos redis如何配置Lua腳本執行時間 centos redis如何配置Lua腳本執行時間 Apr 14, 2025 pm 02:12 PM

    在CentOS系統上,您可以通過修改Redis配置文件或使用Redis命令來限制Lua腳本的執行時間,從而防止惡意腳本佔用過多資源。方法一:修改Redis配置文件定位Redis配置文件:Redis配置文件通常位於/etc/redis/redis.conf。編輯配置文件:使用文本編輯器(例如vi或nano)打開配置文件:sudovi/etc/redis/redis.conf設置Lua腳本執行時間限制:在配置文件中添加或修改以下行,設置Lua腳本的最大執行時間(單位:毫秒)

    redis命令行怎麼用 redis命令行怎麼用 Apr 10, 2025 pm 10:18 PM

    使用 Redis 命令行工具 (redis-cli) 可通過以下步驟管理和操作 Redis:連接到服務器,指定地址和端口。使用命令名稱和參數向服務器發送命令。使用 HELP 命令查看特定命令的幫助信息。使用 QUIT 命令退出命令行工具。

    redis計數器怎麼實現 redis計數器怎麼實現 Apr 10, 2025 pm 10:21 PM

    Redis計數器是一種使用Redis鍵值對存儲來實現計數操作的機制,包含以下步驟:創建計數器鍵、增加計數、減少計數、重置計數和獲取計數。 Redis計數器的優勢包括速度快、高並發、持久性和簡單易用。它可用於用戶訪問計數、實時指標跟踪、遊戲分數和排名以及訂單處理計數等場景。

    redis過期策略怎麼設置 redis過期策略怎麼設置 Apr 10, 2025 pm 10:03 PM

    Redis數據過期策略有兩種:定期刪除:定期掃描刪除過期鍵,可通過 expired-time-cap-remove-count、expired-time-cap-remove-delay 參數設置。惰性刪除:僅在讀取或寫入鍵時檢查刪除過期鍵,可通過 lazyfree-lazy-eviction、lazyfree-lazy-expire、lazyfree-lazy-user-del 參數設置。

    如何優化debian readdir的性能 如何優化debian readdir的性能 Apr 13, 2025 am 08:48 AM

    在Debian系統中,readdir系統調用用於讀取目錄內容。如果其性能表現不佳,可嘗試以下優化策略:精簡目錄文件數量:盡可能將大型目錄拆分成多個小型目錄,降低每次readdir調用處理的項目數量。啟用目錄內容緩存:構建緩存機制,定期或在目錄內容變更時更新緩存,減少對readdir的頻繁調用。內存緩存(如Memcached或Redis)或本地緩存(如文件或數據庫)均可考慮。採用高效數據結構:如果自行實現目錄遍歷,選擇更高效的數據結構(例如哈希表而非線性搜索)存儲和訪問目錄信

    See all articles