前言
當不同的進程,必須以獨占資源的方式實現資源共享,就需要用到分布式鎖。
安全和穩定性
分布式鎖的實現,必須滿足以下2個特性
- 獨享互斥:在任意一個時刻,只能有一個客戶端持有鎖
- 無死鎖:既然有加鎖,則必須存在解鎖。即使持有鎖的客戶端崩潰宕機,鎖仍然允許被其他客戶端獲取,不能造成無限期的等待
例子1
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/lock")
public void lock1() throws InterruptedException {
String lockKey = "lockKey", lockValue = "lockValue";
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, Duration.ofSeconds(10));
if (success){
try{
System.out.println(String.format("Thread-%d:Success", Thread.currentThread().getId()));
}
finally {
stringRedisTemplate.delete(lockKey);
}
}
}
- 獨享互斥:setIfAbsent等同於Redis的SETNX命令,當key不存在才生效,返回的是true;當key存在時返回false。
- 無死鎖:setIfAbsent第3個參數是key超時時間,就算中途應用服務掛了,等時間到了key自動失效。另外,finally處理刪除key操作,及時釋放鎖。
用JMeter發送1000個並發請求,結果如下:
Thread-227:Success
Thread-88:Success
Thread-116:Success
Thread-202:Success
Thread-185:Success
Thread-97:Success
但是,上述Demo存在以下問題:
- 客戶端A獲取到鎖資源,同時設置超時時間10s,緊接着A被其他操作堵塞了進程
- 10s過后,由於A的業務尚未執行完,鎖過期自動失效了
- 客戶端B成功獲取鎖資源
- 此時A的業務邏輯執行完畢,做了釋放鎖操作,此時刪除的KEY是客戶端B加鎖的KEY
- 客戶端C嘗試獲取鎖資源,由於KEY已經被A刪掉了,所以C也加鎖成功,和客戶端B存在了並發的問題
例子2
public void lock3() throws InterruptedException {
String lockKey = "lockKey";
//lock value改成唯一性的UUID
String lockValue = UUID.randomUUID().toString();
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, Duration.ofSeconds(10));
if (success){
try{
System.out.println(String.format("Thread-%d:Success", Thread.currentThread().getId()));
}
finally {
//釋放鎖不再是簡單的DELETE KEY
releaseLock(lockKey, lockValue);
}
}
}
private boolean releaseLock(String key, String value){
String srcValue = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isEmpty(srcValue)){
return true;
}
else if (srcValue.equals(value)){
stringRedisTemplate.delete(key);
return true;
}
return false;
}
上面增加了releaseLock函數:
- 先從Redis取出KEY
- 如果KEY不存在,說明已經過期了,這里直接return true,當做釋放鎖資源成功
- 如果KEY存在,把VALUE和加鎖時用的UUID做比較,相等就說明這是自己占用的鎖資源,直接DELETE;如果不相等,就不要做DELETE操作,說明這是其他客戶端加的鎖
擴展鎖
如果你的業務邏輯可以拆解成多個小步驟,可以將鎖的有效時間設置短一些。在業務處理的過程中,當發現KEY的有效期很短時,再次延長其有效期(前提還是key存在並且value是之前設置的value)
