redis系列:分布式鎖

1 介紹

這篇博文講介紹如何一步步構建一個基于Redis的分布式鎖。會從最原始的版本開始,然后根據問題進行調整,最后完成一個較為合理的分布式鎖。

本篇文章會將分布式鎖的實現分為兩部分,一個是單機環境,另一個是集群環境下的Redis鎖實現。在介紹分布式鎖的實現之前,先來了解下分布式鎖的一些信息。

2 分布式鎖

2.1 什么是分布式鎖?

分布式鎖是控制分布式系統或不同系統之間共同訪問共享資源的一種鎖實現,如果不同的系統或同一個系統的不同主機之間共享了某個資源時,往往需要互斥來防止彼此干擾來保證一致性。

2.2 分布式鎖需要具備哪些條件

  1. 互斥性:在任意一個時刻,只有一個客戶端持有鎖。
  2. 無死鎖:即便持有鎖的客戶端崩潰或者其他意外事件,鎖仍然可以被獲取。
  3. 容錯:只要大部分Redis節點都活著,客戶端就可以獲取和釋放鎖

2.4 分布式鎖的實現有哪些?

  1. 數據庫
  2. Memcached(add命令)
  3. Redis(setnx命令)
  4. Zookeeper(臨時節點)
  5. 等等

3 單機Redis的分布式鎖

3.1 準備工作

3.1.1 定義常量類

public class LockConstants {public static final String OK = "OK";/** NX|XX, NX -- Only set the key if it does not already exist. XX -- Only set the key if it already exist. **/public static final String NOT_EXIST = "NX";public static final String EXIST = "XX";/** expx EX|PX, expire time units: EX = seconds; PX = milliseconds **/public static final String SECONDS = "EX";public static final String MILLISECONDS = "PX";private LockConstants() {}
}
復制代碼

3.1.2 定義鎖的抽象類

抽象類RedisLock實現java.util.concurrent包下的Lock接口,然后對一些方法提供默認實現,子類只需實現lock方法和unlock方法即可。代碼如下

public abstract class RedisLock implements Lock {protected Jedis jedis;protected String lockKey;public RedisLock(Jedis jedis,String lockKey) {this(jedis, lockKey);}public void sleepBySencond(int sencond){try {Thread.sleep(sencond*1000);} catch (InterruptedException e) {e.printStackTrace();}}@Overridepublic void lockInterruptibly(){}@Overridepublic Condition newCondition() {return null;}@Overridepublic boolean tryLock() {return false;}@Overridepublic boolean tryLock(long time, TimeUnit unit){return false;}}
復制代碼

3.2 最基礎的版本1

先來一個最基礎的版本,代碼如下

public class LockCase1 extends RedisLock {public LockCase1(Jedis jedis, String name) {super(jedis, name);}@Overridepublic void lock() {while(true){String result = jedis.set(lockKey, "value", NOT_EXIST);if(OK.equals(result)){System.out.println(Thread.currentThread().getId()+"加鎖成功!");break;}}}@Overridepublic void unlock() {jedis.del(lockKey);}
}
復制代碼

LockCase1類提供了lock和unlock方法。
其中lock方法也就是在reids客戶端執行如下命令

SET lockKey value NX
復制代碼

而unlock方法就是調用DEL命令將鍵刪除。
好了,方法介紹完了。現在來想想這其中會有什么問題?
假設有兩個客戶端A和B,A獲取到分布式的鎖。A執行了一會,突然A所在的服務器斷電了(或者其他什么的),也就是客戶端A掛了。這時出現一個問題,這個鎖一直存在,且不會被釋放,其他客戶端永遠獲取不到鎖。如下示意圖

可以通過設置過期時間來解決這個問題

3.3 版本2-設置鎖的過期時間

public void lock() {while(true){String result = jedis.set(lockKey, "value", NOT_EXIST,SECONDS,30);if(OK.equals(result)){System.out.println(Thread.currentThread().getId()+"加鎖成功!");break;}}
}
復制代碼

類似的Redis命令如下

SET lockKey value NX EX 30
復制代碼

注:要保證設置過期時間和設置鎖具有原子性

這時又出現一個問題,問題出現的步驟如下

  1. 客戶端A獲取鎖成功,過期時間30秒。
  2. 客戶端A在某個操作上阻塞了50秒。
  3. 30秒時間到了,鎖自動釋放了。
  4. 客戶端B獲取到了對應同一個資源的鎖。
  5. 客戶端A從阻塞中恢復過來,釋放掉了客戶端B持有的鎖。

示意圖如下

這時會有兩個問題

  1. 過期時間如何保證大于業務執行時間?
  2. 如何保證鎖不會被誤刪除?

先來解決如何保證鎖不會被誤刪除這個問題。
這個問題可以通過設置value為當前客戶端生成的一個隨機字符串,且保證在足夠長的一段時間內在所有客戶端的所有獲取鎖的請求中都是唯一的。

版本2的完整代碼:Github地址

3.4 版本3-設置鎖的value

抽象類RedisLock增加lockValue字段,lockValue字段的默認值為UUID隨機值假設當前線程ID。

public abstract class RedisLock implements Lock {//...protected String lockValue;public RedisLock(Jedis jedis,String lockKey) {this(jedis, lockKey, UUID.randomUUID().toString()+Thread.currentThread().getId());}public RedisLock(Jedis jedis, String lockKey, String lockValue) {this.jedis = jedis;this.lockKey = lockKey;this.lockValue = lockValue;}//...
}
復制代碼

加鎖代碼

public void lock() {while(true){String result = jedis.set(lockKey, lockValue, NOT_EXIST,SECONDS,30);if(OK.equals(result)){System.out.println(Thread.currentThread().getId()+"加鎖成功!");break;}}
}
復制代碼

解鎖代碼

public void unlock() {String lockValue = jedis.get(lockKey);if (lockValue.equals(lockValue)){jedis.del(lockKey);}
}
復制代碼

這時看看加鎖代碼,好像沒有什么問題啊。
再來看看解鎖的代碼,這里的解鎖操作包含三步操作:獲取值、判斷和刪除鎖。這時你有沒有想到在多線程環境下的i++操作?

3.4.1 i++問題

i++操作也可分為三個步驟:讀i的值,進行i+1,設置i的值。
如果兩個線程同時對i進行i++操作,會出現如下情況

  1. i設置值為0
  2. 線程A讀到i的值為0
  3. 線程B也讀到i的值為0
  4. 線程A執行了+1操作,將結果值1寫入到內存
  5. 線程B執行了+1操作,將結果值1寫入到內存
  6. 此時i進行了兩次i++操作,但是結果卻為1

在多線程環境下有什么方式可以避免這類情況發生?
解決方式有很多種,例如用AtomicInteger、CAS、synchronized等等。
這些解決方式的目的都是要確保i++ 操作的原子性。那么回過頭來看看解鎖,同理我們也是要確保解鎖的原子性。我們可以利用Redis的lua腳本來實現解鎖操作的原子性。

版本3的完整代碼:Github地址

3.5 版本4-具有原子性的釋放鎖

lua腳本內容如下

if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end
復制代碼

這段Lua腳本在執行的時候要把的lockValue作為ARGV[1]的值傳進去,把lockKey作為KEYS[1]的值傳進去。現在來看看解鎖的java代碼

public void unlock() {// 使用lua腳本進行原子刪除操作String checkAndDelScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +"return redis.call('del', KEYS[1]) " +"else " +"return 0 " +"end";jedis.eval(checkAndDelScript, 1, lockKey, lockValue);
}
復制代碼

好了,解鎖操作也確保了原子性了,那么是不是單機Redis環境的分布式鎖到此就完成了?
別忘了版本2-設置鎖的過期時間還有一個,過期時間如何保證大于業務執行時間問題沒有解決。

版本4的完整代碼:Github地址

3.6 版本5-確保過期時間大于業務執行時間

抽象類RedisLock增加一個boolean類型的屬性isOpenExpirationRenewal,用來標識是否開啟定時刷新過期時間。
在增加一個scheduleExpirationRenewal方法用于開啟刷新過期時間的線程。

public abstract class RedisLock implements Lock {//...protected volatile boolean isOpenExpirationRenewal = true;/*** 開啟定時刷新*/protected void scheduleExpirationRenewal(){Thread renewalThread = new Thread(new ExpirationRenewal());renewalThread.start();}/*** 刷新key的過期時間*/private class ExpirationRenewal implements Runnable{@Overridepublic void run() {while (isOpenExpirationRenewal){System.out.println("執行延遲失效時間中...");String checkAndExpireScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +"return redis.call('expire',KEYS[1],ARGV[2]) " +"else " +"return 0 end";jedis.eval(checkAndExpireScript, 1, lockKey, lockValue, "30");//休眠10秒sleepBySencond(10);}}}
}
復制代碼

加鎖代碼在獲取鎖成功后將isOpenExpirationRenewal置為true,并且調用scheduleExpirationRenewal方法,開啟刷新過期時間的線程。

public void lock() {while (true) {String result = jedis.set(lockKey, lockValue, NOT_EXIST, SECONDS, 30);if (OK.equals(result)) {System.out.println("線程id:"+Thread.currentThread().getId() + "加鎖成功!時間:"+LocalTime.now());//開啟定時刷新過期時間isOpenExpirationRenewal = true;scheduleExpirationRenewal();break;}System.out.println("線程id:"+Thread.currentThread().getId() + "獲取鎖失敗,休眠10秒!時間:"+LocalTime.now());//休眠10秒sleepBySencond(10);}
}
復制代碼

解鎖代碼增加一行代碼,將isOpenExpirationRenewal屬性置為false,停止刷新過期時間的線程輪詢。

public void unlock() {//...isOpenExpirationRenewal = false;
}復制代碼

版本5的完整代碼:Github地址

3.7 測試

測試代碼如下

public void testLockCase5() {//定義線程池ThreadPoolExecutor pool = new ThreadPoolExecutor(0, 10,1, TimeUnit.SECONDS,new SynchronousQueue<>());//添加10個線程獲取鎖for (int i = 0; i < 10; i++) {pool.submit(() -> {try {Jedis jedis = new Jedis("localhost");LockCase5 lock = new LockCase5(jedis, lockName);lock.lock();//模擬業務執行15秒lock.sleepBySencond(15);lock.unlock();} catch (Exception e){e.printStackTrace();}});}//當線程池中的線程數為0時,退出while (pool.getPoolSize() != 0) {}
}
復制代碼

測試結果

或許到這里基于單機Redis環境的分布式就介紹完了。但是使用java的同學有沒有發現一個鎖的重要特性

那就是鎖的重入,那么分布式鎖的重入該如何實現呢?這里就留一個坑了

4 集群Redis的分布式鎖

在Redis的分布式環境中,Redis 的作者提供了RedLock 的算法來實現一個分布式鎖。

4.1 加鎖

RedLock算法加鎖步驟如下

  1. 獲取當前Unix時間,以毫秒為單位。
  2. 依次嘗試從N個實例,使用相同的key和隨機值獲取鎖。在步驟2,當向Redis設置鎖時,客戶端應該設置一個網絡連接和響應超時時間,這個超時時間應該小于鎖的失效時間。例如你的鎖自動失效時間為10秒,則超時時間應該在5-50毫秒之間。這樣可以避免服務器端Redis已經掛掉的情況下,客戶端還在死死地等待響應結果。如果服務器端沒有在規定時間內響應,客戶端應該盡快嘗試另外一個Redis實例。
  3. 客戶端使用當前時間減去開始獲取鎖時間(步驟1記錄的時間)就得到獲取鎖使用的時間。當且僅當從大多數(這里是3個節點)的Redis節點都取到鎖,并且使用的時間小于鎖失效時間時,鎖才算獲取成功。
  4. 如果取到了鎖,key的真正有效時間等于有效時間減去獲取鎖所使用的時間(步驟3計算的結果)。
  5. 如果因為某些原因,獲取鎖失敗(沒有在至少N/2+1個Redis實例取到鎖或者取鎖時間已經超過了有效時間),客戶端應該在所有的Redis實例上進行解鎖(即便某些Redis實例根本就沒有加鎖成功)。

4.2 解鎖

向所有的Redis實例發送釋放鎖命令即可,不用關心之前有沒有從Redis實例成功獲取到鎖.


關于RedLock算法,還有一個小插曲,就是Martin Kleppmann 和?RedLock 作者 antirez的對RedLock算法的互懟。 官網原話如下

Martin Kleppmann analyzed Redlock here. I disagree with the analysis and posted my reply to his analysis here.

更多關于RedLock算法這里就不在說明,有興趣的可以到官網閱讀相關文章。

5 總結

這篇文章講述了一個基于Redis的分布式鎖的編寫過程及解決問題的思路,但是本篇文章實現的分布式鎖并不適合用于生產環境。java環境有 Redisson 可用于生產環境,但是分布式鎖還是Zookeeper會比較好一些(可以看Martin Kleppmann 和?RedLock的分析)。

Martin Kleppmann對RedLock的分析:martin.kleppmann.com/2016/02/08/…

RedLock 作者 antirez的回應:antirez.com/news/101

整個項目的地址存放在Github上,有需要的可以看看:Github地址

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/389931.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/389931.shtml
英文地址,請注明出處:http://en.pswp.cn/news/389931.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Qt中的坐標系統

轉載&#xff1a;原野追逐 Qt使用統一的坐標系統來定位窗口部件的位置和大小。 以屏幕的左上角為原點即(0, 0)點&#xff0c;從左向右為x軸正向&#xff0c;從上向下為y軸正向&#xff0c;這整個屏幕的坐標系統就用來定位頂層窗口&#xff1b; 此外&#xff0c;窗口內部也有自己…

預測股票價格 模型_建立有馬模型來預測股票價格

預測股票價格 模型前言 (Preface) If you are reading this, it’s most likely because you love to solve puzzles. I’m a very competitive person by nature. The Mt. Everest of puzzles, in my opinion, is trying to find excess returns through active trading in th…

Python 模塊 timedatetime

time & datetime 模塊 在平常的代碼中&#xff0c;我們常常需要與時間打交道。在Python中&#xff0c;與時間處理有關的模塊就包括&#xff1a;time&#xff0c;datetime,calendar(很少用&#xff0c;不講)&#xff0c;下面分別來介紹。 在開始之前&#xff0c;首先要說明幾…

大數模板Java

import java.util.*; import java.math.BigInteger; public class Main{public static void main(String args[]){Scanner cinnew Scanner(System.in);BigInteger a,b;acin.nextBigInteger();bcin.nextBigInteger();System.out.println(a.add(b));//加法System.out.println(a.…

檸檬工會_工會經營者

檸檬工會Hey guys! This week we’ll be going over some ways to work with result sets in MySQL. These result sets are the outputs of your everyday queries, such as:大家好&#xff01; 本周&#xff0c;我們將介紹一些在MySQL中處理結果集的方法。 這些結果集是您日常…

229. 求眾數 II

229. 求眾數 II 給定一個大小為 n 的整數數組&#xff0c;找出其中所有出現超過 ? n/3 ? 次的元素。 示例 1&#xff1a;輸入&#xff1a;[3,2,3] 輸出&#xff1a;[3]示例 2&#xff1a;輸入&#xff1a;nums [1] 輸出&#xff1a;[1]示例 3&#xff1a;輸入&#xff1a;…

寫給Java開發者看的JavaScript對象機制

幫助面向對象開發者理解關于JavaScript對象機制 本文是以一個熟悉OO語言的開發者視角&#xff0c;來解釋JavaScript中的對象。 對于不了解JavaScript 語言&#xff0c;尤其是習慣了OO語言的開發者來說&#xff0c;由于語法上些許的相似會讓人產生心理預期&#xff0c;JavaScrip…

Pythonic---------詳細講解

作者&#xff1a;半載流殤 鏈接&#xff1a;https://zhuanlan.zhihu.com/p/35219750 來源&#xff1a;知乎 著作權歸作者所有。商業轉載請聯系作者獲得授權&#xff0c;非商業轉載請注明出處。Pythonic&#xff0c;簡言之就是以Python這門語言獨特的方式寫出既簡潔又優美的代碼…

大數據ab 測試_在真實數據上進行AB測試應用程序

大數據ab 測試Hello Everyone!大家好&#xff01; I am back with another article about Data Science. In this article, I will write about what is A-B testing and how to use it on real life data-set to compare two advertisement methods.我回來了另一篇有關數據科…

492. 構造矩形

492. 構造矩形 作為一位web開發者&#xff0c; 懂得怎樣去規劃一個頁面的尺寸是很重要的。 現給定一個具體的矩形頁面面積&#xff0c;你的任務是設計一個長度為 L 和寬度為 W 且滿足以下要求的矩形的頁面。要求&#xff1a; 你設計的矩形頁面必須等于給定的目標面積。 寬度 …

node:爬蟲爬取網頁圖片

前言 周末自己在家閑著沒事&#xff0c;刷著微信&#xff0c;玩著手機&#xff0c;發現自己的微信頭像該換了&#xff0c;就去網上找了一下頭像&#xff0c;看著圖片&#xff0c;自己就想著作為一個碼農&#xff0c;可以把這些圖片都爬取下來做成一個微信小程序&#xff0c;說干…

如何更好的掌握一個知識點_如何成為一個更好的講故事的人3個關鍵點

如何更好的掌握一個知識點You’re launching a digital transformation initiative in the middle of the ongoing pandemic. You are pretty excited about this big-ticket investment, which has the potential to solve remote-work challenges that your organization fac…

centos 搭建jenkins+git+maven

gitmavenjenkins持續集成搭建發布人:[李源] 2017-12-08 04:33:37 一、搭建說明 系統&#xff1a;centos 6.5 jdk&#xff1a;1.8.0_144 jenkins&#xff1a;jenkins-2.93-1.1 git&#xff1a;git-2.9.0 maven&#xff1a;Maven 3.3.9 二、部署 2.1、jdk安裝 1&#xff09;下…

638. 大禮包

638. 大禮包 在 LeetCode 商店中&#xff0c; 有 n 件在售的物品。每件物品都有對應的價格。然而&#xff0c;也有一些大禮包&#xff0c;每個大禮包以優惠的價格捆綁銷售一組物品。 給你一個整數數組 price 表示物品價格&#xff0c;其中 price[i] 是第 i 件物品的價格。另有…

記錄一次spark連接mysql遇到的問題

在使用spark連接mysql的過程中報錯了&#xff0c;錯誤如下 08:51:32.495 [main] ERROR - Error loading factory org.apache.calcite.jdbc.CalciteJdbc41Factory java.lang.NoClassDefFoundError: org/apache/calcite/linq4j/QueryProviderat java.lang.ClassLoader.defineCla…

什么事數據科學_如果您想進入數據科學,則必須知道的7件事

什么事數據科學No way. No freaking way to enter data science any time soon…That is exactly what I thought a year back.沒門。 很快就不會出現進入數據科學的怪異方式 ……這正是我一年前的想法。 A little bit about my data science story: I am a complete beginner…

python基礎03——數據類型string

1. 字符串介紹 在python中&#xff0c;引號中加了引號的字符都被認為是字符串。 1 namejim 2 address"beijing" 3 msg My name is Jim, I am 22 years old! 那單引號、雙引號、多引號有什么區別呢&#xff1f; 1) 單雙引號木有任何區別&#xff0c;部分情況 需要考慮…

Java基礎-基本數據類型

Java中常見的轉義字符: 某些字符前面加上\代表了一些特殊含義: \r :return 表示把光標定位到本行行首. \n :next 表示把光標定位到下一行同樣的位置. 單獨使用在某些平臺上會產生不同的效果.通常這兩個一起使用,即:\r\n. 表示換行. \t :tab鍵,長度上相當于四個或者是八個空格 …

季節性時間序列數據分析_如何指導時間序列數據的探索性數據分析

季節性時間序列數據分析為什么要進行探索性數據分析&#xff1f; (Why Exploratory Data Analysis?) You might have heard that before proceeding with a machine learning problem it is good to do en end-to-end analysis of the data by carrying a proper exploratory …

TortoiseGit上傳項目到GitHub

1. 簡介 gitHub是一個面向開源及私有軟件項目的托管平臺&#xff0c;因為只支持git 作為唯一的版本庫格式進行托管&#xff0c;故名gitHub。 2. 準備 2.1 安裝git&#xff1a;https://git-scm.com/downloads。無腦安裝 2.2 安裝TortoiseGit(小烏龜)&#xff1a;https://torto…