13天 -- Redis 中如何实现分布式锁? Redis 的 Red Lock 是什么?你了解吗? Redis 实现分布式锁时可能遇到的问题有哪些?

Redis 中如何实现分布式锁?

1. 基于 SETNX 命令实现

  • 原理

    • 使用 SETNX 命令(SET if Not eXists)尝试获取锁。如果键不存在,则设置成功,表示获取锁;如果键已存在,则设置失败,表示锁已被其他客户端持有。
    • 为锁设置过期时间(EXPX 参数),以防止死锁。
  • 实现代码

    import redis.clients.jedis.Jedis;
    
    public class RedisLockExample {
        public static void main(String[] args) {
            Jedis jedis = new Jedis("localhost", 6379);
            String lockKey = "lock_key";
            String requestId = UUID.randomUUID().toString();
    
            // 尝试获取锁
            String result = jedis.set(lockKey, requestId, "NX", "EX", 10);
            if ("OK".equals(result)) {
                try {
                    // 执行业务逻辑
                } finally {
                    // 释放锁
                    if (requestId.equals(jedis.get(lockKey))) {
                        jedis.del(lockKey);
                    }
                }
            } else {
                // 获取锁失败,重试或返回
            }
        }
    }
    

2. 基于 Redisson 客户端实现

  • 原理

    • Redisson 是一个基于 Java 的 Redis 客户端,提供了高级的分布式锁功能。
    • 使用 RLock 接口实现分布式锁,支持可重入锁、锁续期(Watch Dog)等功能。
  • 实现代码

    import org.redisson.Redisson;
    import org.redisson.api.RLock;
    import org.redisson.api.RedissonClient;
    import org.redisson.config.Config;
    
    public class RedissonLockExample {
        public static void main(String[] args) {
            Config config = new Config();
            config.useSingleServer().setAddress("redis://localhost:6379");
            RedissonClient redisson = Redisson.create(config);
    
            RLock lock = redisson.getLock("myLock");
            try {
                // 尝试获取锁,最多等待 10 秒,锁持有时间为 10 秒
                if (lock.tryLock(10, 10, TimeUnit.SECONDS)) {
                    try {
                        // 执行业务逻辑
                    } finally {
                        // 释放锁
                        lock.unlock();
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
    

3. 基于 RedLock 算法实现

  • 原理

    • RedLock 算法通过多个独立的 Redis 主节点来实现分布式锁,确保即使部分节点故障,锁仍然安全。
    • 客户端尝试从多个 Redis 主节点获取锁,只有在大多数节点(超过半数)获取成功时,才认为获取锁成功。
  • 实现步骤

    1. 客户端获取当前时间戳 T1
    2. 依次向多个 Redis 主节点发起加锁请求,每个请求设置超时时间。
    3. 如果在大多数节点上加锁成功,则认为获取锁成功。
    4. 如果获取锁失败,释放已获取的锁。
  • 实现代码

    import org.redisson.Redisson;
    import org.redisson.api.RLock;
    import org.redisson.api.RedissonClient;
    import org.redisson.config.Config;
    
    public class RedLockExample {
        public static void main(String[] args) {
            Config config = new Config();
            config.useSingleServer().setAddress("redis://localhost:6379");
            RedissonClient redisson = Redisson.create(config);
    
            RLock lock = redisson.getLock("myLock");
            try {
                if (lock.tryLock(10, 10, TimeUnit.SECONDS)) {
                    try {
                        // 执行业务逻辑
                    } finally {
                        // 释放锁
                        lock.unlock();
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
    

4. 基于 Lua 脚本实现

  • 原理

    • 使用 Lua 脚本确保加锁和解锁操作的原子性,避免竞态条件。
  • 实现代码

    import redis.clients.jedis.Jedis;
    
    public class LuaLockExample {
        public static void main(String[] args) {
            Jedis jedis = new Jedis("localhost", 6379);
            String lockKey = "lock_key";
            String requestId = UUID.randomUUID().toString();
    
            // 加锁 Lua 脚本
            String lockScript = "if redis.call('exists', KEYS[1]) == 0 then " +
                                "redis.call('hset', KEYS[1], ARGV[1], 1);" +
                                "redis.call('pexpire', KEYS[1], ARGV[2]);" +
                                "return nil;" +
                                "elseif redis.call('hexists', KEYS[1], ARGV[1]) == 1 then " +
                                "redis.call('hincrby', KEYS[1], ARGV[1], 1);" +
                                "redis.call('pexpire', KEYS[1], ARGV[2]);" +
                                "return nil;" +
                                "end;" +
                                "return redis.call('pttl', KEYS[1]);";
    
            // 释放锁 Lua 脚本
            String unlockScript = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then " +
                                  "return nil;" +
                                  "end;" +
                                  "local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1);" +
                                  "if counter > 0 then " +
                                  "redis.call('pexpire', KEYS[1], ARGV[2]);" +
                                  "return 0;" +
                                  "else " +
                                  "redis.call('del', KEYS[1]);" +
                                  "return 1;" +
                                  "end;" +
                                  "return nil;";
    
            // 加锁
            jedis.eval(lockScript, Collections.singletonList(lockKey), Arrays.asList(requestId, "10000"));
    
            try {
                // 执行业务逻辑
            } finally {
                // 释放锁
                jedis.eval(unlockScript, Collections.singletonList(lockKey), Arrays.asList(requestId, "10000"));
            }
        }
    }
    

总结

  • 简单实现:使用 SETNX 命令结合过期时间,适用于简单场景。
  • 高级实现:使用 Redisson 客户端或 RedLock 算法,适用于复杂场景,提供更高的可靠性和安全性。
  • 原子性保证:使用 Lua 脚本确保加锁和解锁操作的原子性,避免竞态条件。

根据具体业务需求和场景选择合适的实现方式。

Redis 的 Red Lock 是什么?你了解吗?

1. Red Lock 的核心思想

Red Lock 的核心思想是通过多个独立的 Redis 实例来实现分布式锁,以提高锁的可靠性和安全性。具体步骤如下:

  • 多个独立的 Redis 实例:假设部署了 5 个独立的 Redis 主节点,这些节点之间没有主从关系,也不进行数据同步。
  • 客户端尝试加锁:客户端依次向这些 Redis 实例发送加锁请求,每个请求设置一个较短的超时时间(远小于锁的有效时间)。
  • 多数节点加锁成功:如果客户端在大多数节点(例如 3 个或更多)上成功获取锁,并且获取锁的总时间小于锁的有效时间,则认为加锁成功。
  • 释放锁:客户端在任务完成后,向所有 Redis 实例发送释放锁的请求,无论这些节点是否成功加锁。

2. Red Lock 的实现步骤

  1. 获取当前时间戳:客户端获取当前时间戳 T1
  2. 依次向 Redis 实例发起加锁请求:客户端依次向多个 Redis 实例发送加锁请求,每个请求设置超时时间(例如 50 毫秒)。如果某个节点加锁失败(如锁被占用或网络超时),立即向下一个节点发起请求。
  3. 计算获取锁的总耗时:如果客户端在大多数节点上成功获取锁,获取当前时间戳 T2,计算总耗时 T2 - T1
  4. 判断加锁是否成功:如果总耗时小于锁的有效时间,则加锁成功;否则加锁失败,客户端需要释放已获取的锁。
  5. 释放锁:客户端在任务完成后,向所有 Redis 实例发送释放锁的请求,无论这些节点是否成功加锁。

3. Red Lock 的优势

  • 高可靠性:通过多个独立的 Redis 实例,避免了单点故障的问题,即使部分节点宕机,锁仍然有效。
  • 容错性:不依赖单点 Redis 实例,避免了单点故障导致锁不可用的问题。
  • 时间一致性:使用锁的有效期来限制锁的持有时间,即使客户端异常退出,锁也会自动释放。

4. Red Lock 的劣势

  • 复杂性:需要维护多个 Redis 实例,部署成本较高。
  • 网络延迟:加锁和释放锁的操作需要同时与多个 Redis 实例通信,延迟较单实例锁更高。
  • 时钟漂移:Red Lock 假设 Redis 节点的时钟一致。如果节点时钟漂移较大,可能导致锁的有效期计算错误。

5. Red Lock 的实际应用

Red Lock 适用于需要高可靠性和高可用性的分布式锁场景,例如分布式事务、定时任务调度、库存扣减等。在实际应用中,可以根据业务需求和系统架构,灵活地运用 Red Lock 来保护关键资源的访问。

6. Red Lock 的实现示例

以下是一个简单的 Red Lock 实现示例(使用 Lua 脚本):

-- 加锁脚本
if redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) then
    return 1
else
    return 0
end

-- 解锁脚本
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

7. 注意事项

  • 锁有效期与任务执行时间:锁的有效期应比预估任务执行时间稍长,防止锁过期导致多个客户端同时持有锁。
  • 多数节点原则:Red Lock 假定大多数节点是正常工作的。如果超过一半节点同时故障,加锁可能失效。
  • 网络分区:如果发生网络分区,某些 Redis 节点不可用,加锁和解锁的成功率会降低。

Red Lock 是一种在分布式系统中实现高可靠分布式锁的解决方案,通过多个独立的 Redis 实例协同工作,确保锁的安全性和可靠性。

Redis 实现分布式锁时可能遇到的问题有哪些?

1. 锁过期与业务未完成(锁续期问题)

  • 问题:客户端业务处理时间超过锁过期时间,导致锁提前释放,其他客户端获得锁,引发数据不一致。
  • 解决方案
    • 看门狗机制:启动后台线程定期(如每隔10秒)续期锁的过期时间。
    • 合理设置过期时间:根据业务处理耗时动态调整锁的过期时间(如预估时间+缓冲时间)。

2. 锁误删(非原子性释放)

  • 问题:非原子性操作(先 GETDEL)可能导致释放其他客户端的锁。
  • 解决方案:使用 Lua 脚本保证验证与删除的原子性。

3. 主从切换导致锁丢失

  • 问题:Redis 主节点宕机后,从节点可能未同步锁信息,新主节点上锁丢失。
  • 解决方案
    • RedLock 算法:向多个独立 Redis 实例申请锁,当多数节点(如5个中的3个)加锁成功时,认为锁获取成功。
    • 集群模式:使用 Redis Cluster 或 Sentinel 保障高可用性。

4. 时钟漂移问题

  • 问题:如果 Redis 服务器的时钟向前跳跃,会使锁的 Key 过早超时失效。例如,客户端 1 获取到锁后,设置 Key 的过期时间是 10:02 分,但 Redis 服务器的时钟比客户端快了 2 分钟,那么 Key 可能在 10:00 就失效了。要是此时客户端 1 还没释放锁,就会出现多个客户端同时持有同一把锁的情况。
  • 解决方案:使用 NTP 服务同步服务器时钟,减少时钟漂移的影响。

5. 单点安全问题

  • 问题:当 Redis 集群采用单 Master 模式时,如果这台 Master 服务器宕机,所有客户端都可能无法获取到锁。为了提高可用性,会给 Master 引入 Slave 节点。但由于 Redis 的主从同步是异步进行的,可能会出现这样的情况:客户端 1 设置好锁后,Master 突然挂掉,Slave 晋升为新的 Master,而由于异步复制的特性,客户端 1 设置的锁丢失了。这时,客户端 2 也能成功设置锁,导致客户端 1 和 2 同时拥有同一把锁。
  • 解决方案:使用 Redis Cluster 或 Sentinel 保障高可用性。

6. 锁阻塞问题

  • 问题:在实际业务中,为了实现一个分布式锁,独立部署多个 Redis 实例,整体成本直线上升。实现复杂,整体加锁效率有所降低。
  • 解决方案:使用 Redis Cluster 或 Sentinel 保障高可用性,减少独立部署多个 Redis 实例的成本。

7. 锁重入问题

  • 问题:在秒杀场景中,假设库存有 2000 个商品可以供用户秒杀。为了防止出现超卖,通常会对库存加锁。如果有 1W 的用户竞争同一把锁,显然系统吞吐量会非常的低。
  • 解决方案:将库存分段,例如分为 100 段,每段有 20 个商品可以参与秒杀。在秒杀过程中,先获取用户 ID 的 Hash 值,然后除以 100 取模。模为 1 的用户访问第 1 段库存,模为 2 的用户访问第 2 段库存,以此类推,最后模为 100 的用户访问第 100 段库存。这样在多线程环境中,就可以大大减少锁的冲突。

8. 锁超时失效或锁提前过期问题

  • 问题:如果线程 A 加锁成功,但由于某些原因执行耗时过长,超过锁的超时时间,这时 Redis 会自动释放线程 A 加的锁。但线程 A 还没执行完,还在对共享数据进行访问。如果此时线程 B 尝试加锁,那么也可以加锁成功,并对共享数据进行访问。这样就出现了多个线程对共享数据进行操作的问题。
  • 解决方案:如果达到了超时时间,但业务代码还没执行完,则需要给锁自动续期。可以使用 TimerTask 类来实现自动续期的功能,比如获取锁之后自动开启一个定时任务,每隔 10 秒自动刷新一次过期时间。

9. 原子操作问题

  • 问题:使用 SETNX 命令加锁和设置锁的超时时间是分开的,并非原子操作。假如加锁成功,但设置超时时间失败了,该 lockKey 就变成永不失效。极端情况下,获取锁的客户端如果宕机了,那么就没法释放锁了。
  • 解决方案:使用 SET 命令的 NXPX 选项,可以在一个原子操作中完成获取锁和设置过期时间的两个步骤。

10. 加锁失败的处理

  • 问题:如果有两个线程同时上传文件到 SFTP,上传文件前先要创建目录,假设两个线程需要创建的目录名都是当天的日期。如果不做任何控制,直接并发创建目录,第二个线程必然会失败。如果加一个 Redis 分布式锁后在目录不存在时才进行创建,那么第二个请求加锁失败时,是返回失败,还是返回成功?
  • 解决方案:在规定的时间内,通过自旋 + 睡眠去尝试加锁。比如在规定的 500 毫秒内,不断自旋尝试加锁。如果成功,则直接返回。如果失败,则睡眠 50 毫秒,再发起新一轮加锁的尝试。如果到了超时时间还未成功加锁,则直接返回失败。

11. 锁竞争问题

  • 问题:在高并发场景下,多个客户端同时尝试获取同一把锁,可能导致锁竞争激烈,影响系统性能。
  • 解决方案:使用读写锁或分段锁,减少锁的冲突。例如,将库存分段,每个段使用独立的锁,这样可以提高系统的并发性能。

12. RedLock 算法的问题

  • 问题:RedLock 算法虽然提高了锁的可靠性,但实现过程复杂,且依赖多个 Redis 实例的时钟同步。如果某个实例的时钟发生漂移,可能导致锁提前失效,进而引发并发问题。
  • 解决方案:使用 Redis Cluster 或 Sentinel 保障高可用性,减少对多个 Redis 实例的依赖。

通过以上解决方案,可以有效应对 Redis 分布式锁在实际应用中可能遇到的问题,提高系统的稳定性和可靠性。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值