分布式锁(二)--Redis实现分布式锁

一、背景:

前面了解了分布式锁,做了最简单的入门了解,分布式锁(一)--基础

企业开发中使用最多的分布式锁,是Redis分布式锁,主要考虑到性能,以及Redis使用率高于ZK。

二、Redis实现可靠性分布式锁的条件:

互斥性。在任意时刻,只有一个客户端能持有锁。

不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。

加锁和解锁必须是同一个客户端。

三、加锁思路:

set lockKey value NX PX 3000

lockKey:表示Redis的key。
value:value一定要是随机值。
NX:如果不存在这个key,可以设置成功,如果成功,设置失败,Redis返回nil。
PX:过期时间到了之后,这个key就会自动删除。
timeout(3000):过期时间为3s。
如果没有拿到锁,需要每隔500ms或者其他时间间隔尝试获取锁。

四、释放锁思路:

一般可以用lua脚本删除,判断value一样才删除:

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

1、为什么使用lua脚本:

Redis 将整个脚本作为一个原子执行, 无需担心并发, 也就无需事务。

2、为什么使用随机值呢?

如果客户端A获取到了锁,但是阻塞了很长时间才执行完,此时可能已经自动释放锁了。
此时客户端B已经获取到了这个锁,要是直接删除key的话会有问题,所以用随机值加上面的lua脚本来释放锁。
当客户端A执行完成去删除锁的时候,会判断value值才能够去删除,保证不会误删锁。

五、代码实现:

    public static final String LOCK_SUCCESS = "OK";//加锁成功
    
    public static final String SET_IF_NOT_EXIST = "NX";
    
    public static final String SET_WITH_EXPIRE_TIME = "PX";
    
    public static final Long RELEASE_SUCCESS = 1L;
    
    public class RedisUtils {
    
        @Autowired
        JedisPool jedisPool;
    
        /**
         * 尝试获取分布式锁
         */
        public boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) {
            Jedis jedis = jedisPool.getResource();
            String result = jedis.set(lockKey, requestId, RedisConstant.SET_IF_NOT_EXIST, RedisConstant.SET_WITH_EXPIRE_TIME, expireTime);
            if (StringUtils.equals(result, RedisConstant.LOCK_SUCCESS))
                return true;
            return false;
        }
    
        /**
         * 释放分布式锁
         */
        public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
    
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
    
            if (RedisConstant.RELEASE_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        }
    }

这里可以通过while设置进行尝试获取锁,如果失败,sleep一段时间,直到达到最大过期时间。

KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。

六、存在的问题:

key超时之后业务并没有执行完毕但是会自动释放锁,这样就会导致并发问题。

生产环境Redis是通常都是cluster部署,当key写入master之后,master数据还没来及同步到slave,master直接挂了,这样也会出现并发问题。

七、Redisson实现分布式锁:

1、背景:

具体api如何使用,自行查询官方文档,GitHub地址

2、加锁:看门狗

加锁和解锁过程都是通过lua脚本实现。

当加锁之后,Redisson会开启watchdog线程。

每隔10s是否还被客户端持有,如果是,将过期时间重置为30s。

3、解锁:

同样的,解锁也是通过lua脚本实现。

正常情况下,程序执行完成解锁。

如果加锁的客户端直接挂了,那么Redisson框架启动的watchdog线程肯定也挂了,到了30s就会过期。

4、Redis集群故障,分布式锁是否有效?

这个问题很难解决,除非修改redis和Redisson框架的源码。

保证就是这个分布式锁同时写入master和slave之后,才算加锁成功。

但是这个问题是可以容忍的,Redis集群由中间件团队保证高可用性,出问题的概率很低的,如果出现问题很快解决,最多人工处理一些数据。

5、RedLock算法

通常Redis肯定不是单机的,至少也是Master-Slave的架构,或者Sentinel、Cluster能够保证高可用。

具体实现:
  • 例如:线上使用的Cluster架构,有5个Master节点,每个master挂着2个slave节点。
  • 获取当前时间戳,单位是ms。
  • 轮流尝试在每个master节点上创建锁,过期时间设置很短,一般就几十ms。
  • 尝试在大多数节点建立一个锁,比如5个节点就要要求是3个节点(n /2 + 1)。
  • 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了。
  • 要是锁建立失败了,那么依次删除这个锁。
  • 只要别人建立一把分布式锁,就不断的轮询区尝试获取锁。
  • 但是这种算法可能存在很多问题,无法保证加锁的过程一定正确。

posted @ 2022-01-24 15:52  Diamond-Shine  阅读(177)  评论(0编辑  收藏  举报