redis一般锁和分布式锁的原理简析

redis分布式锁的实现方式

redis中存在一个指令,setnx,即set if not exist,如果此时key不存在则返回1,并对key赋值;如果key已经在redis中存在,则直接返回0,并且啥也不干。

一般锁原理

p1(线程1) setnx lock value返回1说明key不存在,获取锁成功
p2(线程2)也执行 setnx lock value返回0,获取锁失败,所以p2需要不断轮询setnx直到获取1
正常情况下p1 执行完了,del lock,即释放锁,然后p2获得锁

死锁问题与解决

上面场景考虑一种情况,如果进程p1获得锁后,断开了与 Redis 的连接(可能是p1挂掉,或者网络中断),如果没有有效的释放锁的机制,那么其p2会处于一直等待的状态,即出现“死锁”。

解决死锁方案一
上面在使用 SETNX 获得锁时,我们将键 lock 的值设置为锁的有效时间(即current+locktime),进程获得锁后,其他进程还会不断的检测锁是否已超时,如果超时,那么等待的进程也将有机会获得锁。

p1设置 setnx  lock current+locktime
因为 p1挂掉了,p1不会自动删除lock
p2和p3通过循环get获取value值,并和当前current值比较,lock过期状态可能被p2和p3同时获取到,所以p2,p3可能先后执行删除lock并设置新的锁 问题:p2和p3同时获得了锁

p2和p3同时获得了锁解决办法

p2 p3在获取到lockfool的过期状态时并不立刻设置新的锁,而是采用getSet,并比较get到的时间是否小于当前时间,如果是那么设置新的锁,否则说明别的线程已经设置新的锁,只能再次等待。
问题:假设p2先getSet,p3再getSet,那么p2的锁时间会被篡改。这个其实可以忍受
https://blog.csdn.net/lihao21/article/details/49104695

参考官网:https://redis.io/commands/setnx
带有java实现代码:https://juejin.im/post/5b737b9b518825613d3894f4  实现和代码

分布式与集群场景下的问题

上面方案看似比较完美了,但是在redis为集群和分布式场景下可能有问题

机器宕机问题:1master和1slave,lock对应的key刚刚写入master还没有同步给slave,master宕机,slave切换为master,因为key未同步,所以另一线程依旧可以获取锁。
分布式问题:redis有多个实例,并且不是在同一个集群中,那么如果请求一次有返回1就表示成功肯定不行,因为假设5个master,那么每个master都可以提供一个锁,显然是不对的哈

集群Redis的分布式锁RedLock

在Redis的分布式环境中,Redis 的作者提供了RedLock 的算法来实现一个分布式锁。

RedLock-加锁,RedLock算法加锁步骤如下

  1. 获取当前Unix时间,以毫秒为单位。
  2. 依次尝试从N个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
  3. 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
  4. 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
  5. 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。

RedLock-解锁
向所有的Redis实例发送释放锁命令即可,不用关心之前有没有从Redis实例成功获取到锁.

借鉴链接:https://juejin.im/post/5b737b9b518825613d3894f4

生产环境的分布式锁实例解析

代码如下:

/**
 * 利用Redis实现分布式锁
 */
@Component
public class LockUtil {

    private static final Long RELEASE_SUCCESS = 1L;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private static LockUtil thisUtil;

    @PostConstruct
    private void init() {
        thisUtil = this;
    }

    /**
     * 尝试获取分布式锁
     *
     * @param lockName            锁
     * @param requestId           请求标识(用来解锁,可以使用UUID)
     * @param expireSeconds       超期时间(单位:秒)
     * @return 是否获取成功
     */
    public static boolean tryLock(String lockName, String requestId, int expireSeconds) {

        final Boolean success = thisUtil.stringRedisTemplate.execute((RedisCallback<Boolean>) connection ->
                // 采用原生 API 来实现分布式锁
                connection.set(lockName.getBytes(),
                        requestId.getBytes(),
                        Expiration.from(expireSeconds, TimeUnit.SECONDS),
                        RedisStringCommands.SetOption.ifAbsent()));
        return success;
    }

    /**
     * 释放分布式锁
     *
     * @param lockName            锁
     * @param requestId           请求标识(用来解锁)
     * @return 是否释放成功
     */
    public static boolean unlock(String lockName, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        RedisScript<Long> luaScript = new DefaultRedisScript<>(script, Long.class);
        Long result = thisUtil.stringRedisTemplate.execute(luaScript, Collections.singletonList(lockName), requestId);

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

}

有锁超时时间,所以会自动释放锁。但是其问题如下:

 1. 不支持重入:同一个线程多次获取锁会失败,因为获取锁时只是简单地设置了一个值,而没有记录该锁是由哪个线程持有的。

2. 不支持高可用:如果 Redis 节点宕机,那么锁就会失效,从而导致并发问题。因此,需要使用 Redis 集群或者哨兵来保证高可用性。

3. 不支持续期:在获取锁后,如果业务逻辑执行时间超过了锁的过期时间,那么锁就会自动释放,而不是续期。这可能会导致其他线程获取到锁,从而导致并发问题。

4. 不支持阻塞式获取锁:该实现只支持非阻塞式获取锁,如果获取锁失败,就会立即返回。如果需要支持阻塞式获取锁,需要使用 Redis 的阻塞式命令 BLPOP 或者使用 Redlock 算法。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值