手写一个基于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 "成功";
 

 

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