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.3 使用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】的片段
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/297107.html
標籤:其他
