Redis分布式锁解析:从SETNX到Redisson

目录

一、直接使用SETNX存在的问题

 二、优化方案(Redisson实现)

1. 锁续期机制(Watchdog)

2. 原子性保证(Lua脚本)

3. 可重入锁

4. 锁等待和重试机制

四、结合抢券业务场景的面试回答


一、直接使用SETNX存在的问题

  1. 锁超时自动释放问题

    1. 场景:在抢券业务中,用户A获取锁后开始扣减库存,但由于阻塞或异常等原因导致处理时间超过锁的过期时间(如30秒),锁自动释放。此时用户B也能获取锁开始抢券。如果用户A的处理突然恢复,会继续执行释放锁的代码,导致误删用户B的锁。

    2. 问题本质:锁的持有时间和业务执行时间无法精确匹配,检查和删除不是原子操作
  2. 锁不可重入

    1. 场景:抢券业务中如果存在嵌套调用(如先检查资格再扣库存),同一线程无法多次获取同一把锁。

    2. 问题本质:简单的SETNX实现不支持重入计数。

  3. 无失败重试机制

    1. 场景:高并发抢券时,大量用户同时竞争锁,失败的用户直接返回"抢券失败",没有自动重试机制,导致用户体验差且系统吞吐量低。


 二、优化方案(Redisson实现)

1. 锁续期机制(Watchdog,看门狗机制)

// Redisson自动实现的锁续期
RLock lock = redissonClient.getLock("coupon:lock:" + couponId);
try {
    lock.lock(); // 默认30秒,每10秒检查一次自动续期
    // 抢券业务逻辑
} finally {
    lock.unlock();
}

解决:后台线程定期检查业务是否完成,未完成则延长锁时间,防止业务未完成锁已过期。

2. 原子性保证(Lua脚本)

Redisson使用Lua脚本保证"获取锁+设置过期时间"、"检查锁值+删除锁"的原子性。

3. 可重入锁

锁类型简介:Redisson分布式锁类型及获取方式-CSDN博客

public void grabCoupon(Long userId, Long couponId) {
    RLock lock = redissonClient.getLock("coupon:lock:" + couponId);
    lock.lock();
    try {
        // 内部方法也需要获取同一把锁
        check(userId); 
        reduceCouponStock(couponId);
    } finally {
        lock.unlock();
    }
}

解决:可重入就是在内部判断是否是当前线程持有的锁。

4. 锁等待和重试机制

// 尝试获取锁,最多等待100秒,获取后30秒自动释放
boolean res = lock.tryLock(100, 30, TimeUnit.SECONDS);
if (res) {
    try {
        // 处理业务
    } finally {
        lock.unlock();
    }
}

通过Redisson实现的分布式锁,完美解决了SETNX的四大问题,在抢券等高并发场景下既能保证数据一致性,又能提供良好的用户体验。


四、结合抢券业务场景的面试回答

Q:如何控制Redis实现分布式锁的有效时长呢?

redis的SETNX指令不好控制这个问题。我们当时采用的是redis的一个框架Redisson实现的。

在Redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间。当锁住的一个业务还没有执行完成的时候,Redisson会引入一个看门狗机制

就是说,每隔一段时间就检查当前业务是否还持有锁。如果持有,就增加加锁的持有时间。当业务执行完成之后,需要使用释放锁就可以了。

还有一个好处就是,在高并发下,一个业务有可能会执行很快。客户1持有锁的时候,客户2来了以后并不会马上被拒绝。它会不断尝试获取锁,如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。

Q:Redisson实现的分布式锁是可重入的吗

是可以重入的。这样做是为了避免死锁的产生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计数上减一。

在存储上述计数数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数。

Redis分布式锁解析:从SETNX到Redisson_第1张图片

Q:Redisson实现的分布式锁能解决主从一致性的问题吗?

不能的。比如,当线程1加锁成功后,master节点数据会异步复制到slave节点,此时如果当前持有Redis锁的master节点宕机,slave节点被提升为新的master节点,假如现在来了一个线程2,再次加锁,会在新的master节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问题。

我们可以利用Redisson提供的红锁来解决这个问题,它的主要作用是,不能只在一个Redis实例上创建锁,应该是在多个Redis实例上创建锁,并且要求在大多数Redis节点上都成功创建锁,红锁中要求是Redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。

但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变得非常低,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁。

Q:如果业务非要保证数据的强一致性,这个该怎么解决呢?

Redis本身就是支持高可用的,要做到强一致性,就非常影响性能,所以,如果有强一致性要求高的业务,建议使用ZooKeeper实现的分布式锁,它是可以保证强一致性的。

你可能感兴趣的:(Redis篇,redis,分布式,数据库,缓存,java,后端,面试)