詳解redis分佈式鎖(優化redis分佈式鎖的過程及Redisson使用)
1. redis在實際的應用中
不僅可以用來緩存數據,在分佈式應用開發中,經常被用來當作分佈式鎖的使用,為什麼要用到分佈式鎖呢?
在分佈式的開發中,以電商庫存的更新功能進行講解,在實際的應用中相同功能的消費者是有多個的,假如多個消費者同一時刻要去消費一條數據,假如業務邏輯處理邏輯是查詢出redis中的商品庫存,而如果第一個進來的消費的消費者獲取到庫存瞭,還沒進行減庫存操作,相對晚來的消費者就獲取瞭商品的庫存,這樣就導致數據會出錯,導致消費的數據變多瞭。
例如:消費者A和消費者B分別去消費生產者C1和生產者C2的數據,而生產者都是使用同一個redis的數據庫的,如果生產者C1接收到消費者A的消息後,先進行查詢庫存,然後當要進行減庫存的時候,因為生產者C2接收到消費者B的消息後,也去查詢庫存,而因為生產者C1還沒有進行庫存的更新,導致生產者C2獲取到的庫存數是臟數據,而不是生產者C1更新後的數據,導致業務出錯。
如果不是分佈式的應用,可以使用synchronized進行防止庫存更新的問題的產生,但是synchronized隻是基於JVM層面的,如果在不同的JVM中,就不能實現這樣的功能。
@GetMapping("getInt0") public String test() { synchronized (this) { //獲取當前商品的數量 int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product")); //然後對商品進行出庫操作,即進行減1 /* * a業務邏輯 * * */ 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的功能進行實現分佈式鎖
2.1 redis分佈式鎖思想
如果對redis熟悉的話,我們能夠想到redis中具有setnx的命令,該命令的功能宇set功能類似,但是setnx的命令在進行存數據前,會檢查redis中是否已經存在相同的key,如存在的話就返回false,反之則返回true,因此我們可以使用該命令的功能,設計一個分佈式鎖。
2.1.1設計思想:
- 在請求相同功能的接口時,使用redis的setnx命令,如果使用setnx命令後返回的是為true,說明此時沒有其他的調用這個接口,就相當於獲取到鎖瞭,然後就可以繼續執行接下來的業務邏輯瞭。當執行完業務邏輯後,在返回數據前,就把key刪除瞭,然後其他的請求就能獲取到鎖瞭。
- 如果使用setnx命令,返回的是false,說明此時有其他的消費者正在調用這個接口,因此需要等待其他消費者順利消費完成後,才能獲取到分佈式的鎖。
2.1.2 根據上面的設計思想進行代碼實現
代碼片段【1】
@GetMapping("getInt1") public String fubushisuo(){ //setIfAbsent的指令功能和redis命令中的setNx功能一樣,如果redis中已經存在相同的key,則返回false String lockkey = "yigehaimeirumengdechengxuyuan"; String lockvalue = "yigehaimeirumengdechengxuyuan"; boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,lockvalue); //如果能夠成功的設置lockkey,這說明當前獲取到分佈式鎖 if (!opsForSet){ return "false"; } //獲取當前商品的數量 int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product")); //然後對商品進行出庫操作,即進行減1 /* * a業務邏輯 * * */ if (productNum>0){ stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1)); int productNumNow = productNum - 1; }else { return "product=0"; } //然後進行釋放鎖 stringRedisTemplate.delete(lockkey); int productNumNow = productNum-1; return "success="+productNumNow; }
2.1.2.1反思代碼片段【1】
如果使用這種方式,會產生死鎖的方式:
死鎖發生的情況:
(1) 如果在a業務邏輯出現錯誤時,導致不能執行delete()操作,使得其他的請求不能獲取到分佈式鎖,業務lockkey一直存在於reids中,導致setnx操作一直失敗,所以不能獲取到分佈式鎖
(2) 解決方法,使用對業務代碼進行try…catch操作,如果出現錯誤,那麼使用finally對key進行刪除
優化代碼【2】
@GetMapping("getInt2") public String fubushisuo2(){ //setIfAbsent的指令功能和redis命令中的setNx功能一樣,如果redis中已經存在相同的key,則返回false String lockkey = "yigehaimeirumengdechengxuyuan"; String lockvalue = "yigehaimeirumengdechengxuyuan"; boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,lockvalue); int productNumNow = 0; //如果能夠成功的設置lockkey,這說明當前獲取到分佈式鎖 if (!opsForSet){ return "false"; } try { //獲取當前商品的數量 int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product")); //然後對商品進行出庫操作,即進行減1 /* * b業務邏輯 * */ 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 { //然後進行釋放鎖 stringRedisTemplate.delete(lockkey); } return "success="+productNumNow; }
2.1.2.2反思代碼【2】
出現問題的情況:
如果這種情況也有會產生的情況,如果此時有多臺服務器都在運行該方法,
其中有一個方法獲取到瞭分佈式鎖,而在運行下面的業務代碼時,此時該服務器突然宕機瞭,導致其他的不能獲取到分佈式鎖,
解決方法:加上過期時間,但又服務宕機瞭,過瞭設置的時間後,redis會可以把key給刪除,這樣其他的的服務器就可以正常的進行上鎖瞭。
優化代碼【3】
@GetMapping("getInt3") public String fubushisuo3(){ //setIfAbsent的指令功能和redis命令中的setNx功能一樣,如果redis中已經存在相同的key,則返回false String lockkey = "yigehaimeirumengdechengxuyuan"; String lockvalue = "yigehaimeirumengdechengxuyuan"; //[01] boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,lockvalue); //設置過期時間為10秒,但是如果使用該命令,沒有原子性,可能執行expire前宕機瞭,而不是設置過期時間, //[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; //如果能夠成功的設置lockkey,這說明當前獲取到分佈式鎖 if (!opsForSet){ return "false"; } try { //獲取當前商品的數量 int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product")); //然後對商品進行出庫操作,即進行減1 /* * c業務邏輯 * */ 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 { //然後進行釋放鎖 stringRedisTemplate.delete(lockkey); } return "success="+productNumNow; }
2.1.2.3 反思優化代碼【3】
出現問題的情況:
如果c業務邏輯持續超過瞭設置時間,導致redis中的lockkey過期瞭,
而其他的用戶此時訪問該方法時獲取到鎖瞭,而在此時,之前的的c業務邏輯也執行完成瞭,但是他會執行delete,把lcokkey刪除瞭。導致分佈式鎖出錯。
例子:在12:01:55的時刻,有一個A來執行該getInt3方法,並且成功獲取到鎖,但是A執行瞭10秒後還不能完成業務邏輯,導致redis中的鎖過期瞭,而在11秒的時候有B來執行getint3方法,因為key被A刪除瞭,導致B能夠成功的獲取redis鎖,而在B獲取鎖後,A因為執行完成瞭,然後把reids中的key給刪除瞭,但是我們註意的是,A刪除的鎖是B加上去的,而A的鎖是因為過期瞭,才被redis自己刪除瞭,因此這導致瞭C如果此時來時也能獲取redis分佈式鎖
解決方法:使用UUID,產生一個隨機數,當要進行delete(刪除)redis中key時,判斷是不是之前自己設置的UUID
代碼優化【4】
@GetMapping("getInt4") public String fubushisuo4(){ //setIfAbsent的指令功能和redis命令中的setNx功能一樣,如果redis中已經存在相同的key,則返回false String lockkey = "yigehaimeirumengdechengxuyuan"; //獲取UUID String lockvalue = UUID.randomUUID().toString(); Boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey, lockvalue, 10, TimeUnit.SECONDS); int productNumNow = 0; //如果能夠成功的設置lockkey,這說明當前獲取到分佈式鎖 if (!opsForSet){ return "false"; } try { //獲取當前商品的數量 int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product")); //然後對商品進行出庫操作,即進行減1 /* * c業務邏輯 * */ 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 { //進行釋放鎖 if (lockvalue==stringRedisTemplate.opsForValue().get(lockkey)){ stringRedisTemplate.delete(lockkey); } } return "success="+productNumNow; }
2.1.2.4 反思優化代碼【4】
出現問題的情況:
此時該方法是比較完美的,一般並發不是超級大的情況下都可以進行使用,但是關於key的過期時間需要根據業務執行的時間,進行設置,防止在業務還沒執行完時,key就過期瞭.
解決方法:目前有很多redis的分佈式鎖的框架,其中redisson用的是比較多的
2.2 使用redisson進行實現分佈式鎖
先添加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); } }
實現分佈式鎖代碼如下
@GetMapping("getInt5") public String fubushisuo5(){ //setIfAbsent的指令功能和redis命令中的setNx功能一樣,如果redis中已經存在相同的key,則返回false String lockkey = "yigehaimeirumengdechengxuyuan"; //獲取UUID RLock lock = redisson.getLock(lockkey); lock.lock(); int productNumNow = 0; //如果能夠成功的設置lockkey,這說明當前獲取到分佈式鎖 try { //獲取當前商品的數量 int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product")); //然後對商品進行出庫操作,即進行減1 /* * c業務邏輯 * */ 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(); } //然後進行釋放鎖 return "success="+productNumNow; }
從面就能看到,redisson實現分佈式鎖是非常簡單的,隻要簡單的幾條命令就能實現分佈式鎖的功能的。
redisson實現分佈式鎖的隻要原理如下:
redisson使用瞭Lua腳本語言使得命令既有原子性,redisson獲取鎖時,會給key設置30秒的過期是按,同時redisson會記錄當前請求的線程編號,然後定時的去檢查該線程的狀態,如果還處於執行狀態的話,而且key差不多要超期過時時,redisson會修改key的過期時間,一般增加10秒。這樣就可以動態的設置key的過期時間瞭,彌補瞭優化代碼【4】的片段
到此這篇關於redis分佈式鎖詳解(優化redis分佈式鎖的過程及Redisson使用)的文章就介紹到這瞭,更多相關redis分佈式鎖內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 基於Redission的分佈式鎖實戰
- 關於分佈式鎖(Redisson)的原理分析
- SpringBoot基於Redis的分佈式鎖實現過程記錄
- 詳解Redis分佈式鎖的原理與實現
- 關於SpringBoot 使用 Redis 分佈式鎖解決並發問題