手寫一個基於redis的分佈式鎖

基於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 "成功";
 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章