基于redis的分布式锁实现(上)

 阅读约 6 分钟

问题是如何出现的

回到那个经典的问题:下单减库存

try {
    if (lock.lock()) {
        Clothes clothes = clothesDao.getClothes(clothesId);
        if (null != clothes && clothes.getStock() > 0) {
            clothes.setStock(--clothes.getStock()); //减库存
            orderDao.insert(clothes);               //下单
        }
    }
} finally {
    lock.unlock();
}

在单机模式下没有问题,但是如果你的下单系统部署在多台机器上,比如A、B两台节点,那问题就出现了:A节点获取锁后还没解锁,B节点同样也能获取锁,因为他们的锁又不是同一个锁,都是从各自内部获取的,那么就会出现A节点获取到的库存是500,B节点获取到的库存也是500,那么同时减库存、下单、写入数据库,那库存就变成了499,而实际上是498,也就是出现了超卖

如何解决这个问题

开门见山的说啊,只要让各个节点争抢的锁是同一把不就完了嘛。像上面那种各节点从各自内部获取锁肯定不行的,必须让他们从外部去获取。那么redis作为单线程、高性能的一个数据库,就很适合用来做这种外部的锁了
即:将锁存储于redis数据库,各个节点从redis去争抢这个锁

分布式锁的四要素

互斥性

在任意时刻,只能有1个线程(1台节点)持有锁
在redis中你可以使用:set key value NX 命令来实现
key:就是你的锁的名字
value:下面会讲
NX:只有当key不存在时才执行这条命令,就是通过NX来实现互斥性的
相反的有XX,其代表只有当key存在时才执行这条命令

防止死锁

首先必须在finally块中解锁
然后锁必须有超时时间,防止某个节点获取锁之后系统崩溃导致永远不能解锁
那么上面的命令就要加两个东西了:set key value NX PX timeout
timeout:key的超时时间
PX:超时时间的单位为毫秒,EX为秒

解铃还须系铃人

你只能解你上的锁,你不能解别人上的锁
你可能会问:我可能会把别人上的锁解了吗?这是有可能的
在四要素的第二点讲了,这把锁为了防止死锁必须有超时时间,那么此时又会出现新问题:
假如你有3台节点A、B、C同时竞争这把锁,最先是A竞争到了,但是A还没解锁就卡顿了(不是崩溃),卡顿超过锁的超时时间,锁自动释放,此时B又抢到了锁,如果此时A恢复运行,去执行解锁操作,那这时A解的是谁上的锁?B嘛,因为A上的锁已经超时了,那A把B的锁解了,此时C就能抢到锁了,这就造成了一个很恐怖的现象:锁失效了!!!你辛苦写出来的分布式锁代码竟然跟没有一样,简直是薛定谔的锁!这时你该超卖的超卖,用户该投诉的投诉,你老板该赔钱的赔钱,你该被开除的开除,如果你是华为员工搞不好还要进去蹲几百天
未命名表单 (1).png
还记得上面那条命令中的value吗,解铃还须系铃人就要通过value来实现:
每个线程在执行这条命令前,必须获取一个全球唯一的序列号,假设为requestID,传入命令的value字段。注意:这个序列号必须是全球唯一的!千万不能出现两台节点获取到的序列号重复了!
那么有requestID,在解锁前就可以判断这把锁是不是我上的锁了:
if redis.call('get', lockKey) == requestID then return redis.call('del', lockKey) else return 0 end
上面这段代码是lua脚本,仔细看这段脚本实际上是有两个逻辑的:1判断是否是我上的锁,2解锁。不过redis在执行一段lua脚本时是以原子操作执行的,即执行完一个原子操作才会去执行其他的命令。也就是不会出现:A节点解锁时,判断这把锁是A上的,ok,此时还没解锁,锁就超时释放了,B节点瞬间抢到锁,那此时A去解锁解的就是B节点的锁,那么此时又出现了锁失效

redis集群下锁的容错性

如果你存放锁的redis是单机部署,那满足以上3个要素就可以了
但如果你的redis是主从架构这种,你就需要考虑:当你的主节点挂掉,锁也就没了,其他节点就在从节点获取到了一把新的锁,那此时就会有两台节点同时进入了加锁之后的逻辑,即也可能出现超卖
那么这种情况可以使用redis官方提供了java组件redisson来解决,redisson锁使用起来非常简单、方便,且非常强大,推荐使用

上锁的正确姿势

public static boolean lock(Jedis jedis, String requestID) {
    String result = jedis.set(LOCK_KEY, requestID, "NX", "PX", 3000);
    if ("OK".equals(result)) {
        return true;
    }
    return false;
}

解锁的正确姿势

public static boolean unlock(Jedis jedis, String requestID) {
    Long UNLOCK_SUCCESS = 1L;
    String UNLOCK_LUA_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    Object result = jedis.eval(UNLOCK_LUA_SCRIPT, Collections.singletonList(LOCK_KEY), Collections.singletonList(requestID));
    if (UNLOCK_SUCCESS.equals(result)) {
        return true;
    }
    return false;
}

总结

上述代码只是在redis单机部署时管用,如果redis多节点部署,如主从架构,那么你就要考虑当主节点挂掉时该怎么办,这里推荐使用redisson组件做分布式锁,因为它简单、方便,还解决了redis多节点的容错问题

阅读 272更新于 12月4日
推荐阅读

和你一起终生学习

0 人关注
2 篇文章
专栏主页
目录