基於redis的分佈式鎖
一、爲什麼要做
無疑,關於分佈式鎖,我們都已比較熟悉,網上有較多的開源解決方案,如redis的redisson,以及zookeeper的curator等,關於這兩種分佈式鎖的使用及原理,後期會寫文章介紹。本文主要針對小白,分享一下我學習分佈式鎖的一些心得,如果是大神請留下您的寶貴意見。
二、俯視代碼
關於代碼,我寫的比較精簡,力爭在保證功能的情況下讓使用上變得更加簡單。
1.設計思路
redis鎖的核心注意點主要有:
- 設置鎖和過期時間是否是原子操作
- 過期時間設置是否合理?太長如果當前實例crash掉影響其他實例獲取鎖的效率(例如設置一分鐘,則其他線程需要等待一分鐘之後才能重新獲取鎖,在這期間無法處理業務),太短的話可能會導致業務還沒處理完key就過期,導致鎖失效的雪崩效應。
在設計上主要參考了JUC鎖的使用模式,實現其Lock接口,在使用上當作ReentranLock使用即可。
2.代碼分析
首先看一下鎖的主體,首先在過期時間上採取一個比較折中的策略:默認30s,目前直接在代碼寫死,後期優化成可配置的形式,這樣程序宕掉也不至於長時間的不可用;其次,關於業務線程可能阻塞導致的執行時間過長的問題,這邊可以看到在lock的時候會啓動一個WatchDog線程,此線程的作用是用於監視key的剩餘過期時間,發現過小時完成自動續約,以此來保證鎖不會被提前釋放。
@Component
@Slf4j
public class DefaultLock implements Lock {
public static final String LOCK = "lock";
//默認鎖過期時間30秒,後期優化做成可配置
private static long DEFAULT_EXPIRE_TIME = 30L;
@Autowired
private RedisTemplate redisTemplate;
private WatchDog watchDog;
@Override
public void lock() {
//1.這裏應對和本地jvm鎖一樣競爭的場景,如果競爭失敗,則自旋
while(!tryLock()){
log.info("線程{}獲取分佈式鎖失敗",Thread.currentThread().getName());
}
//2.這裏應對有些需要沒有獲取到鎖直接返回失敗的場景,後期會做一個策略優化
// Boolean re = redisTemplate.opsForValue().setIfAbsent(LOCK, UUID.randomUUID(), DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
// if(Boolean.FALSE.equals(re)){
// throw new LockCompititionFailException("獲取分佈式鎖失敗,當前線程ID:"+Thread.currentThread().getId());
// }
//走到這裏說明獲取分佈式鎖成功,開啓線程監視key過期時間,防止業務流程還沒結束就釋放鎖的情況
startWatchDog();
}
private void startWatchDog(){
watchDog = new WatchDog(true,redisTemplate);
watchDog.start();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return redisTemplate.opsForValue().setIfAbsent(LOCK, UUID.randomUUID(), DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
//業務流程結束,釋放鎖
redisTemplate.delete(LOCK);
//將watchdog線程停止
watchDog.setBusinessDone(false);
}
@Override
public Condition newCondition() {
return null;
}
}
再來看一下WatchDog的實現,比較簡單,主要完成續約的問題。
public class WatchDog extends Thread{
//業務流程是否完成
private boolean businessDone;
private RedisTemplate redisTemplate;
public WatchDog(boolean businessDone,RedisTemplate redisTemplate){
this.businessDone = businessDone;
this.redisTemplate = redisTemplate;
}
public boolean isBusinessDone() {
return businessDone;
}
public void setBusinessDone(boolean businessDone) {
this.businessDone = businessDone;
}
@Override
public void run() {
while(businessDone){
//如果鎖的剩餘過期時間小於10s ,則將其重置
if(redisTemplate.getExpire(DefaultLock.LOCK) < 10L){
redisTemplate.expire(DefaultLock.LOCK,30L, TimeUnit.SECONDS);
}
//爲避免空轉太頻繁,適當讓線程sleep
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
三、關於使用
使用上比較簡單,用ioc注入DefaultLock實例即可。
public class ZmqTestController {
@Autowired
DefaultLock defaultLock;
//模擬商品數量
private Integer count = 50;
@RequestMapping("/decrease")
@ResponseBody
public String testDistributeLock(){
try {
//加上鎖
defaultLock.lock();
if(count < 1){
log.info("庫存不足");
return "失敗";
}
count--;
log.info("當前線程:{},庫存扣減成功,剩餘庫存:{}",Thread.currentThread().getId(),count);
}catch (Exception e){
log.info(e.getMessage());
}finally {
//釋放鎖
defaultLock.unlock();
}
return "成功";