爲什麼要使用分佈式鎖?
因爲服務器使用了集羣方案。詞窮。。。
怎麼使用分佈式鎖?
需求
實現一個查詢數據庫,在大於0的情況下減庫存這樣小小的功能。
測試:模擬100併發並看結果
基礎代碼
沒有任何鎖
@RequestMapping("/reduce_stock")
public String reduceStock() {
//查數據庫(redis)中庫存數量
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
//判斷庫存
if (stock > 0) {
System.out.println("消費庫存成功--->" + stock);
//更新庫存
stock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
} else {
System.out.println("消費庫存失敗。。。");
}
return "helloworld";
}
用測壓工具測壓結果:出現併發問題
有鎖:給方法添加synchronized關鍵字
@RequestMapping("/reduce_stock")
public synchronized String reduceStock() {
//查數據庫(redis)中庫存數量
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
//判斷庫存
if (stock > 0) {
System.out.println("消費庫存成功--->" + stock);
//更新庫存
stock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
} else {
System.out.println("消費庫存失敗。。。");
}
return "helloworld";
}
單機測試結果:沒有問題
分佈式測試結果:出現線程安全問題
分析,如下圖所示:
兩個微服務,synchronized關鍵字只能鎖住一個微服務,跨微服務是鎖不住的。
就像你家的屋子A複製一份爲B,A是否鎖門和B是否鎖門是沒有關係的。
基於redis的分佈式鎖(理論+實操)
理論
基於redis的setnx命令實現分佈式鎖
setnx命令的特點是:當你第一次設置的時候會返回1,後面在設置的時候就會返回0(即修改失敗),如下圖所示
手寫基於redis分佈式鎖(此處邏輯、理論大於實操)
一代代碼
分析
---邏輯:先獲取鎖,如果獲取鎖,就繼續;否則就不執行
---問題:容易出現死鎖。如果我獲取鎖成功後在執行業務邏輯的過程中出現異常,則釋放鎖的過程就沒有了,不釋放鎖就會引起死鎖
@RequestMapping("/reduce_stock")
public String reduceStock() {
//key的名稱
String lockKey = "lock";
//setnx key value 加鎖邏輯
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1");
if (!aBoolean){
return "fail";
}
//查數據庫(redis)中庫存數量
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
//判斷庫存
if (stock > 0) {
System.out.println("消費庫存成功--->" + stock);
//更新庫存
stock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
} else {
System.out.println("消費庫存失敗。。。");
}
// del key 釋放鎖邏輯
stringRedisTemplate.delete(lockKey);
return "helloworld";
}
二代代碼
分析:
---優點:在finally中釋放鎖,解決了死鎖的問題
---問題:引起鎖失效問題。看下面的代碼,先加鎖,如果加鎖失敗,返回,但是此時代碼也會去執行finally中釋放鎖的功能,從而使別人加的鎖失效。
@RequestMapping("/reduce_stock")
public String reduceStock() {
//key的名稱
String lockKey = "lock";
try {
//setnx key value 加鎖邏輯
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1");
if (!aBoolean) {
return "fail";
}
//查數據庫(redis)中庫存數量
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
//判斷庫存
if (stock > 0) {
System.out.println("消費庫存成功--->" + stock);
//更新庫存
stock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
} else {
System.out.println("消費庫存失敗。。。");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// del key 釋放鎖邏輯
stringRedisTemplate.delete(lockKey);
}
return "helloworld";
}
三代代碼
分析:
--優點:解決了鎖失效問題
--問題:沒有解決因爲宕機而引起的死鎖,如下圖所示,微服務8082獲取鎖後在執行業務邏輯時系統宕機後就會引起死鎖
@RequestMapping("/reduce_stock")
public String reduceStock() {
//key的名稱
String lockKey = "lock";
//setnx key value 加鎖邏輯
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1");
if (!aBoolean) {
return "fail";
}
try {
//查數據庫(redis)中庫存數量
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
//判斷庫存
if (stock > 0) {
System.out.println("消費庫存成功--->" + stock);
//更新庫存
stock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
} else {
System.out.println("消費庫存失敗。。。");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// del key 釋放鎖邏輯
stringRedisTemplate.delete(lockKey);
}
return "helloworld";
}
四代代碼
分析:
--優點:加鎖邏輯時設置過期時間,可以解決三代代碼的死鎖問題,系統中斷了我到時間就自動釋放鎖
--問題:我設置的時間是30秒(隨機想的一個數),假設我的業務邏輯是35,那會引起鎖失效。如下圖所示
@RequestMapping("/reduce_stock")
public String reduceStock() {
//key的名稱
String lockKey = "lock";
//setnx key value 加鎖邏輯
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1",30, TimeUnit.SECONDS);
if (!aBoolean) {
return "fail";
}
try {
//查數據庫(redis)中庫存數量
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
//判斷庫存
if (stock > 0) {
System.out.println("消費庫存成功--->" + stock);
//更新庫存
stock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
} else {
System.out.println("消費庫存失敗。。。");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// del key 釋放鎖邏輯
stringRedisTemplate.delete(lockKey);
}
return "helloworld";
}
五代代碼
分析:
--優點:解決了四代代碼的鎖失效問題
--缺點:如下圖所示,如果我設置失效時間是30,而我業務邏輯時間是35,在30-35之間是有兩個線程同時訪問,這與獨佔鎖是矛盾的,所以此處存在問題。
@RequestMapping("/reduce_stock")
public String reduceStock() {
//key的名稱
String lockKey = "lock";
//value的值
String clientId = UUID.randomUUID().toString();
//setnx key value 加鎖邏輯
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
if (!aBoolean) {
return "fail";
}
try {
//查數據庫(redis)中庫存數量
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
//判斷庫存
if (stock > 0) {
System.out.println("消費庫存成功--->" + stock);
//更新庫存
stock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
} else {
System.out.println("消費庫存失敗。。。");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//只能釋放自己加的鎖
if (clientId.equals(stringRedisTemplate.opsForValue().get("lock"))) {
// del key 釋放鎖邏輯
stringRedisTemplate.delete(lockKey);
}
}
return "helloworld";
}
瓶頸
我們現在的瓶頸就是超時時間的設置。
如果設置短了會出現五代代碼的問題;如果設置長了,你不能保證業務邏輯一定會比你設置的時間短,就算你設置的時間長,10分鐘,那萬一系統中斷10分鐘內不能有業務處理,也是不可取的。
如果我們能動態修改這個超時時間,那就無敵了
其實還有一個問題,這短代碼的邏輯是獲取鎖失敗後直接返回,其實應該繼續嘗試獲取
基於redisson的分佈式鎖
原理
實踐
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.10.0</version>
</dependency>
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.10.30.146:6379").setDatabase(0).setPassword("123456");
return (Redisson) Redisson.create(config);
}
@RestController
public class DistributedLockController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private Redisson redisson;
@RequestMapping("/reduce_stock")
public String reduceStock() {
//key的名稱
String lockKey = "lock";
RLock lock = redisson.getLock(lockKey);
lock.lock();
try {
//查數據庫(redis)中庫存數量
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
//判斷庫存
if (stock > 0) {
System.out.println("消費庫存成功--->" + stock);
//更新庫存
stock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
} else {
System.out.println("消費庫存失敗。。。");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return "helloworld";
}
}
問題
向redis集羣寫數據的步驟是:
1)向master節點寫數據
2) master節點返回
3)master節點同步到子節點
如果 線程t1 獲取鎖,寫入一個數據 1) 2)成功後 此時master 節點掉線了, 在子節點中選一個master,但是這個master是沒有t1寫的數據,此時此刻t2是可以獲取到鎖的,這個是redis做分佈式鎖的瑕疵。
redis是高性能分佈式鎖,zk是高可靠分佈式鎖,看你看重性能還是一致性了。
壓測工具
Apache JMeter https://jmeter.apache.org/
安裝 https://www.cnblogs.com/telescope11/p/9848106.html
使用 https://www.cnblogs.com/monjeo/p/9330464.html
參考
https://ke.qq.com/course/455755?term_id=100545359&taid=4035731660403787