分布式锁

2020-01-29  本文已影响0人  wbpailxt
    @Scheduled(cron="0 */1 * * * ?")//每1分钟(每个1分钟的整数倍)
    public void closeOrderTaskV1(){
        log.info("关闭订单定时任务启动");
        int hour = Integer.parseInt(PropertiesUtil.getProperty("close.order.task.time.hour","2"));
        iOrderService.closeOrder(hour);
        log.info("关闭订单定时任务结束");
    }

在tomcat集群环境下,这样的代码是有问题的,会有多个节点同时执行关闭订单操作。这不是我所希望的,我只希望其中一个集群节点执行。
因此需要引入“锁”的概念,某一时刻,哪一个节点获得锁,该节点拥有执行关闭订单操作的权利。

使用redis实现分布式锁

锁的名字是lockkey。
setnx(key,value)命令会先判断key是否存在,不存在才设置相应的key-value,并且返回1。存在则直接返回0,表示设置key-value键值对失败,以此来代表获取锁失败。
判断key是否存在和设置key-value键值对是原子性操作。
设置完key-value键值对后,即拥有锁之后,执行业务,执行完业务就从redis缓存中删除key,即代表释放该锁。
可是这看似很完美的‘获取锁-释放锁’流程是存在不足的。
当某一节点获取锁,在执行业务的过程中节点所在服务器发生宕机,那么这个key永远会在redis中。意味着所有节点不会再拥有该锁。这是很严重的。
所以在执行业务前需要有一个“兜底”的动作,设置key的过期时间--expire(lockkey),这样即便在执行业务的过程中节点所在服务器发生宕机,那么这个key还会因为过期而从redis中失效,被其他节点获取到锁。

    @Scheduled(cron="0 */1 * * * ?")
    public void closeOrderTaskV2(){
        log.info("关闭订单定时任务启动");
        long lockTimeout = Long.parseLong(PropertiesUtil.getProperty("lock.timeout","5000"));

        Long setnxResult = RedisShardedPoolUtil.setnx(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,String.valueOf(System.currentTimeMillis()+lockTimeout));
        if(setnxResult != null && setnxResult.intValue() == 1){
            //如果返回值是1,代表设置成功,获取锁
            closeOrder(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        }else{
            log.info("没有获得分布式锁:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        }
        log.info("关闭订单定时任务结束");
    }

    private void closeOrder(String lockName){
        RedisShardedPoolUtil.expire(lockName,5);//有效期50秒,防止死锁
        log.info("获取{},ThreadName:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,Thread.currentThread().getName());
        int hour = Integer.parseInt(PropertiesUtil.getProperty("close.order.task.time.hour","2"));
        iOrderService.closeOrder(hour);
        RedisShardedPoolUtil.del(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        log.info("释放{},ThreadName:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,Thread.currentThread().getName());
        log.info("===============================");
    }

    //RedisShardedPoolUtil
     public static Long setnx(String key,String value){
        ShardedJedis jedis = null;
        Long result = null;

        try {
            jedis = RedisShardedPool.getJedis();
            result = jedis.setnx(key,value);
        } catch (Exception e) {
            log.error("setnx key:{} value:{} error",key,value,e);
            RedisShardedPool.returnBrokenResource(jedis);
            return result;
        }
        RedisShardedPool.returnResource(jedis);
        return result;
    }

执行业务前设置锁的“持有”时间,解决了执行业务时可能会发生宕机而锁没有释放(delete)的问题。可是宕机会发生在任何一刻,万一发生在expire(lockkey)之前呢?依旧面临同样的问题。
在刚刚的解决方案中,redis中存储的value值是currenttime+timeout。似乎在上一个解决方案中没有起到任何作用。其实它是为引出的这个解决方案作铺垫。

分布式锁演进

若获取锁的集群节点所在服务器发生宕机而没来得及将锁的超时时间设置,导致死锁的问题,currentTime+timeout(A节点获得锁时的当前时间+设置的锁超时时间)将派上用场。
B节点获取锁时的当前时间>A节点获得锁时的当前时间+设置的锁超时时间:说明这个key(锁)本应该释放的,但是发生意外没有释放,B节点应该能获得锁(重新设置key对应的value值)。
B集群节点重新设置redis的key对应的value值,可是在重新设置新值之前可能会被其他集群节点抢先一步被重新设置redis的key对应value值。这就说明锁被其他集群节点拥有了。
所以我们需要判断B集群节点重新设置redis的key对应的value值之前有没被其他集群节点改动过,只有没有被其他集群节点改动过,才说明该锁还没有被其他集群节点占有,我依旧拥有该锁的权利。
当然,B集群节点重新设置redis的key对应的value值之前过期了,那必定是可以占有该锁。

    @Scheduled(cron="0 */1 * * * ?")
    public void closeOrderTaskV3(){
        log.info("关闭订单定时任务启动");
        long lockTimeout = Long.parseLong(PropertiesUtil.getProperty("lock.timeout","5000"));
        Long setnxResult = RedisShardedPoolUtil.setnx(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,String.valueOf(System.currentTimeMillis()+lockTimeout));
        if(setnxResult != null && setnxResult.intValue() == 1){
            closeOrder(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        }else{
            //未获取到锁,继续判断,判断时间戳,看是否可以重置并获取到锁
            String lockValueStr = RedisShardedPoolUtil.get(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
            if(lockValueStr != null && System.currentTimeMillis() > Long.parseLong(lockValueStr)){
                String getSetResult = RedisShardedPoolUtil.getSet(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,String.valueOf(System.currentTimeMillis()+lockTimeout));
                //再次用当前时间戳getset。
                //返回给定的key的旧值,->旧值判断,是否可以获取锁
                //当key没有旧值时,即key不存在时,返回nil ->获取锁
                //这里我们set了一个新的value值,获取旧的值。
                if(getSetResult == null || (getSetResult != null && StringUtils.equals(lockValueStr,getSetResult))){
                    //真正获取到锁
                    closeOrder(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
                }else{
                    log.info("没有获取到分布式锁:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
                }
            }else{
                log.info("没有获取到分布式锁:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
            }
        }
        log.info("关闭订单定时任务结束");
    }

    private void closeOrder(String lockName){
        RedisShardedPoolUtil.expire(lockName,5);//有效期50秒,防止死锁
        log.info("获取{},ThreadName:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,Thread.currentThread().getName());
        int hour = Integer.parseInt(PropertiesUtil.getProperty("close.order.task.time.hour","2"));
        iOrderService.closeOrder(hour);
        RedisShardedPoolUtil.del(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        log.info("释放{},ThreadName:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,Thread.currentThread().getName());
        log.info("===============================");
    }

    //RedisShardedPoolUtil
     public static Long setnx(String key,String value){
        ShardedJedis jedis = null;
        Long result = null;

        try {
            jedis = RedisShardedPool.getJedis();
            result = jedis.setnx(key,value);
        } catch (Exception e) {
            log.error("setnx key:{} value:{} error",key,value,e);
            RedisShardedPool.returnBrokenResource(jedis);
            return result;
        }
        RedisShardedPool.returnResource(jedis);
        return result;
    }
上一篇 下一篇

猜你喜欢

热点阅读