Redis實(shí)現(xiàn)分布式鎖方法詳細(xì)
在單體應(yīng)用中,如果我們對(duì)共享數(shù)據(jù)不進(jìn)行加鎖操作,會(huì)出現(xiàn)數(shù)據(jù)一致性問(wèn)題,我們的解決辦法通常是加鎖。
在分布式架構(gòu)中,我們同樣會(huì)遇到數(shù)據(jù)共享操作問(wèn)題,本文章使用Redis來(lái)解決分布式架構(gòu)中的數(shù)據(jù)一致性問(wèn)題。
1. 單機(jī)數(shù)據(jù)一致性
單機(jī)數(shù)據(jù)一致性架構(gòu)如下圖所示:多個(gè)可客戶訪問(wèn)同一個(gè)服務(wù)器,連接同一個(gè)數(shù)據(jù)庫(kù)。

場(chǎng)景描述:客戶端模擬購(gòu)買商品過(guò)程,在Redis中設(shè)定庫(kù)存總數(shù)剩100個(gè),多個(gè)客戶端同時(shí)并發(fā)購(gòu)買。

@RestController
public class IndexController1 {
@Autowired
StringRedisTemplate template;
@RequestMapping("/buy1")
public String index(){
// Redis中存有g(shù)oods:001號(hào)商品,數(shù)量為100
String result = template.opsForValue().get("goods:001");
// 獲取到剩余商品數(shù)
int total = result == null ? 0 : Integer.parseInt(result);
if( total > 0 ){
// 剩余商品數(shù)大于0 ,則進(jìn)行扣減
int realTotal = total -1;
// 將商品數(shù)回寫(xiě)數(shù)據(jù)庫(kù)
template.opsForValue().set("goods:001",String.valueOf(realTotal));
System.out.println("購(gòu)買商品成功,庫(kù)存還剩:"+realTotal +"件, 服務(wù)端口為8001");
return "購(gòu)買商品成功,庫(kù)存還剩:"+realTotal +"件, 服務(wù)端口為8001";
}else{
System.out.println("購(gòu)買商品失敗,服務(wù)端口為8001");
}
return "購(gòu)買商品失敗,服務(wù)端口為8001";
}
}
使用Jmeter模擬高并發(fā)場(chǎng)景,測(cè)試結(jié)果如下:

測(cè)試結(jié)果出現(xiàn)多個(gè)用戶購(gòu)買同一商品,發(fā)生了數(shù)據(jù)不一致問(wèn)題!
解決辦法:?jiǎn)误w應(yīng)用的情況下,對(duì)并發(fā)的操作進(jìn)行加鎖操作,保證對(duì)數(shù)據(jù)的操作具有原子性
- synchronized
- ReentrantLock
@RestController
public class IndexController2 {
// 使用ReentrantLock鎖解決單體應(yīng)用的并發(fā)問(wèn)題
Lock lock = new ReentrantLock();
@Autowired
StringRedisTemplate template;
@RequestMapping("/buy2")
public String index() {
lock.lock();
try {
String result = template.opsForValue().get("goods:001");
int total = result == null ? 0 : Integer.parseInt(result);
if (total > 0) {
int realTotal = total - 1;
template.opsForValue().set("goods:001", String.valueOf(realTotal));
System.out.println("購(gòu)買商品成功,庫(kù)存還剩:" + realTotal + "件, 服務(wù)端口為8001");
return "購(gòu)買商品成功,庫(kù)存還剩:" + realTotal + "件, 服務(wù)端口為8001";
} else {
System.out.println("購(gòu)買商品失敗,服務(wù)端口為8001");
}
} catch (Exception e) {
lock.unlock();
} finally {
lock.unlock();
}
return "購(gòu)買商品失敗,服務(wù)端口為8001";
}
}

2. 分布式數(shù)據(jù)一致性
上面解決了單體應(yīng)用的數(shù)據(jù)一致性問(wèn)題,但如果是分布式架構(gòu)部署呢,架構(gòu)如下:
提供兩個(gè)服務(wù),端口分別為8001、8002,連接同一個(gè)Redis服務(wù),在服務(wù)前面有一臺(tái)Nginx作為負(fù)載均衡

兩臺(tái)服務(wù)代碼相同,只是端口不同
將8001、8002兩個(gè)服務(wù)啟動(dòng),每個(gè)服務(wù)依然用ReentrantLock加鎖,用Jmeter做并發(fā)測(cè)試,發(fā)現(xiàn)會(huì)出現(xiàn)數(shù)據(jù)一致性問(wèn)題!

3. Redis實(shí)現(xiàn)分布式鎖
3.1 方式一
取消單機(jī)鎖,下面使用redis的set命令來(lái)實(shí)現(xiàn)分布式加鎖
SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]
EX seconds ? 設(shè)置指定的到期時(shí)間(以秒為單位)
PX milliseconds ? 設(shè)置指定的到期時(shí)間(以毫秒為單位)
NX ? 僅在鍵不存在時(shí)設(shè)置鍵
XX ? 只有在鍵已存在時(shí)才設(shè)置
@RestController
public class IndexController4 {
// Redis分布式鎖的key
public static final String REDIS_LOCK = "good_lock";
@Autowired
StringRedisTemplate template;
@RequestMapping("/buy4")
public String index(){
// 每個(gè)人進(jìn)來(lái)先要進(jìn)行加鎖,key值為"good_lock",value隨機(jī)生成
String value = UUID.randomUUID().toString().replace("-","");
try{
// 加鎖
Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value);
// 加鎖失敗
if(!flag){
return "搶鎖失?。?;
}
System.out.println( value+ " 搶鎖成功");
String result = template.opsForValue().get("goods:001");
int total = result == null ? 0 : Integer.parseInt(result);
if (total > 0) {
int realTotal = total - 1;
template.opsForValue().set("goods:001", String.valueOf(realTotal));
// 如果在搶到所之后,刪除鎖之前,發(fā)生了異常,鎖就無(wú)法被釋放,
// 釋放鎖操作不能在此操作,要在finally處理
// template.delete(REDIS_LOCK);
System.out.println("購(gòu)買商品成功,庫(kù)存還剩:" + realTotal + "件, 服務(wù)端口為8001");
return "購(gòu)買商品成功,庫(kù)存還剩:" + realTotal + "件, 服務(wù)端口為8001";
} else {
System.out.println("購(gòu)買商品失敗,服務(wù)端口為8001");
}
return "購(gòu)買商品失敗,服務(wù)端口為8001";
}finally {
// 釋放鎖
template.delete(REDIS_LOCK);
}
}
}
上面的代碼,可以解決分布式架構(gòu)中數(shù)據(jù)一致性問(wèn)題。但再仔細(xì)想想,還是會(huì)有問(wèn)題,下面進(jìn)行改進(jìn)。
3.2 方式二(改進(jìn)方式一)
在上面的代碼中,如果程序在運(yùn)行期間,部署了微服務(wù)jar包的機(jī)器突然掛了,代碼層面根本就沒(méi)有走到finally代碼塊,也就是說(shuō)在宕機(jī)前,鎖并沒(méi)有被刪除掉,這樣的話,就沒(méi)辦法保證解鎖
所以,這里需要對(duì)這個(gè)key加一個(gè)過(guò)期時(shí)間,Redis中設(shè)置過(guò)期時(shí)間有兩種方法:
- template.expire(REDIS_LOCK,10, TimeUnit.SECONDS)
- template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS)
第一種方法需要單獨(dú)的一行代碼,且并沒(méi)有與加鎖放在同一步操作,所以不具備原子性,也會(huì)出問(wèn)題
第二種方法在加鎖的同時(shí)就進(jìn)行了設(shè)置過(guò)期時(shí)間,所有沒(méi)有問(wèn)題,這里采用這種方式
調(diào)整下代碼,在加鎖的同時(shí),設(shè)置過(guò)期時(shí)間:
// 為key加一個(gè)過(guò)期時(shí)間,其余代碼不變 Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK,value,10L,TimeUnit.SECONDS);
這種方式解決了因服務(wù)突然宕機(jī)而無(wú)法釋放鎖的問(wèn)題。但再仔細(xì)想想,還是會(huì)有問(wèn)題,下面進(jìn)行改進(jìn)。
3.3 方式三(改進(jìn)方式二)
方式二設(shè)置了key的過(guò)期時(shí)間,解決了key無(wú)法刪除的問(wèn)題,但問(wèn)題又來(lái)了
上面設(shè)置了key的過(guò)期時(shí)間為10秒,如果業(yè)務(wù)邏輯比較復(fù)雜,需要調(diào)用其他微服務(wù),處理時(shí)間需要15秒(模擬場(chǎng)景,別較真),而當(dāng)10秒鐘過(guò)去之后,這個(gè)key就過(guò)期了,其他請(qǐng)求就又可以設(shè)置這個(gè)key,此時(shí)如果耗時(shí)15秒的請(qǐng)求處理完了,回來(lái)繼續(xù)執(zhí)行程序,就會(huì)把別人設(shè)置的key給刪除了,這是個(gè)很嚴(yán)重的問(wèn)題!
所以,誰(shuí)上的鎖,誰(shuí)才能刪除
@RestController
public class IndexController6 {
public static final String REDIS_LOCK = "good_lock";
@Autowired
StringRedisTemplate template;
@RequestMapping("/buy6")
public String index(){
// 每個(gè)人進(jìn)來(lái)先要進(jìn)行加鎖,key值為"good_lock"
String value = UUID.randomUUID().toString().replace("-","");
try{
// 為key加一個(gè)過(guò)期時(shí)間
Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS);
// 加鎖失敗
if(!flag){
return "搶鎖失??!";
}
System.out.println( value+ " 搶鎖成功");
String result = template.opsForValue().get("goods:001");
int total = result == null ? 0 : Integer.parseInt(result);
if (total > 0) {
// 如果在此處需要調(diào)用其他微服務(wù),處理時(shí)間較長(zhǎng)。。。
int realTotal = total - 1;
template.opsForValue().set("goods:001", String.valueOf(realTotal));
System.out.println("購(gòu)買商品成功,庫(kù)存還剩:" + realTotal + "件, 服務(wù)端口為8001");
return "購(gòu)買商品成功,庫(kù)存還剩:" + realTotal + "件, 服務(wù)端口為8001";
} else {
System.out.println("購(gòu)買商品失敗,服務(wù)端口為8001");
}
return "購(gòu)買商品失敗,服務(wù)端口為8001";
}finally {
// 誰(shuí)加的鎖,誰(shuí)才能刪除?。。?!
if(template.opsForValue().get(REDIS_LOCK).equals(value)){
template.delete(REDIS_LOCK);
}
}
}
}
這種方式解決了因服務(wù)處理時(shí)間太長(zhǎng)而釋放了別人鎖的問(wèn)題。這樣就沒(méi)問(wèn)題了嗎?
3.4 方式四(改進(jìn)方式三)
在上面方式三下,規(guī)定了誰(shuí)上的鎖,誰(shuí)才能刪除,但finally快的判斷和del刪除操作不是原子操作,并發(fā)的時(shí)候也會(huì)出問(wèn)題,并發(fā)嘛,就是要保證數(shù)據(jù)的一致性,保證數(shù)據(jù)的一致性,最好要保證對(duì)數(shù)據(jù)的操作具有原子性。
在Redis的set命令介紹中,最后推薦Lua腳本進(jìn)行鎖的刪除,地址:https://redis.io/commands/set
@RestController
public class IndexController7 {
public static final String REDIS_LOCK = "good_lock";
@Autowired
StringRedisTemplate template;
@RequestMapping("/buy7")
public String index(){
// 每個(gè)人進(jìn)來(lái)先要進(jìn)行加鎖,key值為"good_lock"
String value = UUID.randomUUID().toString().replace("-","");
try{
// 為key加一個(gè)過(guò)期時(shí)間
Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS);
// 加鎖失敗
if(!flag){
return "搶鎖失??!";
}
System.out.println( value+ " 搶鎖成功");
String result = template.opsForValue().get("goods:001");
int total = result == null ? 0 : Integer.parseInt(result);
if (total > 0) {
// 如果在此處需要調(diào)用其他微服務(wù),處理時(shí)間較長(zhǎng)。。。
int realTotal = total - 1;
template.opsForValue().set("goods:001", String.valueOf(realTotal));
System.out.println("購(gòu)買商品成功,庫(kù)存還剩:" + realTotal + "件, 服務(wù)端口為8001");
return "購(gòu)買商品成功,庫(kù)存還剩:" + realTotal + "件, 服務(wù)端口為8001";
} else {
System.out.println("購(gòu)買商品失敗,服務(wù)端口為8001");
}
return "購(gòu)買商品失敗,服務(wù)端口為8001";
}finally {
// 誰(shuí)加的鎖,誰(shuí)才能刪除,使用Lua腳本,進(jìn)行鎖的刪除
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();
}
}
}
}
}
3.5 方式五(改進(jìn)方式四)
在方式四下,規(guī)定了誰(shuí)上的鎖,誰(shuí)才能刪除,并且解決了刪除操作沒(méi)有原子性問(wèn)題。但還沒(méi)有考慮緩存續(xù)命,以及Redis集群部署下,異步復(fù)制造成的鎖丟失:主節(jié)點(diǎn)沒(méi)來(lái)得及把剛剛set進(jìn)來(lái)這條數(shù)據(jù)給從節(jié)點(diǎn),就掛了。所以直接上RedLock的Redisson落地實(shí)現(xiàn)。
@RestController
public class IndexController8 {
public static final String REDIS_LOCK = "good_lock";
@Autowired
StringRedisTemplate template;
@Autowired
Redisson redisson;
@RequestMapping("/buy8")
public String index(){
RLock lock = redisson.getLock(REDIS_LOCK);
lock.lock();
// 每個(gè)人進(jìn)來(lái)先要進(jìn)行加鎖,key值為"good_lock"
String value = UUID.randomUUID().toString().replace("-","");
try{
String result = template.opsForValue().get("goods:001");
int total = result == null ? 0 : Integer.parseInt(result);
if (total > 0) {
// 如果在此處需要調(diào)用其他微服務(wù),處理時(shí)間較長(zhǎng)。。。
int realTotal = total - 1;
template.opsForValue().set("goods:001", String.valueOf(realTotal));
System.out.println("購(gòu)買商品成功,庫(kù)存還剩:" + realTotal + "件, 服務(wù)端口為8001");
return "購(gòu)買商品成功,庫(kù)存還剩:" + realTotal + "件, 服務(wù)端口為8001";
} else {
System.out.println("購(gòu)買商品失敗,服務(wù)端口為8001");
}
return "購(gòu)買商品失敗,服務(wù)端口為8001";
}finally {
if(lock.isLocked() && lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
}
3.6 小結(jié)
分析問(wèn)題的過(guò)程,也是解決問(wèn)題的過(guò)程,也能鍛煉自己編寫(xiě)代碼時(shí)思考問(wèn)題的方式和角度。
上述測(cè)試代碼地址:
https://github.com/Hofanking/springboot-redis-example?
到此這篇關(guān)于Redis實(shí)現(xiàn)分布式鎖方法詳細(xì)的文章就介紹到這了,更多相關(guān)Redis分布式鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 基于Redis實(shí)現(xiàn)分布式鎖以及任務(wù)隊(duì)列
- Redis實(shí)現(xiàn)分布式鎖的幾種方法總結(jié)
- 淺談Redis分布式鎖的正確實(shí)現(xiàn)方式
- Redis分布式鎖的實(shí)現(xiàn)方式(redis面試題)
- Redis分布式鎖實(shí)現(xiàn)方式及超時(shí)問(wèn)題解決
- redis分布式鎖及會(huì)出現(xiàn)的問(wèn)題解決
- Redis分布式鎖的使用和實(shí)現(xiàn)原理詳解
- Redis分布式鎖如何實(shí)現(xiàn)續(xù)期
- Redis實(shí)現(xiàn)分布式鎖的五種方法詳解
- Redis分布式鎖及安全問(wèn)題解決
相關(guān)文章
如何監(jiān)聽(tīng)Redis中Key值的變化(SpringBoot整合)
測(cè)試過(guò)程中我們有一部分常量值放入redis,共大部分應(yīng)用調(diào)用,但在測(cè)試過(guò)程中經(jīng)常有人會(huì)清空redis,回歸測(cè)試,下面這篇文章主要給大家介紹了關(guān)于如何監(jiān)聽(tīng)Redis中Key值變化的相關(guān)資料,需要的朋友可以參考下2024-03-03
Redis之RedisTemplate配置方式(序列和反序列化)
這篇文章主要介紹了Redis之RedisTemplate配置方式(序列和反序列化),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
阿里云官方Redis開(kāi)發(fā)規(guī)范總結(jié)
本文主要介紹了阿里云官方Redis開(kāi)發(fā)規(guī)范總結(jié),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08
?Redis 串行生成順序編碼的方法實(shí)現(xiàn)
本文主要介紹了?Redis 串行生成順序編碼的方法實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04
詳解redis腳本命令執(zhí)行問(wèn)題(redis.call)
這篇文章主要介紹了redis腳本命令執(zhí)行問(wèn)題(redis.call),分別介紹了redis-cli命令行中執(zhí)行及l(fā)inux命令行中執(zhí)行問(wèn)題,本文給大家介紹的非常詳細(xì),需要的朋友參考下吧2022-03-03
redis數(shù)據(jù)類型_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了redis數(shù)據(jù)類型,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-08-08

