基于redis实现分布式锁的几种方式与问题

5 篇文章 0 订阅
2 篇文章 0 订阅

业务场景

在费用报销的业务场景下,可报销科目与科目的可报销金额全都存在redis中,比如下面这样:
在这里插入图片描述
现在我们需要通过程序来实现报销的逻辑。

初始版

代码样例

@PostMapping("/reimbursement")
public String reimbursement(@RequestBody ReimbursementVO vo) {
     String jineStr = stringRedisTemplate.opsForValue().get(vo.getKmcode());
     if (jineStr == null) {
         logger.info("没钱了");
         return "没钱了";
     }
     int jine = Integer.parseInt(jineStr);
     if (jine < vo.getJine()) {
         logger.info(vo.getKmcode() + "的剩余费用不足" + vo.getJine());
         return vo.getKmcode() + "的剩余费用不足" + vo.getJine();
     }
     stringRedisTemplate.opsForValue().set(vo.getKmcode(), String.valueOf(jine- vo.getJine()));
     logger.info("报销成功");
     return "报销成功";
}

代码解析

在上面的这段逻辑中,没有锁的保护。在单机多线程的情况下会出现超额报销的问题, 比如一共5000块,实际报销了1000块,但是依旧剩余4000多的问题。

单机线程安全版

代码样例

private ConcurrentHashMap<String,Object> lockMap = new ConcurrentHashMap<>();
@PostMapping("/reimbursement2")
public String reimbursement2(@RequestBody ReimbursementVO vo) {
    Object o = lockMap.computeIfAbsent(vo.getKmcode(), k -> new Object());
    synchronized (o){
        return reimbursement(vo);
    }
}

代码解析

使用分段锁机制,在保证效率的情况下保证单个服务内部线程安全,单体服务下不会出现超额报销的问题。 但是在集群环境下还是会出现超额报销的问题,所以需要一定的手段来保证分布式环境下的线程安全。

使用redis实现分布式锁的第一版

代码样例

@PostMapping("/reimbursement3")
public String reimbursement3(@RequestBody ReimbursementVO vo) {
    String kmcode = vo.getKmcode();
    String lockKey = "lock:"+kmcode;
    String lockValue = stringRedisTemplate.opsForValue().get(lockKey);
    if (lockValue == null) {
        stringRedisTemplate.opsForValue().set(lockKey, "1");
        try {
            return reimbursement(vo);
        }finally {
            stringRedisTemplate.delete(lockKey);
        }
    }else {
        return "当前报销人数较多,请稍后再试";
    }
}

代码解析

在上面的方案中使用redis中间件同步锁的状态,但是存在一定的问题:

  1. redis的get和set分成两行,无法保证原子性,在并发场景下会存在线程安全问题。 比如线程1 get出来的lockValue为null,此时线程1被挂起,线程2也过来get相同的lockKey,此时获取到的lockValue也是null。 所以线程1和线程2都可以加锁成功,也就导致了锁失效。
  2. 在redis中设置lockKey之后,并没有设置超时时间,假如一个服务加锁成功,但是因为某种原因异常宕机了, 此时任何其他服务都无法再针对相同的kmcode加锁成功了,也就形成了死锁

使用redis实现实现分布式锁的第二版

代码样例

@PostMapping("/reimbursement4")
public String reimbursement4(@RequestBody ReimbursementVO vo) {
    String kmcode = vo.getKmcode();
    String lockKey = "lock:"+kmcode;
    Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
    int tryCount = 1;
    while (!lock && tryCount<3){
        int randomTime = ThreadLocalRandom.current().nextInt(15);
        try {
            TimeUnit.MILLISECONDS.sleep(randomTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1",10,TimeUnit.SECONDS);
        tryCount++;
    }
    if (lock) {
        try {
            return reimbursement(vo);
        }finally {
            stringRedisTemplate.delete(lockKey);
        }
    }else {
        return "当前报销人数较多,请稍后再试";
    }
}

代码解析

方案3的优化版:

  • 使用redis的setnx命令保证当key不存在时才能设置成功同时设置了lockKey的超时时间, 不用担心加锁的服务异常宕机导致死锁发生。
  • 增加了适当的自旋机制,尽可能保证加锁的成功率。

但是这个方案依旧存在问题。假如服务A加锁成功,但是没能在10秒内完成业务逻辑处理,导致锁自动失效。 锁失效后,服务B成功加锁,此时服务A的业务逻辑处理完毕,执行锁删除操作,又会在服务B没有执行完毕时释放锁, 最坏的情况会导致锁完全失效。 在这里插入图片描述

使用redis实现实现分布式锁的第三版

代码样例

@PostMapping("/reimbursement5")
public String reimbursement5(@RequestBody ReimbursementVO vo) {
    String kmcode = vo.getKmcode();
    String lockKey = "lock:"+kmcode;
    String lockValue = UUID.randomUUID().toString();
    Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);
    int tryCount = 1;
    while (!lock && tryCount<3){
        int randomTime = ThreadLocalRandom.current().nextInt(15);
        try {
            TimeUnit.MILLISECONDS.sleep(randomTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue,10,TimeUnit.SECONDS);
        tryCount++;
    }
    if (lock) {
        try {
            return reimbursement(vo);
        }finally {
            if (lockValue.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
                stringRedisTemplate.delete(lockKey);
            }
        }
    }else {
        return "当前报销人数较多,请稍后再试";
    }
}

代码解析

上面的代码中使用UUID作为lockValue,在锁释放时判断一下当前redis中的lockValue是不是本次加锁时存放的lockValue。如果是,再删除lockKey,如果不是,就不执行删除lockKey的逻辑。
但是本方案依旧存在一定的漏洞,假如服务A获取到锁后,在finally中判断当前的lockValue确实是本次加锁时的lockValue,判断成功后,发生FGC或其他原因导致redis中的lockKey过期。此时服务B加锁成功,服务B开始执行业务逻辑。然后A执行锁释放操作,依旧会导致锁失效问题。 只是现在出现锁失效的概率低了很多
在这里插入图片描述

使用redis实现分布式锁的第四版

代码样例

@PostMapping("/reimbursement6")
public String reimbursement6(@RequestBody ReimbursementVO vo) {
     String kmcode = vo.getKmcode();
     String lockKey = "lock:"+kmcode;
     RLock rLock = redissonClient.getLock(lockKey);
     rLock.lock();
     try {
         return reimbursement(vo);
     }finally {
         rLock.unlock();
     }
 }

代码解析

当然了,基于redis实现分布式锁的终极解决方案还是使用Redisson。 在Redisson使用lua脚本保证命令的原子性,同时使用watchDog实现锁续命

总结

  1. 如果使用单redis节点,同时项目中的并发量也不并不是太大,那完全可使用第三版来手写分布式锁(需要设置一个长一些的timeout来避免过期导致的锁失效问题)
  2. 基于redis实现的分布式锁在redis集群或哨兵模式下,都可能出现失效的问题
  3. 可以使用Redisson的RedLock来实现基于CP的分布式锁,但是红锁需要至少三个独立的Redis服务,同时性能下降的比较厉害。如果一定要保证CP,那应该考虑使用基于ZooKeeper实现的分布式锁。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值