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

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

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

測試結(jié)果出現(xiàn)多個用戶購買同一商品,發(fā)生了數(shù)據(jù)不一致問題!
解決辦法:單體應用的情況下,對并發(fā)的操作進行加鎖操作,保證對數(shù)據(jù)的操作具有原子性
synchronizedReentrantLock
@RestController
public class IndexController2 {
// 使用ReentrantLock鎖解決單體應用的并發(fā)問題
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("購買商品成功,庫存還剩:" + realTotal + "件, 服務端口為8001");
return "購買商品成功,庫存還剩:" + realTotal + "件, 服務端口為8001";
} else {
System.out.println("購買商品失敗,服務端口為8001");
}
} catch (Exception e) {
lock.unlock();
} finally {
lock.unlock();
}
return "購買商品失敗,服務端口為8001";
}
}
2. 分布式數(shù)據(jù)一致性
上面解決了單體應用的數(shù)據(jù)一致性問題,但如果是分布式架構(gòu)部署呢,架構(gòu)如下:
提供兩個服務,端口分別為8001、8002,連接同一個Redis服務,在服務前面有一臺Nginx作為負載均衡

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

3. Redis實現(xiàn)分布式鎖
3.1 方式一
取消單機鎖,下面使用redis的set命令來實現(xiàn)分布式加鎖
SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]
- EX seconds 設置指定的到期時間(以秒為單位)
- PX milliseconds 設置指定的到期時間(以毫秒為單位)
- NX 僅在鍵不存在時設置鍵
- XX 只有在鍵已存在時才設置
@RestController
public class IndexController4 {
// Redis分布式鎖的key
public static final String REDIS_LOCK = "good_lock";
@Autowired
StringRedisTemplate template;
@RequestMapping("/buy4")
public String index(){
// 每個人進來先要進行加鎖,key值為"good_lock",value隨機生成
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ā)生了異常,鎖就無法被釋放,
// 釋放鎖操作不能在此操作,要在finally處理
// template.delete(REDIS_LOCK);
System.out.println("購買商品成功,庫存還剩:" + realTotal + "件, 服務端口為8001");
return "購買商品成功,庫存還剩:" + realTotal + "件, 服務端口為8001";
} else {
System.out.println("購買商品失敗,服務端口為8001");
}
return "購買商品失敗,服務端口為8001";
}finally {
// 釋放鎖
template.delete(REDIS_LOCK);
}
}
}
上面的代碼,可以解決分布式架構(gòu)中數(shù)據(jù)一致性問題。但再仔細想想,還是會有問題,下面進行改進。
3.2 方式二(改進方式一)
在上面的代碼中,如果程序在運行期間,部署了微服務jar包的機器突然掛了,代碼層面根本就沒有走到finally代碼塊,也就是說在宕機前,鎖并沒有被刪除掉,這樣的話,就沒辦法保證解鎖
所以,這里需要對這個key加一個過期時間,Redis中設置過期時間有兩種方法:
template.expire(REDIS_LOCK,10, TimeUnit.SECONDS)template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS)
第一種方法需要單獨的一行代碼,且并沒有與加鎖放在同一步操作,所以不具備原子性,也會出問題
第二種方法在加鎖的同時就進行了設置過期時間,所有沒有問題,這里采用這種方式
調(diào)整下代碼,在加鎖的同時,設置過期時間:
// 為key加一個過期時間,其余代碼不變 Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK,value,10L,TimeUnit.SECONDS);
這種方式解決了因服務突然宕機而無法釋放鎖的問題。但再仔細想想,還是會有問題,下面進行改進。
3.3 方式三(改進方式二)
方式二設置了key的過期時間,解決了key無法刪除的問題,但問題又來了
上面設置了key的過期時間為10秒,如果業(yè)務邏輯比較復雜,需要調(diào)用其他微服務,處理時間需要15秒(模擬場
景,別較真),而當10秒鐘過去之后,這個key就過期了,其他請求就又可以設置這個key,此時如果耗時15秒
的請求處理完了,回來繼續(xù)執(zhí)行程序,就會把別人設置的key給刪除了,這是個很嚴重的問題!
所以,誰上的鎖,誰才能刪除
@RestController
public class IndexController6 {
public static final String REDIS_LOCK = "good_lock";
@Autowired
StringRedisTemplate template;
@RequestMapping("/buy6")
public String index(){
// 每個人進來先要進行加鎖,key值為"good_lock"
String value = UUID.randomUUID().toString().replace("-","");
try{
// 為key加一個過期時間
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)用其他微服務,處理時間較長。。。
int realTotal = total - 1;
template.opsForValue().set("goods:001", String.valueOf(realTotal));
System.out.println("購買商品成功,庫存還剩:" + realTotal + "件, 服務端口為8001");
return "購買商品成功,庫存還剩:" + realTotal + "件, 服務端口為8001";
} else {
System.out.println("購買商品失敗,服務端口為8001");
}
return "購買商品失敗,服務端口為8001";
}finally {
// 誰加的鎖,誰才能刪除?。。?!
if(template.opsForValue().get(REDIS_LOCK).equals(value)){
template.delete(REDIS_LOCK);
}
}
}
}
這種方式解決了因服務處理時間太長而釋放了別人鎖的問題。這樣就沒問題了嗎?
3.4 方式四(改進方式三)
在上面方式三下,規(guī)定了誰上的鎖,誰才能刪除,但finally快的判斷和del刪除操作不是原子操作,并發(fā)的時候也會出問題,并發(fā)嘛,就是要保證數(shù)據(jù)的一致性,保證數(shù)據(jù)的一致性,最好要保證對數(shù)據(jù)的操作具有原子性。
在Redis的set命令介紹中,最后推薦Lua腳本進行鎖的刪除,地址
@RestController
public class IndexController7 {
public static final String REDIS_LOCK = "good_lock";
@Autowired
StringRedisTemplate template;
@RequestMapping("/buy7")
public String index(){
// 每個人進來先要進行加鎖,key值為"good_lock"
String value = UUID.randomUUID().toString().replace("-","");
try{
// 為key加一個過期時間
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)用其他微服務,處理時間較長。。。
int realTotal = total - 1;
template.opsForValue().set("goods:001", String.valueOf(realTotal));
System.out.println("購買商品成功,庫存還剩:" + realTotal + "件, 服務端口為8001");
return "購買商品成功,庫存還剩:" + realTotal + "件, 服務端口為8001";
} else {
System.out.println("購買商品失敗,服務端口為8001");
}
return "購買商品失敗,服務端口為8001";
}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();
}
}
}
}
}
3.5 方式五(改進方式四)
在方式四下,規(guī)定了誰上的鎖,誰才能刪除,并且解決了刪除操作沒有原子性問題。但還沒有考慮緩存續(xù)命,以及Redis集群部署下,異步復制造成的鎖丟失:主節(jié)點沒來得及把剛剛set進來這條數(shù)據(jù)給從節(jié)點,就掛了。所以直接上RedLock的Redisson落地實現(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();
// 每個人進來先要進行加鎖,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)用其他微服務,處理時間較長。。。
int realTotal = total - 1;
template.opsForValue().set("goods:001", String.valueOf(realTotal));
System.out.println("購買商品成功,庫存還剩:" + realTotal + "件, 服務端口為8001");
return "購買商品成功,庫存還剩:" + realTotal + "件, 服務端口為8001";
} else {
System.out.println("購買商品失敗,服務端口為8001");
}
return "購買商品失敗,服務端口為8001";
}finally {
if(lock.isLocked() && lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
}
3.6 小結(jié)
分析問題的過程,也是解決問題的過程,也能鍛煉自己編寫代碼時思考問題的方式和角度。
上述測試代碼地址
以上就是Redis實現(xiàn)分布式鎖的五種方法詳解的詳細內(nèi)容,更多關(guān)于Redis分布式鎖的資料請關(guān)注本站其它相關(guān)文章!
版權(quán)聲明:本站文章來源標注為YINGSOO的內(nèi)容版權(quán)均為本站所有,歡迎引用、轉(zhuǎn)載,請保持原文完整并注明來源及原文鏈接。禁止復制或仿造本網(wǎng)站,禁止在非maisonbaluchon.cn所屬的服務器上建立鏡像,否則將依法追究法律責任。本站部分內(nèi)容來源于網(wǎng)友推薦、互聯(lián)網(wǎng)收集整理而來,僅供學習參考,不代表本站立場,如有內(nèi)容涉嫌侵權(quán),請聯(lián)系alex-e#qq.com處理。
關(guān)注官方微信