詳解Redis分布式鎖的原理與實現(xiàn)
前言
在單體應(yīng)用中,如果我們對共享數(shù)據(jù)不進行加鎖操作,會出現(xiàn)數(shù)據(jù)一致性問題,我們的解決辦法通常是加鎖。在分布式架構(gòu)中,我們同樣會遇到數(shù)據(jù)共享操作問題,此時,我們就需要分布式鎖來解決問題,下面我們一起聊聊使用redis來實現(xiàn)分布式鎖。
使用場景
- 庫存超賣 比如 5個筆記本 A 看 準備買3個 B 買2個 C 4個 一下單 3+2+4 =9
- 防止用戶重復(fù)下單
- MQ消息去重
- 訂單操作變更
為什么要使用分布式鎖
從業(yè)務(wù)場景來分析,有一個共性,共享資源的競爭,比如庫存商品,用戶,消息,訂單等,這些資源在同一時間點只能有一個線程去操作,并且在操作期間,禁止其他線程操作。要達到這個效果,就要實現(xiàn)共享資源互斥,共享資源串行化。其實,就是對共享資源加鎖的問題。在單應(yīng)用(單進程多線程)中使用鎖,我們可以使用synchronize、ReentrantLock等關(guān)鍵字,對共享資源進行加鎖。在分布式應(yīng)用(多進程多線程)中,分布式鎖是控制分布式系統(tǒng)之間同步訪問共享資源的一種方式。
如何使用分布式鎖
流程圖

分布式鎖的狀態(tài)
- 客戶端通過競爭獲取鎖才能對共享資源進行操作
- 當(dāng)持有鎖的客戶端對共享資源進行操作時
- 其他客戶端都不可以對這個資源進行操作
- 直到持有鎖的客戶端完成操作
分布式鎖的特點
互斥性
在任意時刻,只有一個客戶端可以持有鎖(排他性)
高可用,具有容錯性
只要鎖服務(wù)集群中的大部分節(jié)點正常運行,客戶端就可以進行加鎖解鎖操作
避免死鎖
具備鎖失效機制,鎖在一段時間之后一定會釋放。(正常釋放或超時釋放)
加鎖和解鎖為同一個客戶端
一個客戶端不能釋放其他客戶端加的鎖了
分布式鎖的實現(xiàn)方式(以redis分布式鎖實現(xiàn)為例)
簡單版本
/**
* 簡單版本
* @author:liyajie
* @createTime:2022/6/22 15:42
* @version:1.0
*/
public class SimplyRedisLock {
// Redis分布式鎖的key
public static final String REDIS_LOCK = "redis_lock";
@Autowired
StringRedisTemplate template;
public String index(){
// 每個人進來先要進行加鎖,key值為"redis_lock",value隨機生成
String value = UUID.randomUUID().toString().replace("-","");
try{
// 加鎖
Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value);
// 加鎖失敗
if(!flag){
return "搶鎖失?。?;
}
System.out.println( value+ " 搶鎖成功");
// 業(yè)務(wù)邏輯
String result = template.opsForValue().get("001");
int total = result == null ? 0 : Integer.parseInt(result);
if (total > 0) {
int realTotal = total - 1;
template.opsForValue().set("001", String.valueOf(realTotal));
// 如果在搶到所之后,刪除鎖之前,發(fā)生了異常,鎖就無法被釋放,
// 釋放鎖操作不能在此操作,要在finally處理
// template.delete(REDIS_LOCK);
System.out.println("購買商品成功,庫存還剩:" + realTotal + "件");
return "購買商品成功,庫存還剩:" + realTotal + "件";
} else {
System.out.println("購買商品失敗");
}
return "購買商品失敗";
}finally {
// 釋放鎖
template.delete(REDIS_LOCK);
}
}
}
該種實現(xiàn)方案比較簡單,但是有一些問題。假如服務(wù)運行期間掛掉了,代碼完成了加鎖的處理,但是沒用走的finally部分,即鎖沒有釋放,這樣的情況下,鎖是永遠沒法釋放的。于是就有了改進版本。
進階版本
/**
* 進階版本
* @author:liyajie
* @createTime:2022/6/22 15:42
* @version:1.0
*/
public class SimplyRedisLock2 {
// Redis分布式鎖的key
public static final String REDIS_LOCK = "redis_lock";
@Autowired
StringRedisTemplate template;
public String index(){
// 每個人進來先要進行加鎖,key值為"redis_lock",value隨機生成
String value = UUID.randomUUID().toString().replace("-","");
try{
// 加鎖
Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L, TimeUnit.SECONDS);
// 加鎖失敗
if(!flag){
return "搶鎖失?。?;
}
System.out.println( value+ " 搶鎖成功");
// 業(yè)務(wù)邏輯
String result = template.opsForValue().get("001");
int total = result == null ? 0 : Integer.parseInt(result);
if (total > 0) {
int realTotal = total - 1;
template.opsForValue().set("001", String.valueOf(realTotal));
// 如果在搶到所之后,刪除鎖之前,發(fā)生了異常,鎖就無法被釋放,
// 釋放鎖操作不能在此操作,要在finally處理
// template.delete(REDIS_LOCK);
System.out.println("購買商品成功,庫存還剩:" + realTotal + "件");
return "購買商品成功,庫存還剩:" + realTotal + "件";
} else {
System.out.println("購買商品失敗");
}
return "購買商品失敗";
}finally {
// 釋放鎖
template.delete(REDIS_LOCK);
}
}
}
這種實現(xiàn)方案,對key增加了一個過期時間,這樣即使服務(wù)掛掉,到了過期時間之后,鎖會自動釋放。但是仔細想想,還是有問題。比如key值的過期時間為10s,但是業(yè)務(wù)處理邏輯需要15s的時間,這樣就會導(dǎo)致某一個線程處理完業(yè)務(wù)邏輯之后,在釋放鎖,即刪除key的時候,刪除的key不是自己set的,而是其他線程設(shè)置的,這樣就會造成數(shù)據(jù)的不一致性,引起數(shù)據(jù)的錯誤,從而影響業(yè)務(wù)。還需要改進。
進階版本2-誰設(shè)置的鎖,誰釋放
/**
* 進階版本2-誰設(shè)置的鎖,誰釋放
* @author:liyajie
* @createTime:2022/6/22 15:42
* @version:1.0
*/
public class SimplyRedisLock3 {
// Redis分布式鎖的key
public static final String REDIS_LOCK = "redis_lock";
@Autowired
StringRedisTemplate template;
public String index(){
// 每個人進來先要進行加鎖,key值為"redis_lock",value隨機生成
String value = UUID.randomUUID().toString().replace("-","");
try{
// 加鎖
Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L, TimeUnit.SECONDS);
// 加鎖失敗
if(!flag){
return "搶鎖失??!";
}
System.out.println( value+ " 搶鎖成功");
// 業(yè)務(wù)邏輯
String result = template.opsForValue().get("001");
int total = result == null ? 0 : Integer.parseInt(result);
if (total > 0) {
int realTotal = total - 1;
template.opsForValue().set("001", String.valueOf(realTotal));
// 如果在搶到所之后,刪除鎖之前,發(fā)生了異常,鎖就無法被釋放,
// 釋放鎖操作不能在此操作,要在finally處理
// template.delete(REDIS_LOCK);
System.out.println("購買商品成功,庫存還剩:" + realTotal + "件");
return "購買商品成功,庫存還剩:" + realTotal + "件";
} else {
System.out.println("購買商品失敗");
}
return "購買商品失敗";
}finally {
// 誰加的鎖,誰才能刪除!?。。?
if(template.opsForValue().get(REDIS_LOCK).equals(value)){
template.delete(REDIS_LOCK);
}
}
}
}
這種方式解決了因業(yè)務(wù)復(fù)雜,處理時間太長,超過了過期時間,而釋放了別人鎖的問題。還會有其他問題嗎?其實還是有的,finally塊的判斷和del刪除操作不是原子操作,并發(fā)的時候也會出問題,并發(fā)就是要保證數(shù)據(jù)的一致性,保證數(shù)據(jù)的一致性,最好要保證對數(shù)據(jù)的操作具有原子性。于是還是要改進。
進階版本3-Lua版本
/**
* 進階版本-Lua版本
* @author:liyajie
* @createTime:2022/6/22 15:42
* @version:1.0
*/
public class SimplyRedisLock3 {
// Redis分布式鎖的key
public static final String REDIS_LOCK = "redis_lock";
@Autowired
StringRedisTemplate template;
public String index(){
// 每個人進來先要進行加鎖,key值為"redis_lock",value隨機生成
String value = UUID.randomUUID().toString().replace("-","");
try{
// 加鎖
Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L, TimeUnit.SECONDS);
// 加鎖失敗
if(!flag){
return "搶鎖失?。?;
}
System.out.println( value+ " 搶鎖成功");
// 業(yè)務(wù)邏輯
String result = template.opsForValue().get("001");
int total = result == null ? 0 : Integer.parseInt(result);
if (total > 0) {
int realTotal = total - 1;
template.opsForValue().set("001", String.valueOf(realTotal));
// 如果在搶到所之后,刪除鎖之前,發(fā)生了異常,鎖就無法被釋放,
// 釋放鎖操作不能在此操作,要在finally處理
// template.delete(REDIS_LOCK);
System.out.println("購買商品成功,庫存還剩:" + realTotal + "件");
return "購買商品成功,庫存還剩:" + realTotal + "件";
} else {
System.out.println("購買商品失敗");
}
return "購買商品失敗";
}finally {
// 誰加的鎖,誰才能刪除,使用Lua腳本,進行鎖的刪除
Jedis jedis = null;
try{
jedis = RedisUtils.getJedis();
String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +
"then " +
"return redis.call('del',KEYS[1]) " +
"else " +
" return 0 " +
"end";
Object eval = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value));
if("1".equals(eval.toString())){
System.out.println("-----del redis lock ok....");
}else{
System.out.println("-----del redis lock error ....");
}
}catch (Exception e){
}finally {
if(null != jedis){
jedis.close();
}
}
}
}
}
這種方式,規(guī)定了誰上的鎖,誰才能刪除,并且解決了刪除操作沒有原子性問題。但還沒有考慮緩存,以及Redis集群部署下,異步復(fù)制造成的鎖丟失:主節(jié)點沒來得及把剛剛set進來這條數(shù)據(jù)給從節(jié)點,就掛了。所以還得改進。
終極進化版
/**
* 終極進化版
* @author:liyajie
* @createTime:2022/6/22 15:42
* @version:1.0
*/
public class SimplyRedisLock5 {
// Redis分布式鎖的key
public static final String REDIS_LOCK = "redis_lock";
@Autowired
StringRedisTemplate template;
@Autowired
Redisson redisson;
public String index(){
RLock lock = redisson.getLock(REDIS_LOCK);
lock.lock();
// 每個人進來先要進行加鎖,key值為"redis_lock"
String value = UUID.randomUUID().toString().replace("-","");
try {
String result = template.opsForValue().get("001");
int total = result == null ? 0 : Integer.parseInt(result);
if (total > 0) {
// 如果在此處需要調(diào)用其他微服務(wù),處理時間較長。。。
int realTotal = total - 1;
template.opsForValue().set("001", String.valueOf(realTotal));
System.out.println("購買商品成功,庫存還剩:" + realTotal + "件");
return "購買商品成功,庫存還剩:" + realTotal + "件";
} else {
System.out.println("購買商品失敗");
}
return "購買商品失敗";
}finally {
if(lock.isLocked() && lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
}
這種實現(xiàn)方案,底層封裝了多節(jié)點redis實現(xiàn)的分布式鎖算法,有效防止單點故障,感興趣的可以去研究一下。
總結(jié)
分析問題的過程,也是解決問題的過程,也能鍛煉自己編寫代碼時思考問題的方式和角度。
到此這篇關(guān)于詳解Redis分布式鎖的原理與實現(xiàn)的文章就介紹到這了,更多相關(guān)Redis分布式鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis事務(wù)機制與Springboot項目中的使用方式
Redis事務(wù)機制允許將多個命令打包在一起,作為一個原子操作來執(zhí)行,開啟事務(wù)使用MULTI命令,執(zhí)行事務(wù)使用EXEC命令,取消事務(wù)使用DISCARD命令,監(jiān)視一個或多個鍵使用WATCH命令,Redis事務(wù)的核心思想是將多個命令放入一個隊列中2025-03-03
Redis如何實現(xiàn)計數(shù)統(tǒng)計
這篇文章主要介紹了Redis如何實現(xiàn)計數(shù)統(tǒng)計方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-04-04
基于Redis實現(xiàn)分布式鎖的方法(lua腳本版)
這篇文章主要介紹了基于Redis實現(xiàn)分布式鎖的方法(lua腳本版),本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-05-05
kubernetes環(huán)境部署單節(jié)點redis數(shù)據(jù)庫的方法
這篇文章主要介紹了kubernetes環(huán)境部署單節(jié)點redis數(shù)據(jù)庫的方法,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-01-01

