掌握Redis分布式鎖的正確姿勢(shì)
本文中案例都會(huì)在上傳到git上,請(qǐng)放心瀏覽 git地址:https://github.com/muxiaonong/Spring-Cloud/tree/master/order-lock 本文會(huì)使用到 三臺(tái) redis 獨(dú)立服務(wù)器,可以自行提前搭建好
前言
在Java中,我們對(duì)于鎖會(huì)比較熟悉,常用的有 synchronized、Lock
鎖,在java并發(fā)編程中,我們通過(guò)鎖,來(lái)實(shí)現(xiàn)當(dāng)多個(gè)線程競(jìng)爭(zhēng)同一個(gè)共享資源或者變量而造成的數(shù)據(jù)不一致的問(wèn)題,但是JVM鎖只能針對(duì)于單個(gè)應(yīng)用服務(wù),隨著我們業(yè)務(wù)的發(fā)展需要,單體單機(jī)部署的系統(tǒng)早已演化成分布式系統(tǒng),由于分布式系統(tǒng)的多線程、多進(jìn)程而且分布在不同的機(jī)器上,這個(gè)時(shí)候JVM鎖的并發(fā)控制就沒(méi)有效果了,為了解決跨JVM鎖并且能夠控制共享資源的訪問(wèn),于是有了分布式鎖的誕生。
什么是分布式鎖
分布式鎖是控制分布式系統(tǒng)之間同步訪問(wèn)共享資源的一種方式。在分布式系統(tǒng)中,常常需要協(xié)調(diào)他們的動(dòng)作。如果不同的系統(tǒng)或是同一個(gè)系統(tǒng)的不同主機(jī)之間共享了一個(gè)或一組資源,那么訪問(wèn)這些資源的時(shí)候,往往需要互斥來(lái)防止彼此干擾來(lái)保證一致性,在這種情況下,便需要使用到分布式鎖
為什么JVM鎖在分布式下不可以呢?
我們通過(guò)代碼來(lái)看一下就知道,為什么集群下jvm鎖是不可靠的呢?我們模擬一下商品搶購(gòu)的場(chǎng)景,A服務(wù)有十個(gè)用戶去搶購(gòu)這個(gè)商品,B服務(wù)有十個(gè)用戶去搶購(gòu)這個(gè)商品,當(dāng)有其中一個(gè)用戶搶購(gòu)成功后,其他用戶不可以在對(duì)這個(gè)商品進(jìn)行下單操作,那么到底是A服務(wù)會(huì)搶到還是B服務(wù)會(huì)搶到這個(gè)商品呢,我們來(lái)看一下
當(dāng)其中有一個(gè)用戶搶購(gòu)成功后,status會(huì)變成1
GrabService:
public interface GrabService {
/**
* 商品搶單
* @param orderId
* @param driverId
* @return
*/
public ResponseResult grabOrder(int orderId, int driverId);
}
GrabJvmLockServiceImpl:
@Service("grabJvmLockService")
public class GrabJvmLockServiceImpl implements GrabService {
@Autowired
OrderService orderService;
@Override
public ResponseResult grabOrder(int orderId, int driverId) {
String lock = (orderId+"");
synchronized (lock.intern()) {
try {
System.out.println("用戶:"+driverId+" 執(zhí)行下單邏輯");
boolean b = orderService.grab(orderId, driverId);
if(b) {
System.out.println("用戶:"+driverId+" 下單成功");
}else {
System.out.println("用戶:"+driverId+" 下單失敗");
}
} finally {
}
}
return null;
}
}
OrderService :
public interface OrderService {
public boolean grab(int orderId, int driverId);
}
OrderServiceImpl :
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper mapper;
public boolean grab(int orderId, int driverId) {
Order order = mapper.selectByPrimaryKey(orderId);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(order.getStatus().intValue() == 0) {
order.setStatus(1);
mapper.updateByPrimaryKeySelective(order);
return true;
}
return false;
}
}
這里我們模擬集群環(huán)境,啟動(dòng)兩個(gè)端口,8004和8005進(jìn)行訪問(wèn) 這里我們用jmeter進(jìn)行測(cè)試 如果不會(huì)jmeter的可以看我之前對(duì)tomcat進(jìn)行壓測(cè)的文章:tomcat優(yōu)化
項(xiàng)目啟動(dòng)順序:先啟動(dòng) Server-eureka注冊(cè)中心、在啟動(dòng) 8004和8005端口
測(cè)試結(jié)果:
這里我們可以看到 8004 服務(wù)和 8005 服務(wù) 同時(shí)都有一個(gè)用戶去下單成功這個(gè)商品,但是這個(gè)商品只能有一個(gè)用戶能夠去搶到,因此jvm鎖如果是在集群或分布式下,是無(wú)法保證訪問(wèn)共享變量的數(shù)據(jù)同時(shí)只有一個(gè)線程訪問(wèn)的,無(wú)法解決分布式,集群環(huán)境的問(wèn)題。所以需要使用到分布鎖。
分布式鎖三種實(shí)現(xiàn)方式
分布式鎖的實(shí)現(xiàn)方式總共有三種:
基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖
基于緩存(Redis)實(shí)現(xiàn)分布式鎖
基于Zookeeper實(shí)現(xiàn)分布式鎖
今天,我們主要講的是基于Redis實(shí)現(xiàn)的分布式鎖
reids實(shí)現(xiàn)分布式鎖有三種方式
1、基于redis的 SETNX 實(shí)現(xiàn)分布式鎖 2、Redisson實(shí)現(xiàn)分布式鎖 4、使用redLock實(shí)現(xiàn)分布式鎖
目錄結(jié)構(gòu):
方式一:基于 SETNX 實(shí)現(xiàn)分布式鎖
將key的值設(shè)為value ,當(dāng)且僅當(dāng)key不存在。若給定的key已經(jīng)存在,則SETNX不做任何動(dòng)作。setnx:當(dāng)key存在,不做任何操作,key不存在,才設(shè)置
加鎖:
SET orderId driverId NX PX 30000 上面的命令如果執(zhí)行成功,則客戶端成功獲取到了鎖,接下來(lái)就可以訪問(wèn)共享資源了;而如果上面的命令執(zhí)行失敗,則說(shuō)明獲取鎖失敗。
釋放鎖:關(guān)鍵,判斷是不是自己加的鎖。
GrabService :
public interface GrabService {
/**
* 商品搶單
* @param orderId
* @param driverId
* @return
public ResponseResult grabOrder(int orderId, int driverId);
}
GrabRedisLockServiceImpl :
@Service("grabRedisLockService")
public class GrabRedisLockServiceImpl implements GrabService {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
OrderService orderService;
@Override
public ResponseResult grabOrder(int orderId , int driverId){
//生成key
String lock = "order_"+(orderId+"");
/*
* 情況一,如果鎖沒(méi)執(zhí)行到釋放,比如業(yè)務(wù)邏輯執(zhí)行一半,運(yùn)維重啟服務(wù),或 服務(wù)器掛了,沒(méi)走 finally,怎么辦?
* 加超時(shí)時(shí)間
*/
// boolean lockStatus = stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(), driverId+"");
// if(!lockStatus) {
// return null;
// }
/*
* 情況二:加超時(shí)時(shí)間,會(huì)有加不上的情況,運(yùn)維重啟
*/
// boolean lockStatus = stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(), driverId+"");
// stringRedisTemplate.expire(lock.intern(), 30L, TimeUnit.SECONDS);
// if(!lockStatus) {
// return null;
// }
/*
* 情況三:超時(shí)時(shí)間應(yīng)該一次加,不應(yīng)該分2行代碼,
*
*/
boolean lockStatus = stringRedisTemplate.opsForValue().setIfAbsent(lock.intern(), driverId+"", 30L, TimeUnit.SECONDS);
if(!lockStatus) {
return null;
}
try {
System.out.println("用戶:"+driverId+" 執(zhí)行搶單邏輯");
boolean b = orderService.grab(orderId, driverId);
if(b) {
System.out.println("用戶:"+driverId+" 搶單成功");
}else {
System.out.println("用戶:"+driverId+" 搶單失敗");
}
} finally {
/**
* 這種釋放鎖有,可能釋放了別人的鎖。
*/
// stringRedisTemplate.delete(lock.intern());
/**
* 下面代碼避免釋放別人的鎖
*/
if((driverId+"").equals(stringRedisTemplate.opsForValue().get(lock.intern()))) {
stringRedisTemplate.delete(lock.intern());
}
}
return null;
}
}
這里可能會(huì)有人問(wèn),如果我業(yè)務(wù)的執(zhí)行時(shí)間超過(guò)了鎖釋放的時(shí)間,會(huì)怎么辦呢?我們可以使用守護(hù)線程,只要我們當(dāng)前線程還持有這個(gè)鎖,到了10S的時(shí)候,守護(hù)線程會(huì)自動(dòng)對(duì)該線程進(jìn)行加時(shí)操作,會(huì)續(xù)上30S的過(guò)期時(shí)間,直到把鎖釋放,就不會(huì)在進(jìn)行續(xù)約了,開啟一個(gè)子線程,原來(lái)時(shí)間是N,每隔N/3,在去續(xù)上N
關(guān)注點(diǎn):
key,是我們的要鎖的目標(biāo),比如訂單ID。
driverId 是由我們的商品ID,它要保證在足夠長(zhǎng)的一段時(shí)間內(nèi)在所有客戶端的所有獲取鎖的請(qǐng)求中都是唯一的。即一個(gè)訂單被一個(gè)用戶搶。
NX表示只有當(dāng)orderId不存在的時(shí)候才能SET成功。這保證了只有第一個(gè)請(qǐng)求的客戶端才能獲得鎖,而其它客戶端在鎖被釋放之前都無(wú)法獲得鎖。
PX 30000表示這個(gè)鎖有一個(gè)30秒的自動(dòng)過(guò)期時(shí)間。當(dāng)然,這里30秒只是一個(gè)例子,客戶端可以選擇合適的過(guò)期時(shí)間。
這個(gè)鎖必須要設(shè)置一個(gè)過(guò)期時(shí)間。 否則的話,當(dāng)一個(gè)客戶端獲取鎖成功之后,假如它崩潰了,或者由于發(fā)生了網(wǎng)絡(luò)分區(qū),導(dǎo)致它再也無(wú)法和Redis節(jié)點(diǎn)通信了,那么它就會(huì)一直持有這個(gè)鎖,而其它客戶端永遠(yuǎn)無(wú)法獲得鎖了。antirez在后面的分析中也特別強(qiáng)調(diào)了這一點(diǎn),而且把這個(gè)過(guò)期時(shí)間稱為鎖的有效時(shí)間(lock validity time)。獲得鎖的客戶端必須在這個(gè)時(shí)間之內(nèi)完成對(duì)共享資源的訪問(wèn)。
此操作不能分割。>SETNX orderId driverId EXPIRE orderId 30 雖然這兩個(gè)命令和前面算法描述中的一個(gè)SET命令執(zhí)行效果相同,但卻不是原子的。如果客戶端在執(zhí)行完SETNX后崩潰了,那么就沒(méi)有機(jī)會(huì)執(zhí)行EXPIRE了,導(dǎo)致它一直持有這個(gè)鎖。造成死鎖。
方式二:基于redisson實(shí)現(xiàn)分布式鎖
流程圖:代碼實(shí)現(xiàn):
@Service("grabRedisRedissonService")
public class GrabRedisRedissonServiceImpl implements GrabService {
@Autowired
RedissonClient redissonClient;
@Autowired
OrderService orderService;
@Override
public ResponseResult grabOrder(int orderId , int driverId){
//生成key
String lock = "order_"+(orderId+"");
RLock rlock = redissonClient.getLock(lock.intern());
try {
// 此代碼默認(rèn) 設(shè)置key 超時(shí)時(shí)間30秒,過(guò)10秒,再延時(shí)
rlock.lock();
System.out.println("用戶:"+driverId+" 執(zhí)行搶單邏輯");
boolean b = orderService.grab(orderId, driverId);
if(b) {
System.out.println("用戶:"+driverId+" 搶單成功");
}else {
System.out.println("用戶:"+driverId+" 搶單失敗");
}
} finally {
rlock.unlock();
}
return null;
}
}
關(guān)注點(diǎn):
redis故障問(wèn)題。如果redis故障了,所有客戶端無(wú)法獲取鎖,服務(wù)變得不可用。為了提高可用性。我們給redis 配置主從。當(dāng)master不可用時(shí),系統(tǒng)切換到slave,由于Redis的主從復(fù)制(replication)是異步的,這可能導(dǎo)致喪失鎖的安全性
1.客戶端1從Master獲取了鎖。2.Master宕機(jī)了,存儲(chǔ)鎖的key還沒(méi)有來(lái)得及同步到Slave上。3.Slave升級(jí)為Master。4.客戶端2從新的Master獲取到了對(duì)應(yīng)同一個(gè)資源的鎖。
客戶端1和客戶端2同時(shí)持有了同一個(gè)資源的鎖。鎖的安全性被打破。
鎖的有效時(shí)間(lock validity time),設(shè)置成多少合適?如果設(shè)置太短的話,鎖就有可能在客戶端完成對(duì)于共享資源的訪問(wèn)之前過(guò)期,從而失去保護(hù);如果設(shè)置太長(zhǎng)的話,一旦某個(gè)持有鎖的客戶端釋放鎖失敗,那么就會(huì)導(dǎo)致所有其它客戶端都無(wú)法獲取鎖,從而長(zhǎng)時(shí)間內(nèi)無(wú)法正常工作。應(yīng)該設(shè)置稍微短一些,如果線程持有鎖,開啟線程自動(dòng)延長(zhǎng)有效期
方式三:基于RedLock實(shí)現(xiàn)分布式鎖
針對(duì)于以上兩點(diǎn),antirez設(shè)計(jì)了Redlock算法 Redis的作者antirez給出了一個(gè)更好的實(shí)現(xiàn),稱為Redlock,算是Redis官方對(duì)于實(shí)現(xiàn)分布式鎖的指導(dǎo)規(guī)范。Redlock的算法描述就放在Redis的官網(wǎng)上:https://redis.io/topics/distlock
目的:對(duì)共享資源做互斥訪問(wèn)
因此antirez提出了新的分布式鎖的算法Redlock,它基于N個(gè)完全獨(dú)立的Redis節(jié)點(diǎn)(通常情況下N可以設(shè)置成5),意思就是N個(gè)Redis數(shù)據(jù)不互通,類似于幾個(gè)陌生人
代碼實(shí)現(xiàn):
@Service("grabRedisRedissonRedLockLockService")
public class GrabRedisRedissonRedLockLockServiceImpl implements GrabService {
@Autowired
private RedissonClient redissonRed1;
@Autowired
private RedissonClient redissonRed2;
@Autowired
private RedissonClient redissonRed3;
@Autowired
OrderService orderService;
@Override
public ResponseResult grabOrder(int orderId , int driverId){
//生成key
String lockKey = (RedisKeyConstant.GRAB_LOCK_ORDER_KEY_PRE + orderId).intern();
//紅鎖
RLock rLock1 = redissonRed1.getLock(lockKey);
RLock rLock2 = redissonRed2.getLock(lockKey);
RLock rLock3 = redissonRed2.getLock(lockKey);
RedissonRedLock rLock = new RedissonRedLock(rLock1,rLock2,rLock3);
try {
rLock.lock();
// 此代碼默認(rèn) 設(shè)置key 超時(shí)時(shí)間30秒,過(guò)10秒,再延時(shí)
System.out.println("用戶:"+driverId+" 執(zhí)行搶單邏輯");
boolean b = orderService.grab(orderId, driverId);
if(b) {
System.out.println("用戶:"+driverId+" 搶單成功");
}else {
System.out.println("用戶:"+driverId+" 搶單失敗");
}
} finally {
rLock.unlock();
}
return null;
}
}
運(yùn)行Redlock算法的客戶端依次執(zhí)行下面各個(gè)步驟,來(lái)完成 獲取鎖 的操作:
獲取當(dāng)前時(shí)間(毫秒數(shù))。
按順序依次向N個(gè)Redis節(jié)點(diǎn)執(zhí)行 獲取鎖 的操作。這個(gè)獲取操作跟前面基于單Redis節(jié)點(diǎn)的 獲取鎖 的過(guò)程相同,包含value driverId ,也包含過(guò)期時(shí)間(比如
PX30000
,即鎖的有效時(shí)間)。為了保證在某個(gè)Redis節(jié)點(diǎn)不可用的時(shí)候算法能夠繼續(xù)運(yùn)行,這個(gè) 獲取鎖 的操作還有一個(gè)超時(shí)時(shí)間(time out),它要遠(yuǎn)小于鎖的有效時(shí)間(幾十毫秒量級(jí))。客戶端在向某個(gè)Redis節(jié)點(diǎn)獲取鎖失敗以后,應(yīng)該立即嘗試下一個(gè)Redis節(jié)點(diǎn)。這里的失敗,應(yīng)該包含任何類型的失敗,比如該Redis節(jié)點(diǎn)不可用,或者該Redis節(jié)點(diǎn)上的鎖已經(jīng)被其它客戶端持有
計(jì)算整個(gè)獲取鎖的過(guò)程總共消耗了多長(zhǎng)時(shí)間,計(jì)算方法是用當(dāng)前時(shí)間減去第1步記錄的時(shí)間。如果客戶端從大多數(shù)Redis節(jié)點(diǎn)(>= N/2+1)成功獲取到了鎖,比如:五臺(tái)機(jī)器如果加鎖成功三臺(tái)就默認(rèn)加鎖成功,并且獲取鎖總共消耗的時(shí)間沒(méi)有超過(guò)鎖的有效時(shí)間(lock validity time),那么這時(shí)客戶端才認(rèn)為最終獲取鎖成功;否則,認(rèn)為最終獲取鎖失敗
如果最終獲取鎖成功了,那么這個(gè)鎖的有效時(shí)間應(yīng)該重新計(jì)算,它等于最初的鎖的有效時(shí)間減去第3步計(jì)算出來(lái)的獲取鎖消耗的時(shí)間。
如果最終獲取鎖失敗了(可能由于獲取到鎖的Redis節(jié)點(diǎn)個(gè)數(shù)少于N/2+1,或者整個(gè)獲取鎖的過(guò)程消耗的時(shí)間超過(guò)了鎖的最初有效時(shí)間),那么客戶端應(yīng)該立即向所有Redis節(jié)點(diǎn)發(fā)起 釋放鎖 的操作(即前面介紹的Redis Lua腳本)。
上面描述的只是 獲取鎖 的過(guò)程,而 釋放鎖 的過(guò)程比較簡(jiǎn)單:客戶端向所有Redis節(jié)點(diǎn)發(fā)起 釋放鎖 的操作,不管這些節(jié)點(diǎn)當(dāng)時(shí)在獲取鎖的時(shí)候成功與否。
總結(jié)
到這里redis分布式鎖就講完了,具體使用哪一種類型的分布式鎖需要看公司業(yè)務(wù)的,流量大的可以使用RedLock實(shí)現(xiàn)分布式鎖,流量小的可以使用redisson,后面會(huì)講解Zookeeper實(shí)現(xiàn)分布式鎖,喜歡的小伙伴可以關(guān)注我,對(duì)本文內(nèi)容有疑問(wèn)或者問(wèn)題的同學(xué)可以留言,小農(nóng)看到了會(huì)第一時(shí)間回復(fù),謝謝大家,大家加油!
特別推薦一個(gè)分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒(méi)關(guān)注的小伙伴,可以長(zhǎng)按關(guān)注一下:
長(zhǎng)按訂閱更多精彩▼
如有收獲,點(diǎn)個(gè)在看,誠(chéng)摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!