詳解redis分布式鎖(優(yōu)化redis分布式鎖的過程及Redisson使用)
1. redis在實(shí)際的應(yīng)用中
不僅可以用來緩存數(shù)據(jù),在分布式應(yīng)用開發(fā)中,經(jīng)常被用來當(dāng)作分布式鎖的使用,為什么要用到分布式鎖呢?
在分布式的開發(fā)中,以電商庫存的更新功能進(jìn)行講解,在實(shí)際的應(yīng)用中相同功能的消費(fèi)者是有多個(gè)的,假如多個(gè)消費(fèi)者同一時(shí)刻要去消費(fèi)一條數(shù)據(jù),假如業(yè)務(wù)邏輯處理邏輯是查詢出redis中的商品庫存,而如果第一個(gè)進(jìn)來的消費(fèi)的消費(fèi)者獲取到庫存了,還沒進(jìn)行減庫存操作,相對(duì)晚來的消費(fèi)者就獲取了商品的庫存,這樣就導(dǎo)致數(shù)據(jù)會(huì)出錯(cuò),導(dǎo)致消費(fèi)的數(shù)據(jù)變多了。
例如:消費(fèi)者A和消費(fèi)者B分別去消費(fèi)生產(chǎn)者C1和生產(chǎn)者C2的數(shù)據(jù),而生產(chǎn)者都是使用同一個(gè)redis的數(shù)據(jù)庫的,如果生產(chǎn)者C1接收到消費(fèi)者A的消息后,先進(jìn)行查詢庫存,然后當(dāng)要進(jìn)行減庫存的時(shí)候,因?yàn)樯a(chǎn)者C2接收到消費(fèi)者B的消息后,也去查詢庫存,而因?yàn)樯a(chǎn)者C1還沒有進(jìn)行庫存的更新,導(dǎo)致生產(chǎn)者C2獲取到的庫存數(shù)是臟數(shù)據(jù),而不是生產(chǎn)者C1更新后的數(shù)據(jù),導(dǎo)致業(yè)務(wù)出錯(cuò)。
如果不是分布式的應(yīng)用,可以使用synchronized進(jìn)行防止庫存更新的問題的產(chǎn)生,但是synchronized只是基于JVM層面的,如果在不同的JVM中,就不能實(shí)現(xiàn)這樣的功能。
@GetMapping("getInt0") public String test() { synchronized (this) { //獲取當(dāng)前商品的數(shù)量 int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product")); //然后對(duì)商品進(jìn)行出庫操作,即進(jìn)行減1 /* * a業(yè)務(wù)邏輯 * * */ if (productNum > 0) { stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1)); int productNumNow = productNum - 1; } else { return "product=0"; } int productNumNow = productNum - 1; return "success=" + productNumNow; } }
2.如何使用redis的功能進(jìn)行實(shí)現(xiàn)分布式鎖
2.1 redis分布式鎖思想
如果對(duì)redis熟悉的話,我們能夠想到redis中具有setnx的命令,該命令的功能宇set功能類似,但是setnx的命令在進(jìn)行存數(shù)據(jù)前,會(huì)檢查redis中是否已經(jīng)存在相同的key,如存在的話就返回false,反之則返回true,因此我們可以使用該命令的功能,設(shè)計(jì)一個(gè)分布式鎖。
2.1.1設(shè)計(jì)思想:
- 在請(qǐng)求相同功能的接口時(shí),使用redis的setnx命令,如果使用setnx命令后返回的是為true,說明此時(shí)沒有其他的調(diào)用這個(gè)接口,就相當(dāng)于獲取到鎖了,然后就可以繼續(xù)執(zhí)行接下來的業(yè)務(wù)邏輯了。當(dāng)執(zhí)行完業(yè)務(wù)邏輯后,在返回?cái)?shù)據(jù)前,就把key刪除了,然后其他的請(qǐng)求就能獲取到鎖了。
- 如果使用setnx命令,返回的是false,說明此時(shí)有其他的消費(fèi)者正在調(diào)用這個(gè)接口,因此需要等待其他消費(fèi)者順利消費(fèi)完成后,才能獲取到分布式的鎖。
2.1.2 根據(jù)上面的設(shè)計(jì)思想進(jìn)行代碼實(shí)現(xiàn)
代碼片段【1】
@GetMapping("getInt1") public String fubushisuo(){ //setIfAbsent的指令功能和redis命令中的setNx功能一樣,如果redis中已經(jīng)存在相同的key,則返回false String lockkey = "yigehaimeirumengdechengxuyuan"; String lockvalue = "yigehaimeirumengdechengxuyuan"; boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,lockvalue); //如果能夠成功的設(shè)置lockkey,這說明當(dāng)前獲取到分布式鎖 if (!opsForSet){ return "false"; } //獲取當(dāng)前商品的數(shù)量 int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product")); //然后對(duì)商品進(jìn)行出庫操作,即進(jìn)行減1 /* * a業(yè)務(wù)邏輯 * * */ if (productNum>0){ stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1)); int productNumNow = productNum - 1; }else { return "product=0"; } //然后進(jìn)行釋放鎖 stringRedisTemplate.delete(lockkey); int productNumNow = productNum-1; return "success="+productNumNow; }
2.1.2.1反思代碼片段【1】
如果使用這種方式,會(huì)產(chǎn)生死鎖的方式:
死鎖發(fā)生的情況:
(1) 如果在a業(yè)務(wù)邏輯出現(xiàn)錯(cuò)誤時(shí),導(dǎo)致不能執(zhí)行delete()操作,使得其他的請(qǐng)求不能獲取到分布式鎖,業(yè)務(wù)lockkey一直存在于reids中,導(dǎo)致setnx操作一直失敗,所以不能獲取到分布式鎖
(2) 解決方法,使用對(duì)業(yè)務(wù)代碼進(jìn)行try…catch操作,如果出現(xiàn)錯(cuò)誤,那么使用finally對(duì)key進(jìn)行刪除
優(yōu)化代碼【2】
@GetMapping("getInt2") public String fubushisuo2(){ //setIfAbsent的指令功能和redis命令中的setNx功能一樣,如果redis中已經(jīng)存在相同的key,則返回false String lockkey = "yigehaimeirumengdechengxuyuan"; String lockvalue = "yigehaimeirumengdechengxuyuan"; boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,lockvalue); int productNumNow = 0; //如果能夠成功的設(shè)置lockkey,這說明當(dāng)前獲取到分布式鎖 if (!opsForSet){ return "false"; } try { //獲取當(dāng)前商品的數(shù)量 int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product")); //然后對(duì)商品進(jìn)行出庫操作,即進(jìn)行減1 /* * b業(yè)務(wù)邏輯 * */ if (productNum>0){ stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1)); productNumNow = productNum-1; }else { return "product=0"; } }catch (Exception e){ System.out.println(e.getCause()); }finally { //然后進(jìn)行釋放鎖 stringRedisTemplate.delete(lockkey); } return "success="+productNumNow; }
2.1.2.2反思代碼【2】
出現(xiàn)問題的情況:
如果這種情況也有會(huì)產(chǎn)生的情況,如果此時(shí)有多臺(tái)服務(wù)器都在運(yùn)行該方法,
其中有一個(gè)方法獲取到了分布式鎖,而在運(yùn)行下面的業(yè)務(wù)代碼時(shí),此時(shí)該服務(wù)器突然宕機(jī)了,導(dǎo)致其他的不能獲取到分布式鎖,
解決方法:加上過期時(shí)間,但又服務(wù)宕機(jī)了,過了設(shè)置的時(shí)間后,redis會(huì)可以把key給刪除,這樣其他的的服務(wù)器就可以正常的進(jìn)行上鎖了。
優(yōu)化代碼【3】
@GetMapping("getInt3") public String fubushisuo3(){ //setIfAbsent的指令功能和redis命令中的setNx功能一樣,如果redis中已經(jīng)存在相同的key,則返回false String lockkey = "yigehaimeirumengdechengxuyuan"; String lockvalue = "yigehaimeirumengdechengxuyuan"; //[01] boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,lockvalue); //設(shè)置過期時(shí)間為10秒,但是如果使用該命令,沒有原子性,可能執(zhí)行expire前宕機(jī)了,而不是設(shè)置過期時(shí)間, //[02] stringRedisTemplate.expire(lockkey, Duration.ofSeconds(10)); //使用setIfAbsent(lockkey,lockvalue,10,TimeUnit.SECONDS);代碼代替上面[01],[02]行代碼 Boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey, lockvalue, 10, TimeUnit.SECONDS); int productNumNow = 0; //如果能夠成功的設(shè)置lockkey,這說明當(dāng)前獲取到分布式鎖 if (!opsForSet){ return "false"; } try { //獲取當(dāng)前商品的數(shù)量 int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product")); //然后對(duì)商品進(jìn)行出庫操作,即進(jìn)行減1 /* * c業(yè)務(wù)邏輯 * */ if (productNum>0){ stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1)); productNumNow = productNum-1; }else { return "product=0"; } }catch (Exception e){ System.out.println(e.getCause()); }finally { //然后進(jìn)行釋放鎖 stringRedisTemplate.delete(lockkey); } return "success="+productNumNow; }
2.1.2.3 反思優(yōu)化代碼【3】
出現(xiàn)問題的情況:
如果c業(yè)務(wù)邏輯持續(xù)超過了設(shè)置時(shí)間,導(dǎo)致redis中的lockkey過期了,
而其他的用戶此時(shí)訪問該方法時(shí)獲取到鎖了,而在此時(shí),之前的的c業(yè)務(wù)邏輯也執(zhí)行完成了,但是他會(huì)執(zhí)行delete,把lcokkey刪除了。導(dǎo)致分布式鎖出錯(cuò)。
例子:在12:01:55的時(shí)刻,有一個(gè)A來執(zhí)行該getInt3方法,并且成功獲取到鎖,但是A執(zhí)行了10秒后還不能完成業(yè)務(wù)邏輯,導(dǎo)致redis中的鎖過期了,而在11秒的時(shí)候有B來執(zhí)行g(shù)etint3方法,因?yàn)閗ey被A刪除了,導(dǎo)致B能夠成功的獲取redis鎖,而在B獲取鎖后,A因?yàn)閳?zhí)行完成了,然后把reids中的key給刪除了,但是我們注意的是,A刪除的鎖是B加上去的,而A的鎖是因?yàn)檫^期了,才被redis自己刪除了,因此這導(dǎo)致了C如果此時(shí)來時(shí)也能獲取redis分布式鎖
解決方法:使用UUID,產(chǎn)生一個(gè)隨機(jī)數(shù),當(dāng)要進(jìn)行delete(刪除)redis中key時(shí),判斷是不是之前自己設(shè)置的UUID
代碼優(yōu)化【4】
@GetMapping("getInt4") public String fubushisuo4(){ //setIfAbsent的指令功能和redis命令中的setNx功能一樣,如果redis中已經(jīng)存在相同的key,則返回false String lockkey = "yigehaimeirumengdechengxuyuan"; //獲取UUID String lockvalue = UUID.randomUUID().toString(); Boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey, lockvalue, 10, TimeUnit.SECONDS); int productNumNow = 0; //如果能夠成功的設(shè)置lockkey,這說明當(dāng)前獲取到分布式鎖 if (!opsForSet){ return "false"; } try { //獲取當(dāng)前商品的數(shù)量 int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product")); //然后對(duì)商品進(jìn)行出庫操作,即進(jìn)行減1 /* * c業(yè)務(wù)邏輯 * */ if (productNum>0){ stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1)); productNumNow = productNum-1; }else { return "product=0"; } }catch (Exception e){ System.out.println(e.getCause()); }finally { //進(jìn)行釋放鎖 if (lockvalue==stringRedisTemplate.opsForValue().get(lockkey)){ stringRedisTemplate.delete(lockkey); } } return "success="+productNumNow; }
2.1.2.4 反思優(yōu)化代碼【4】
出現(xiàn)問題的情況:
此時(shí)該方法是比較完美的,一般并發(fā)不是超級(jí)大的情況下都可以進(jìn)行使用,但是關(guān)于key的過期時(shí)間需要根據(jù)業(yè)務(wù)執(zhí)行的時(shí)間,進(jìn)行設(shè)置,防止在業(yè)務(wù)還沒執(zhí)行完時(shí),key就過期了.
解決方法:目前有很多redis的分布式鎖的框架,其中redisson用的是比較多的
2.2 使用redisson進(jìn)行實(shí)現(xiàn)分布式鎖
先添加redisson的maven依賴
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.11.1</version> </dependency>
redisson的bean配置
@Configuration public class RedissonConfigure { @Bean public Redisson redisson(){ Config config = new Config(); config.useSingleServer().setAddress("redis://27.196.106.42:6380").setDatabase(0); return (Redisson) Redisson.create(config); } }
實(shí)現(xiàn)分布式鎖代碼如下
@GetMapping("getInt5") public String fubushisuo5(){ //setIfAbsent的指令功能和redis命令中的setNx功能一樣,如果redis中已經(jīng)存在相同的key,則返回false String lockkey = "yigehaimeirumengdechengxuyuan"; //獲取UUID RLock lock = redisson.getLock(lockkey); lock.lock(); int productNumNow = 0; //如果能夠成功的設(shè)置lockkey,這說明當(dāng)前獲取到分布式鎖 try { //獲取當(dāng)前商品的數(shù)量 int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product")); //然后對(duì)商品進(jìn)行出庫操作,即進(jìn)行減1 /* * c業(yè)務(wù)邏輯 * */ if (productNum>0){ stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1)); productNumNow = productNum-1; }else { return "product=0"; } }catch (Exception e){ System.out.println(e.getCause()); }finally { lock.unlock(); } //然后進(jìn)行釋放鎖 return "success="+productNumNow; }
從面就能看到,redisson實(shí)現(xiàn)分布式鎖是非常簡單的,只要簡單的幾條命令就能實(shí)現(xiàn)分布式鎖的功能的。
redisson實(shí)現(xiàn)分布式鎖的只要原理如下:
redisson使用了Lua腳本語言使得命令既有原子性,redisson獲取鎖時(shí),會(huì)給key設(shè)置30秒的過期是按,同時(shí)redisson會(huì)記錄當(dāng)前請(qǐng)求的線程編號(hào),然后定時(shí)的去檢查該線程的狀態(tài),如果還處于執(zhí)行狀態(tài)的話,而且key差不多要超期過時(shí)時(shí),redisson會(huì)修改key的過期時(shí)間,一般增加10秒。這樣就可以動(dòng)態(tài)的設(shè)置key的過期時(shí)間了,彌補(bǔ)了優(yōu)化代碼【4】的片段
到此這篇關(guān)于redis分布式鎖詳解(優(yōu)化redis分布式鎖的過程及Redisson使用)的文章就介紹到這了,更多相關(guān)redis分布式鎖內(nèi)容請(qǐng)搜索本站以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持本站!
版權(quán)聲明:本站文章來源標(biāo)注為YINGSOO的內(nèi)容版權(quán)均為本站所有,歡迎引用、轉(zhuǎn)載,請(qǐng)保持原文完整并注明來源及原文鏈接。禁止復(fù)制或仿造本網(wǎng)站,禁止在非www.sddonglingsh.com所屬的服務(wù)器上建立鏡像,否則將依法追究法律責(zé)任。本站部分內(nèi)容來源于網(wǎng)友推薦、互聯(lián)網(wǎng)收集整理而來,僅供學(xué)習(xí)參考,不代表本站立場(chǎng),如有內(nèi)容涉嫌侵權(quán),請(qǐng)聯(lián)系alex-e#qq.com處理。