文章

面试题学习笔记 | Redis 分布式锁

实现分布式锁是 Redis 的一个常见用途

在单体架构中,为了解决并发问题,我们通常会在本地使用锁来让线程互斥访问资源,从而避免因原子性、有序性、可见性问题而导致的并发安全隐患。然而,在分布式架构下,传统的本地锁无法跨多个应用实例进行资源同步管理,因为这些实例运行在不同的机器上,彼此难以直接访问对方的锁状态。为了解决这一问题,我们需要借助中间件来实现分布式锁,使分布在不同机器上的实例能够通过访问同一个共享存储来获取锁,以此实现分布式系统的并发控制

Redis 由于基于内存,具备高效的存取性能,成为实现分布式锁的一种常见选择。以下是几个与 Redis 分布式锁相关的常见面试问题

Redis 中如何实现分布式锁?

一个最简单的锁通常有两种状态:无锁有锁。相应的基本操作有两种:加锁和解锁,分别将锁的状态从无锁变为有锁,或者从有锁变为无锁

Redis 是一个基于键值对(key-value)的 NoSQL 数据库,我们可以将一个特殊的 key 作为锁:当 key 不存在时,表示无锁状态;当 key 存在时,表示锁已被持有。因此,我们可以通过在 Redis 中设置和删除 key 来实现锁的加锁和解锁操作

加锁的实现

加锁的核心思路很简单,Redis 提供的 SETNX(SET if Not Exists)命令可以用于加锁。SETNX 命令只有在 key 不存在时才会设置该 key 的值,并返回 1,如果 key 已经存在,则不会进行任何操作,返回 0。尽管这个思路看起来简单,但在实际实现中需要进一步细化

  • 设置锁的过期时间
    在分布式环境下,如果一个服务在加锁后宕机或者由于其他原因未能释放锁,就会导致其他服务无法获取锁,形成死锁。因此,锁应当具备自动过期机制,避免锁长期占用而无法释放。在 Redis 中,我们可以使用 SET lock_key value EX <expiration_time> NX 命令来同时设置 key 的值并指定过期时间,这样一旦锁超时,Redis 就会自动释放锁

  • 唯一标识锁的持有者
    如果锁的值只是一个随意的字符串,那么可能会导致锁被其他客户端误释放。例如,假设以下场景:

    1. 服务 A 获取锁,开始执行业务逻辑

    2. 由于执行时间过长,锁的过期时间到了,Redis 自动删除了锁

    3. 服务 B 成功获取了锁,并开始执行自己的逻辑

    4. 服务 A 业务执行完毕,尝试释放锁,导致服务 B 的锁被误删除

    为避免这种情况,每个服务在加锁时应该存储一个唯一标识符,例如 UUID,并在释放锁时检查该标识符是否匹配,确保只有持有锁的服务能够释放锁。因此,加锁的最终实现方式如下:

    SET lock_key [唯一值] EX [过期时间] NX
    

解锁的实现

在解锁时,需要先判断当前锁是否是自己持有的,只有满足条件的情况下才能进行释放。因此,解锁的正确操作流程如下:

  1. 先获取锁的值,判断是否与自己持有的唯一标识符匹配

  2. 如果匹配,则执行删除操作

由于这两个步骤涉及多个操作,可能会受到并发影响,因此需要保证其原子性。在 Redis 中,我们可以使用 Lua 脚本确保操作的原子性,如下所示:

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

这段脚本先通过 GET 获取 key 的值,并与当前服务的唯一标识符进行对比,只有匹配时才执行 DEL 删除操作,从而确保锁的释放是安全的


Redisson 实现分布式锁

在实际开发中,通常不会手动实现 Redis 分布式锁,而是使用成熟的 Redis 客户端工具,例如 Redisson。Redisson 的基本思路与手动实现类似,但在某些细节上进行了优化:

  • 基于哈希结构的可重入锁
    在某些场景下,同一个线程可能需要多次获取同一把锁,这种需求被称为可重入锁。Redisson 通过哈希结构维护锁的 value 值,记录当前锁的持有次数。当 value = 0 时表示锁已释放,否则每次获取锁都会使 value 加 1,释放锁时则减少 1,直到 value = 0 时才真正删除锁

  • 自动续期机制(Watchdog 机制)
    由于锁的过期时间是固定的,如果业务逻辑执行时间超过锁的超时时间,锁可能会被自动释放,导致其他线程错误地获取到锁。为了避免这种情况,Redisson 采用 看门狗机制(Watchdog) 实现锁的自动续期:

    • 定时续期:如果锁没有被主动释放,Redisson 会周期性地向 Redis 发送心跳请求来续期锁,默认每 10 秒发送一次,每次续期 30 秒

    • 自动释放:如果持有锁的客户端宕机,心跳机制也会停止,锁会在超时时间后自动释放


Redis 的 Red Lock(红锁)

在分布式系统中,如果存储分布式锁的 Redis 节点宕机,可能会导致锁信息丢失,造成数据不一致。为了解决这个问题,Redis 设计了一种**Red Lock(红锁)**机制

红锁要求 Redis 以集群模式部署,官方建议至少使用 5 个独立的 Redis 实例(不需要主从或哨兵)。加锁流程如下:

  1. 客户端获取当前时间 t1

  2. 依次向 n 个 Redis 实例发送加锁请求,每个节点的请求都有超时时间(远小于锁的总过期时间)

  3. 如果超过半数(n/2 + 1)的节点成功加锁,则计算耗时 t2 - t1,若小于锁的总过期时间,则加锁成功

  4. 若加锁成功,执行业务逻辑;否则,依次向所有节点发送解锁请求

红锁的潜在问题

  • GC 导致 STW(Stop-The-World):如果持有锁的进程发生长时间 GC,锁可能会过期,而 GC 结束后进程仍认为自己持有锁,可能造成并发问题

  • 时钟漂移:不同 Redis 实例的系统时间不一致可能导致锁过早或过晚释放,影响一致性

由于红锁的实现复杂度较高,且需要多个 Redis 实例,一般在业务中仍然会采用主从+哨兵的方式来实现分布式锁


总结

Redis 分布式锁在实际开发中有多种实现方式,从最基本的 SETNX + 过期时间,到基于 Redisson 提供的可重入锁与自动续期机制,甚至更复杂的 Red Lock 方案。在选择方案时,需要根据业务需求权衡性能、可用性和一致性

License:  CC BY 4.0