Redis源码解析:如何实现分布式锁?

在这里插入图片描述

Redis分布式锁加锁

对分布式锁不太了解的小伙伴,可以先看一下这篇文章
https://mp.weixin.qq.com/s/8fdBKAyHZrfHmSajXT_dnA

最开始的分布式锁是使用setnx+expire命令来实现的。setnx设置成功返回1,表示获取到锁,返回0,表示没有获取到锁,同时为了避免显示释放锁失败,导致资源永远也不释放,获取到锁后还会用expire命令设置锁超时的时间。

但有个问题就是setnx+expire不是原子性的,有可能获取到锁后,还没执行expire命令,也没执行释放锁的操作,服务就挂了,这样这个资源就永远也不会访问到了。

为了解决这个问题,Redis 2.6.12版本以后,为set命令增加了一系列的参数,我们此时用NX和PX参数就可以解决这个问题

所以现在Redis分布式锁的加锁命令如下

SET resource_name random_value NX PX 30000

NX只会在key不存在的时候给key赋值,PX通知Redis保存这个key 30000ms,当资源被锁定超过这个时间时,锁将自动释放

random_value最好是全局唯一的值,保证释放锁的安全性

# 设置成功返回OK
127.0.0.1:6379> SET lock1 100 NX PX 30000
OK
127.0.0.1:6379> SET lock1 100 NX PX 30000
(nil)

当某个key不存在时才能设置成功。这就可以让多个并发线程同时去设置同一个key,只有一个能设置成功。而其他线程设置失败,也就是获得锁失败

Redis分布式锁解锁

解锁不能简单的使用如下命令

del resource_name 

因为有可能节点A加锁后执行超时,锁被释放了。节点B又重新加锁,A正常执行到del命令的话就把节点B的锁给释放了。所以在解锁之前先判断一下是不是自己加的锁,是自己加的锁再释放,不是就不释放。伪代码如下

if (random_value.equals(redisClient.get(resource_name))) {
    del(key)
}

因为判断和解锁是2个独立的操作,不具有原子性,还是有可能会出问题。所以解锁的过程要执行如下的Lua脚本
,通过Lua脚本来保证判断和解锁具有原子性

if redis.call("get", KEYS[1]) == ARGV[1] then 
    return redis.call("del", KEYS[1]) 
else 
    return 0 
end

如果key对应的value一致,则删除这个key,通过这个方式释放锁是为了避免Client释放了其他Client申请的锁

到此你已经彻底理解了该如何实现一个分布式锁了,以及为什么要这样做的原因

加锁执行命令

SET resource_name random_value NX PX 30000

解锁执行脚本

if redis.call("get", KEYS[1]) == ARGV[1] then 
    return redis.call("del", KEYS[1]) 
else 
    return 0 
end

一个分布式锁的工具类写法如下

public class LockUtil {

    private static final String OK = "OK";
    private static final Long LONG_ONE = 1L;
    private static final String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    public static boolean lock(String lockKey, String requestId, long expire) {
        Jedis jedis = RedisPool.getJedis();
        SetParams setParams = new SetParams();
        setParams.nx().px(expire);
        return OK.equals(jedis.set(lockKey, requestId, setParams));
    }

    public static boolean unlock(String lockKey, String requestId) {
        Jedis jedis = RedisPool.getJedis();
        return LONG_ONE.equals(jedis.eval(script, 1, lockKey, requestId));
    }
}

Java代码中如何正确的加解锁?

错误的加解锁逻辑

public void workV1() {
    String lockKey = "testKey";
    String requestId = UUID.randomUUID().toString();
    if (LockUtil.lock(lockKey, requestId, 2000)) {
        try {

            // 执行业务逻辑

            LockUtil.unlock(lockKey, requestId);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这个例子的加解锁都有问题

解锁:当发生异常的时候,解锁逻辑并不会执行,所以需要将其放在finally语句中
加锁:将加锁的命令成功发到服务端并成功执行,但是获取响应超时,抛出异常,需要执行解锁的逻辑。因此需要将加锁的逻辑放在try语句中

正确的加解锁逻辑

public void workV2() {
    String lockKey = "testKey";
    String requestId = UUID.randomUUID().toString();
    try {
        if (LockUtil.lock(lockKey, requestId, 2000)) {

            // 执行业务逻辑

        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        LockUtil.unlock(lockKey, requestId);
    }
}

分布式锁在主从场景下会遇到的问题

上面我们只提到了在单机场景下分布式锁可能遇到的问题,但是在我们的应用中Redis肯定是采用Cluster集群或者哨兵集群来部署的

这2种部署方式又会造成哪些问题呢?

  1. 客户端A在master获取锁成功
  2. 锁的信息还没有同步到slave时,master宕机
  3. slave被选举为新的master
  4. 客户端B此时就能获取到客户端A持有的锁,违背了锁的定义

虽然这个概率很低,但我们必须承认这种问题客观存在,Redis作者为了解决这个问题,提出了RedLock的概念,用来解决这种问题。

RedLock的思想很简单,我们同时对N个master节点同时进行加锁(这N个master节点完全独立,不存在主从和集群),只有当N/2+1个节点都加锁成功,才算加锁成功
在这里插入图片描述
可以看到RedLock的实现效率比较低,另外很多业界的大佬对RedLock这种思想持怀疑态度,因此在生产环境中我们很少使用这种方案。要是真出现这种问题,人工修复足够了

如果生产环境中不允许这种问题的出现,你可以使用Zoookeeper来实现分布式锁,毕竟Redis是一个AP系统,而Zoookeeper是一个CP系统

参考博客

[1]https://mp.weixin.qq.com/s/8fdBKAyHZrfHmSajXT_dnA
[2]https://wudashan.cn/2017/10/23/Redis-Distributed-Lock-Implement/
红锁
[3]https://mp.weixin.qq.com/s/5E9_CSpnvf_KnDnKO0XjQg
[4]https://segmentfault.com/a/1190000039362581

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java识堂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值