本文小編為大家詳細(xì)介紹“redis實(shí)現(xiàn)分布式鎖的五種方法是什么”,內(nèi)容詳細(xì),步驟清晰,細(xì)節(jié)處理妥當(dāng),希望這篇“Redis實(shí)現(xiàn)分布式鎖的五種方法是什么”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學(xué)習(xí)新知識(shí)吧。
創(chuàng)新互聯(lián)公司專注于略陽網(wǎng)站建設(shè)服務(wù)及定制,我們擁有豐富的企業(yè)做網(wǎng)站經(jīng)驗(yàn)。 熱誠為您提供略陽營銷型網(wǎng)站建設(shè),略陽網(wǎng)站制作、略陽網(wǎng)頁設(shè)計(jì)、略陽網(wǎng)站官網(wǎng)定制、微信小程序開發(fā)服務(wù),打造略陽網(wǎng)絡(luò)公司原創(chuàng)品牌,更為您提供略陽網(wǎng)站排名全網(wǎng)營銷落地服務(wù)。
單機(jī)數(shù)據(jù)一致性架構(gòu)如下圖所示:多個(gè)可客戶訪問同一個(gè)服務(wù)器,連接同一個(gè)數(shù)據(jù)庫。
場(chǎng)景描述:客戶端模擬購買商品過程,在Redis
中設(shè)定庫存總數(shù)剩100個(gè)
,多個(gè)客戶端同時(shí)并發(fā)購買。
@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ù)回寫數(shù)據(jù)庫 template.opsForValue().set("goods:001",String.valueOf(realTotal)); System.out.println("購買商品成功,庫存還剩:"+realTotal +"件, 服務(wù)端口為8001"); return "購買商品成功,庫存還剩:"+realTotal +"件, 服務(wù)端口為8001"; }else{ System.out.println("購買商品失敗,服務(wù)端口為8001"); } return "購買商品失敗,服務(wù)端口為8001"; } }
使用Jmeter
模擬高并發(fā)場(chǎng)景,測(cè)試結(jié)果如下:
測(cè)試結(jié)果出現(xiàn)多個(gè)用戶購買同一商品,發(fā)生了數(shù)據(jù)不一致問題!
解決辦法:單體應(yīng)用的情況下,對(duì)并發(fā)的操作進(jìn)行加鎖操作,保證對(duì)數(shù)據(jù)的操作具有原子性
synchronized
ReentrantLock
@RestController public class IndexController2 { // 使用ReentrantLock鎖解決單體應(yīng)用的并發(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 + "件, 服務(wù)端口為8001"); return "購買商品成功,庫存還剩:" + realTotal + "件, 服務(wù)端口為8001"; } else { System.out.println("購買商品失敗,服務(wù)端口為8001"); } } catch (Exception e) { lock.unlock(); } finally { lock.unlock(); } return "購買商品失敗,服務(wù)端口為8001"; } }
上面解決了單體應(yīng)用的數(shù)據(jù)一致性問題,但如果是分布式架構(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ù)一致性問題!
取消單機(jī)鎖,下面使用redis
的set
命令來實(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)來先要進(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ā)生了異常,鎖就無法被釋放, // 釋放鎖操作不能在此操作,要在finally處理 // template.delete(REDIS_LOCK); System.out.println("購買商品成功,庫存還剩:" + realTotal + "件, 服務(wù)端口為8001"); return "購買商品成功,庫存還剩:" + realTotal + "件, 服務(wù)端口為8001"; } else { System.out.println("購買商品失敗,服務(wù)端口為8001"); } return "購買商品失敗,服務(wù)端口為8001"; }finally { // 釋放鎖 template.delete(REDIS_LOCK); } } }
上面的代碼,可以解決分布式架構(gòu)中數(shù)據(jù)一致性問題。但再仔細(xì)想想,還是會(huì)有問題,下面進(jìn)行改進(jìn)。
在上面的代碼中,如果程序在運(yùn)行期間,部署了微服務(wù)jar
包的機(jī)器突然掛了,代碼層面根本就沒有走到finally
代碼塊,也就是說在宕機(jī)前,鎖并沒有被刪除掉,這樣的話,就沒辦法保證解鎖
所以,這里需要對(duì)這個(gè)key
加一個(gè)過期時(shí)間,Redis
中設(shè)置過期時(shí)間有兩種方法:
template.expire(REDIS_LOCK,10, TimeUnit.SECONDS)
template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS)
第一種方法需要單獨(dú)的一行代碼,且并沒有與加鎖放在同一步操作,所以不具備原子性,也會(huì)出問題
第二種方法在加鎖的同時(shí)就進(jìn)行了設(shè)置過期時(shí)間,所有沒有問題,這里采用這種方式
調(diào)整下代碼,在加鎖的同時(shí),設(shè)置過期時(shí)間:
// 為key加一個(gè)過期時(shí)間,其余代碼不變 Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK,value,10L,TimeUnit.SECONDS);
這種方式解決了因服務(wù)突然宕機(jī)而無法釋放鎖的問題。但再仔細(xì)想想,還是會(huì)有問題,下面進(jìn)行改進(jìn)。
方式二設(shè)置了key
的過期時(shí)間,解決了key
無法刪除的問題,但問題又來了
上面設(shè)置了key
的過期時(shí)間為10
秒,如果業(yè)務(wù)邏輯比較復(fù)雜,需要調(diào)用其他微服務(wù),處理時(shí)間需要15
秒(模擬場(chǎng)
景,別較真),而當(dāng)10
秒鐘過去之后,這個(gè)key
就過期了,其他請(qǐng)求就又可以設(shè)置這個(gè)key
,此時(shí)如果耗時(shí)15
秒
的請(qǐng)求處理完了,回來繼續(xù)執(zhí)行程序,就會(huì)把別人設(shè)置的key
給刪除了,這是個(gè)很嚴(yán)重的問題!
所以,誰上的鎖,誰才能刪除
@RestController public class IndexController6 { public static final String REDIS_LOCK = "good_lock"; @Autowired StringRedisTemplate template; @RequestMapping("/buy6") public String index(){ // 每個(gè)人進(jìn)來先要進(jìn)行加鎖,key值為"good_lock" String value = UUID.randomUUID().toString().replace("-",""); try{ // 為key加一個(gè)過期時(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í)間較長。。。 int realTotal = total - 1; template.opsForValue().set("goods:001", String.valueOf(realTotal)); System.out.println("購買商品成功,庫存還剩:" + realTotal + "件, 服務(wù)端口為8001"); return "購買商品成功,庫存還剩:" + realTotal + "件, 服務(wù)端口為8001"; } else { System.out.println("購買商品失敗,服務(wù)端口為8001"); } return "購買商品失敗,服務(wù)端口為8001"; }finally { // 誰加的鎖,誰才能刪除?。。?! if(template.opsForValue().get(REDIS_LOCK).equals(value)){ template.delete(REDIS_LOCK); } } } }
這種方式解決了因服務(wù)處理時(shí)間太長而釋放了別人鎖的問題。這樣就沒問題了嗎?
在上面方式三下,規(guī)定了誰上的鎖,誰才能刪除,但finally
快的判斷和del
刪除操作不是原子操作,并發(fā)的時(shí)候也會(huì)出問題,并發(fā)嘛,就是要保證數(shù)據(jù)的一致性,保證數(shù)據(jù)的一致性,最好要保證對(duì)數(shù)據(jù)的操作具有原子性。
在Redis
的set
命令介紹中,最后推薦Lua
腳本進(jìn)行鎖的刪除,地址
@RestController public class IndexController7 { public static final String REDIS_LOCK = "good_lock"; @Autowired StringRedisTemplate template; @RequestMapping("/buy7") public String index(){ // 每個(gè)人進(jìn)來先要進(jìn)行加鎖,key值為"good_lock" String value = UUID.randomUUID().toString().replace("-",""); try{ // 為key加一個(gè)過期時(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í)間較長。。。 int realTotal = total - 1; template.opsForValue().set("goods:001", String.valueOf(realTotal)); System.out.println("購買商品成功,庫存還剩:" + realTotal + "件, 服務(wù)端口為8001"); return "購買商品成功,庫存還剩:" + realTotal + "件, 服務(wù)端口為8001"; } else { System.out.println("購買商品失敗,服務(wù)端口為8001"); } return "購買商品失敗,服務(wù)端口為8001"; }finally { // 誰加的鎖,誰才能刪除,使用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(); } } } } }
在方式四下,規(guī)定了誰上的鎖,誰才能刪除,并且解決了刪除操作沒有原子性問題。但還沒有考慮緩存續(xù)命,以及Redis
集群部署下,異步復(fù)制造成的鎖丟失:主節(jié)點(diǎn)沒來得及把剛剛set
進(jìn)來這條數(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)來先要進(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í)間較長。。。 int realTotal = total - 1; template.opsForValue().set("goods:001", String.valueOf(realTotal)); System.out.println("購買商品成功,庫存還剩:" + realTotal + "件, 服務(wù)端口為8001"); return "購買商品成功,庫存還剩:" + realTotal + "件, 服務(wù)端口為8001"; } else { System.out.println("購買商品失敗,服務(wù)端口為8001"); } return "購買商品失敗,服務(wù)端口為8001"; }finally { if(lock.isLocked() && lock.isHeldByCurrentThread()){ lock.unlock(); } } } }
讀到這里,這篇“Redis實(shí)現(xiàn)分布式鎖的五種方法是什么”文章已經(jīng)介紹完畢,想要掌握這篇文章的知識(shí)點(diǎn)還需要大家自己動(dòng)手實(shí)踐使用過才能領(lǐng)會(huì),如果想了解更多相關(guān)內(nèi)容的文章,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。