Redis分布式锁实战:SETNX、Redisson与Lua脚本方案深度解析
2026/8/13 12:40:34 网站建设 项目流程

1. 项目概述与核心价值

在分布式系统里,锁是个绕不开的话题。想象一下,一个秒杀活动,库存就剩最后一件,成千上万的请求同时涌来,如果不用锁,很可能这件商品会被好几个人同时“抢到”,导致超卖。在单机时代,我们用synchronized或者ReentrantLock就能搞定,但到了微服务遍地走的今天,服务部署在多台机器上,这些本地锁就完全失效了。这时候,我们就需要一个所有服务节点都能“看见”和“遵守”的锁——分布式锁。

Redis,凭借其高性能、丰富的数据结构和原子操作,成为了实现分布式锁的热门选择。但“用Redis实现分布式锁”这句话说起来简单,背后却藏着不少门道。是直接用SETNX命令?还是用 Redisson 客户端?又或者用 Lua 脚本?不同的方案,在可靠性、性能、复杂度上天差地别。选错了,可能在测试环境风平浪静,一到线上高并发场景就“翻车”,出现锁失效、死锁或者性能瓶颈。

今天,我就结合自己踩过的坑和项目实战经验,来深度拆解三种主流的 Redis 分布式锁实现方案:基于 SETNX 和 EXPIRE 的基础命令方案基于 Redisson 的看门狗方案,以及基于 Lua 脚本的原子操作方案。我会把每种方案的原理、实现步骤、参数怎么设、坑在哪里、怎么避,都掰开揉碎了讲清楚。目标是让你不仅能“抄作业”把锁用起来,更能理解背后的设计权衡,在面对复杂场景时,能做出最适合自己业务的选择。

2. 三种方案的核心原理与设计权衡

在动手写代码之前,我们必须先搞清楚,一个合格的分布式锁应该满足哪些基本条件,以及 Redis 是如何利用其特性来满足这些条件的。这决定了我们方案设计的出发点。

2.1 分布式锁的四大核心要求

  1. 互斥性:这是锁最基本的功能,在任意时刻,只能有一个客户端持有锁。这是实现资源安全访问的基石。
  2. 防死锁:即使持有锁的客户端崩溃或者发生网络分区,锁也必须在某个时间点被自动释放,以避免系统永久阻塞。这通常通过给锁设置一个过期时间(TTL)来实现。
  3. 容错性:只要 Redis 集群中的大部分节点还存活,客户端就能正常获取和释放锁。这要求我们的方案不能过度依赖单个 Redis 实例的可用性。
  4. 解铃还须系铃人:锁只能由加锁的客户端来释放。防止其他客户端误删了不属于自己的锁,导致锁安全机制失效。

Redis 的SET命令配合NX(Not eXist)和PX(毫秒过期时间)参数,可以原子性地完成“不存在则设置值并设置过期时间”的操作,完美满足了互斥性和防死锁的前两个要求。这也是所有方案最底层的依赖。

2.2 方案选型背后的核心考量

为什么会有三种方案?因为它们分别解决了不同层面的问题:

  • 基础命令方案(SETNX + EXPIRE):这是最直观、最接近 Redis 原生命令的方案。它的优势是简单、轻量,不引入额外依赖。但它的致命缺陷在于,SETNXEXPIRE是两个独立的命令,不是原子操作。如果在执行完SETNX后、执行EXPIRE前客户端崩溃,就会导致锁永远不会过期,形成死锁。虽然可以用SET key value NX PX timeout这个原子命令来规避,但它仍然无法解决锁的自动续期和可重入性问题。
  • Redisson 方案:Redisson 是一个在 Redis 基础上实现的 Java 驻内存数据网格客户端。它提供的分布式锁RLock,封装了看门狗(Watchdog)机制。简单说,就是当你获取锁时,它会启动一个后台线程,在锁过期前的一段时间(默认是过期时间的1/3)去自动续期,直到你主动释放锁。这完美解决了业务执行时间可能超过锁过期时间的问题。同时,它原生支持可重入锁、公平锁等多种锁类型,功能非常强大。代价是引入了 Redisson 这个较重的客户端,需要理解其运行机制。
  • Lua 脚本方案:Redis 支持 Lua 脚本,且保证一个脚本在执行时是原子性的,不会被其他命令打断。我们可以将获取锁(判断+设置值+设过期时间)和释放锁(判断值是否为自己设置+删除)的逻辑,分别写成两个 Lua 脚本。这确保了整个操作的原子性,是比基础命令更严谨的实现。它比 Redisson 轻量,又弥补了基础命令的非原子性缺陷,是一种折中的、需要一定开发量的方案。

选择哪种方案,取决于你的业务场景:

  • 对可靠性要求极高,业务执行时间不确定,且团队愿意引入和维护一个功能丰富的客户端,选Redisson
  • 追求轻量,业务执行时间可控且短,希望代码完全可控,愿意自己实现原子性,选Lua 脚本
  • 用于简单的、非核心的同步控制,或者快速原型验证,可以谨慎使用基础命令(务必用原子SET命令)

3. 方案一:基于 SET 命令的基础实现与深度避坑

我们先从最基础,也是坑最多的方案开始。这里我直接使用原子命令SET resource_name random_value NX PX 30000来讲解,避免SETNX+EXPIRE的经典陷阱。

3.1 核心实现步骤与代码解析

假设我们使用 Spring Boot 和Spring-boot-starter-data-redis

1. 定义锁工具类方法:

@Component public class RedisLockUtil { @Autowired private StringRedisTemplate stringRedisTemplate; private static final String LOCK_PREFIX = “LOCK:”; private static final long DEFAULT_EXPIRE_TIME = 30000L; // 默认30秒 /** * 尝试获取分布式锁 * @param lockKey 锁的Key * @param requestId 请求标识(推荐使用UUID),用于保证“解铃还须系铃人” * @param expireTime 锁的过期时间(毫秒) * @return 是否获取成功 */ public boolean tryLock(String lockKey, String requestId, long expireTime) { String key = LOCK_PREFIX + lockKey; // 核心:使用SET命令的NX和PX参数进行原子操作 Boolean result = stringRedisTemplate.opsForValue() .setIfAbsent(key, requestId, expireTime, TimeUnit.MILLISECONDS); // Boolean.TRUE.equals() 是为了安全处理null值 return Boolean.TRUE.equals(result); } /** * 释放分布式锁 * @param lockKey 锁的Key * @param requestId 请求标识 * @return 是否释放成功 */ public boolean unlock(String lockKey, String requestId) { String key = LOCK_PREFIX + lockKey; String currentValue = stringRedisTemplate.opsForValue().get(key); // 判断当前锁的值是否与自己加锁时设置的值一致 if (requestId.equals(currentValue)) { // 一致,则删除Key,释放锁 Boolean delete = stringRedisTemplate.delete(key); return Boolean.TRUE.equals(delete); } // 不一致,说明锁可能已过期或被其他客户端持有,本次释放无效(也可能已超时) return false; } }

2. 业务中使用示例:

@Service public class OrderService { @Autowired private RedisLockUtil redisLockUtil; public void createOrder(String productId) { String lockKey = “CREATE_ORDER:” + productId; // 锁粒度细化到具体商品 String requestId = UUID.randomUUID().toString(); // 生成唯一客户端ID boolean locked = false; try { // 尝试获取锁,设置10秒过期 locked = redisLockUtil.tryLock(lockKey, requestId, 10000L); if (!locked) { throw new RuntimeException(“系统繁忙,请稍后重试”); } // 执行业务逻辑,例如:查询库存、创建订单、扣减库存 doBusiness(); } finally { // 务必在finally块中释放锁 if (locked) { redisLockUtil.unlock(lockKey, requestId); } } } }

3.2 关键细节与致命陷阱

这个方案看似简单,但每一步都暗藏玄机:

1. Value 必须使用唯一标识(如 UUID)这是实现“解铃还须系铃人”的关键。如果你用固定的值,比如“1”,考虑以下场景:

  • 客户端A获取锁成功,SET lock:order 1 NX PX 10000
  • 客户端A业务执行了12秒(超时了),锁被Redis自动删除。
  • 客户端B此时获取锁成功,SET lock:order 1 NX PX 10000
  • 客户端A终于执行完,调用unlock,它检查lock:order的值,发现是“1”,和自己当初设置的一样(但它不知道这个锁已经换主人了!),于是删除了锁。
  • 结果:客户端B的锁被客户端A意外释放,锁的互斥性被破坏。 使用 UUID 作为 value,可以绝对区分不同客户端的锁。

2. 释放锁的逻辑不是简单的delete,必须先getcompare and delete上面的unlock方法展示了这个过程。但请注意,这里的getdelete仍然是两个独立命令,不是原子操作!这会产生一个新的竞态条件:

  • 客户端A执行get(lockKey),得到自己的 requestIduuid-a
  • 就在此时,锁过期了(TTL变为0)。
  • 客户端B成功获取了锁,SET lock:order uuid-b NX PX ...
  • 客户端A继续执行delete(lockKey)
  • 结果:客户端B刚获得的锁又被客户端A删除了。 要解决这个问题,必须让getdelete原子性执行,这正是Lua 脚本方案要解决的核心问题之一。基础命令方案在此处存在理论上的安全隐患。

3. 过期时间(expireTime)的设置是门艺术

  • 设置过短:业务没执行完,锁就过期了,导致其他客户端进入,数据一致性被破坏。
  • 设置过长:客户端崩溃后,锁需要很长时间才能自动释放,系统恢复能力变差。
  • 最佳实践:这个时间应该略大于业务逻辑的平均执行时间。你需要通过监控和压测来确定这个值。例如,你的创建订单逻辑 99% 的请求在 800ms 内完成,那么你可以设置锁过期时间为 2-3 秒,提供一个安全缓冲。对于执行时间波动很大的业务,这个方案就力不从心了,此时Redisson 的看门狗才是更优解。

踩坑实录:早期我们有个对账任务,执行时间受数据量影响,从几分钟到几十分钟不等。当时图省事用了固定30秒过期的基础锁,结果经常出现任务执行到一半锁过期,另一个节点又启动了一个任务,导致数据被重复处理,对账结果完全混乱。后来不得不重构为 Redisson 锁。

4. 方案二:基于 Redisson 的高可靠可重入锁

当你被基础方案的原子性、过期时间难题困扰时,Redisson 就像一个开箱即用的瑞士军刀,它帮你处理了所有底层复杂性。

4.1 Redisson 锁的核心机制:看门狗(Watchdog)

这是 Redisson 锁最精髓的部分。其工作流程如下:

  1. 你通过redissonClient.getLock(“myLock”)获取一个RLock对象,并调用lock()tryLock()
  2. 在 Redisson 内部,它向 Redis 发送一个 Lua 脚本,原子性地完成“判断+加锁+设过期时间”(默认30秒)。
  3. 关键一步:加锁成功后,Redisson 会启动一个后台定时任务(看门狗),这个任务会每隔过期时间的 1/3(即10秒)去检查客户端是否还持有锁(通过判断 Redis 中该锁的 hash 结构中是否存在当前客户端的标识)。
  4. 如果客户端还持有锁(即业务没执行完),看门狗就会通过 Lua 脚本将锁的过期时间重置为初始值(30秒),实现自动续期
  5. 当客户端主动调用unlock()时,Redisson 会先停止看门狗线程,然后再发送释放锁的 Lua 脚本到 Redis。

这样一来,只要客户端进程没有挂掉,且没有主动释放锁,这个锁就会一直被持有,业务就不会因为执行时间过长而出现锁过期的问题。客户端挂掉后,看门狗线程也随之停止,锁在最终的过期时间到达后会被 Redis 自动清理,防止了死锁。

4.2 集成与使用实战

1. 引入依赖与配置:

<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.27.0</version> <!-- 请使用最新稳定版本 --> </dependency>

application.yml中配置单机模式(也支持集群、哨兵等模式):

spring: redis: host: localhost port: 6379 # database: 0 # password: # Redisson 配置 (如果starter不支持自动装配,需手动配置Bean)

2. 在业务中直接使用:

@Service public class InventoryService { @Autowired private RedissonClient redissonClient; // 由starter自动注入 public boolean deductStock(String itemId, int quantity) { String lockKey = “STOCK_LOCK:” + itemId; RLock lock = redissonClient.getLock(lockKey); // 方式1:阻塞式加锁,直到成功或中断 // lock.lock(); // try { ... } finally { lock.unlock(); } // 方式2:尝试加锁,最多等待10秒,锁持有时间设为30秒(看门狗会续期) boolean isLocked = false; try { isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS); if (isLocked) { // 成功获取锁,执行业务 return doDeductStock(itemId, quantity); } else { // 获取锁失败,处理逻辑(如返回错误信息) return false; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 恢复中断状态 throw new RuntimeException(“获取锁被中断”, e); } finally { if (isLocked && lock.isHeldByCurrentThread()) { lock.unlock(); } } } }

4.3 Redisson 锁的高级特性与注意事项

  1. 可重入性RLock继承了java.util.concurrent.locks.Lock接口,并且是可重入的。这意味着同一个线程可以多次获取同一把锁,而不会把自己阻塞死。内部通过 Redis 的 Hash 结构记录客户端ID和重入次数。
  2. 公平锁redissonClient.getFairLock(“myFairLock”)可以获取一个公平锁,它按照请求的顺序来分配锁,避免了某些线程饥饿。但性能会比非公平锁稍差。
  3. 联锁(MultiLock)红锁(RedLock):Redisson 提供了更复杂的锁机制来处理极端情况,比如需要同时锁定多个资源,或者需要更高的可靠性以应对单个 Redis 实例故障。红锁算法(RedLock)旨在解决在Redis主从架构下,主节点宕机可能导致锁丢失的问题,它要求客户端向超过半数的独立Redis实例申请锁,成功才算获取锁。但请注意,RedLock 算法本身在分布式系统领域存在争议(比如时钟漂移问题),使用前需充分评估。
  4. 务必在 finally 块中判断后解锁lock.isHeldByCurrentThread()这个判断很重要,特别是在使用了tryLock(waitTime, leaseTime, unit)且业务中可能发生中断的情况下,确保不会释放其他线程的锁。
  5. 性能与资源消耗:看门狗机制意味着只要持有锁,就会有一个后台线程在运行并定期与 Redis 通信。在大规模、高并发的应用中,成千上万的锁会对应成千上万的定时任务,对客户端和 Redis 都会产生一定的压力。对于执行时间非常短且确定的业务,可以考虑使用tryLock(0, 10, TimeUnit.SECONDS)并指定一个较短的leaseTime(第二个参数),这样 Redisson 就不会启动看门狗,锁在10秒后自动过期。

5. 方案三:基于 Lua 脚本的原子操作实现

如果你觉得 Redisson 太重,又对基础命令方案的非原子性不放心,那么自己编写 Lua 脚本是一个很好的折中方案。它让你对锁的控制粒度更细,且保证了核心操作的原子性。

5.1 Lua 脚本的原子性保障

Redis 保证,当一个 Lua 脚本在执行时,不会有其他命令或脚本被插入执行。这相当于在这个脚本的执行过程中,你拥有了一个“单线程”的环境。我们可以利用这一点,将判断和删除操作打包。

5.2 获取锁与释放锁的 Lua 脚本实现

1. 获取锁的 Lua 脚本 (lock.lua):这个脚本的目标是:如果key不存在,则设置它并设置过期时间;如果key已存在,则检查value是否与当前请求相同(支持可重入,简单版这里先实现互斥)。

-- KEYS[1]: 锁的key -- ARGV[1]: 锁的value (唯一标识,如UUID) -- ARGV[2]: 锁的过期时间(毫秒) if (redis.call(‘exists’, KEYS[1]) == 0) then -- 键不存在,可以加锁 redis.call(‘set’, KEYS[1], ARGV[1]) redis.call(‘pexpire’, KEYS[1], ARGV[2]) return 1 -- 返回1表示加锁成功 end -- 键已存在,检查value是否为自己加的(可重入逻辑基础) if (redis.call(‘get’, KEYS[1]) == ARGV[1]) then -- 如果是自己持有的锁,可以刷新过期时间或直接返回成功(这里选择刷新时间) redis.call(‘pexpire’, KEYS[1], ARGV[2]) return 1 -- 返回1表示加锁成功(重入) end return 0 -- 返回0表示加锁失败

注意:这个简单脚本实现了基本的互斥和防误删,但可重入计数等复杂功能需要更复杂的Hash结构,这里为简化先不展开。

2. 释放锁的 Lua 脚本 (unlock.lua):这个脚本的目标是:只有锁存在且value匹配时,才删除锁。

-- KEYS[1]: 锁的key -- ARGV[1]: 期望的锁的value if (redis.call(‘get’, KEYS[1]) == ARGV[1]) then -- 值匹配,删除锁 return redis.call(‘del’, KEYS[1]) else -- 值不匹配,说明锁可能已过期或不属于当前客户端,返回0表示释放失败或无需释放 return 0 end

5.3 在 Java 中集成与调用 Lua 脚本

我们可以利用StringRedisTemplateexecute方法来执行脚本。

@Component public class RedisLuaLockUtil { @Autowired private StringRedisTemplate stringRedisTemplate; private static final String LOCK_PREFIX = “LUAlock:”; // 使用DefaultRedisScript封装Lua脚本 private static final DefaultRedisScript<Long> LOCK_SCRIPT; private static final DefaultRedisScript<Long> UNLOCK_SCRIPT; static { // 初始化加锁脚本 LOCK_SCRIPT = new DefaultRedisScript<>(); LOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource(“lua/lock.lua”))); LOCK_SCRIPT.setResultType(Long.class); // 脚本返回数字类型 // 初始化解锁脚本 UNLOCK_SCRIPT = new DefaultRedisScript<>(); UNLOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource(“lua/unlock.lua”))); UNLOCK_SCRIPT.setResultType(Long.class); } public boolean tryLockWithLua(String lockKey, String requestId, long expireTime) { String key = LOCK_PREFIX + lockKey; // 执行脚本,参数:脚本对象,Key列表,可变参数(对应ARGV) Long result = stringRedisTemplate.execute( LOCK_SCRIPT, Collections.singletonList(key), // KEYS 数组 requestId, String.valueOf(expireTime) // ARGV 参数 ); // 根据脚本定义,返回1成功,0失败 return result != null && result == 1L; } public boolean unlockWithLua(String lockKey, String requestId) { String key = LOCK_PREFIX + lockKey; Long result = stringRedisTemplate.execute( UNLOCK_SCRIPT, Collections.singletonList(key), requestId ); // 脚本返回删除的key数量,1表示成功释放,0表示释放失败(锁已过期或非己持有) return result != null && result == 1L; } }

5.4 Lua 脚本方案的优缺点与适用场景

优点:

  1. 原子性保证:获取锁和释放锁的核心逻辑在一个原子操作中完成,彻底解决了基础命令方案中getdel之间的竞态条件问题。
  2. 轻量灵活:不依赖像 Redisson 这样的重型框架,只依赖 Redis 本身和简单的客户端。脚本逻辑完全由你控制,可以根据业务需要定制(比如实现简单的可重入、读写锁等)。
  3. 性能优异:Lua 脚本在 Redis 服务端执行,减少了网络往返次数(RTT),对于简单操作,性能比多次发送命令更好。

缺点与挑战:

  1. 功能需要自实现:像 Redisson 提供的看门狗(自动续期)、公平锁、红锁等高级功能,都需要你自己在 Lua 脚本和业务逻辑中实现,复杂度陡增。
  2. 脚本管理与调试:Lua 脚本需要作为资源文件管理,调试不如 Java 代码方便。脚本写错了可能导致 Redis 阻塞或逻辑错误。
  3. 集群环境下的注意点:在 Redis 集群模式下,Lua 脚本中操作的所有 key 必须位于同一个哈希槽(hash slot)中,否则会报错。对于分布式锁,锁的 key 本身是确定的,所以通常没问题。但如果你的脚本涉及多个 key,就需要使用{tag}语法来确保它们被分配到同一个槽位。

适用场景:适用于对性能和控制力有要求,且业务锁模型相对固定(不需要复杂的自动续期、公平锁等),团队有一定 Redis 和 Lua 脚本开发能力的项目。它是介于“简单但不安全”和“安全但较重”之间的一个优雅平衡点。

6. 生产环境常见问题排查与实战技巧

无论选择哪种方案,在线上环境都可能遇到各种问题。这里我总结了一份排查清单和实战技巧。

6.1 问题排查速查表

问题现象可能原因排查思路与解决方案
锁获取失败率突然升高1. Redis 服务端压力大,响应变慢。
2. 业务执行时间变长,锁持有时间变久,竞争加剧。
3. 网络波动导致命令超时。
1. 监控 Redis CPU、内存、网络IO、连接数。检查是否有慢查询。
2. 在业务代码中打点,统计锁内业务逻辑的执行时间分布。检查是否有外部依赖变慢。
3. 检查客户端与 Redis 服务器之间的网络延迟。
出现数据不一致(超卖等)1.锁过期:业务未执行完,锁已释放。
2.锁误释放:其他客户端释放了不属于自己的锁(基础命令方案风险)。
3.锁未释放:客户端异常退出,未执行 finally 块中的 unlock。
1.针对锁过期:使用 Redisson 看门狗,或根据业务监控合理调大过期时间。关键技巧:锁的过期时间应 > 业务平均执行时间 + 网络与GC抖动时间
2.针对误释放:确保使用唯一标识(UUID)作为 value,并使用 Lua 脚本或 Redisson 等原子性方案释放锁。
3.针对未释放:确保 unlock 逻辑在finally块中。对于非阻塞锁,要处理中断异常。
Redis CPU 占用率高1. 大量客户端频繁争抢同一把锁(热点锁)。
2. Redisson 看门狗续期任务过多。
3. Lua 脚本过于复杂或存在死循环。
1.细化锁粒度:不要用一把大锁锁住整个库存,而是锁到具体的商品ID甚至库存行ID。
2. 对于执行时间短的锁,使用 Redisson 时指定较短的leaseTime并关闭看门狗(tryLock(0, 短时间, TimeUnit.SECONDS))。
3. 审查 Lua 脚本逻辑,确保其复杂度为 O(1) 或 O(N) 且 N 很小。
客户端报连接超时或命令超时1. 网络问题。
2. Redis 服务端阻塞(如执行慢查询、RDB/AOF持久化)。
3. 客户端连接池配置不当。
1. 检查网络连通性。
2. 检查 Redis 日志,排查是否有SLOWLOGMONITOR发现的慢命令。
3.优化连接池:合理配置maxTotal(最大连接数)、maxIdle(最大空闲连接)和minIdle(最小空闲连接)。在 Spring Boot 中可以通过LettucePoolingClientConfigurationJedisPoolConfig配置。

6.2 实战进阶技巧

  1. 锁粒度控制:锁的粒度越细,并发度越高。例如,秒杀场景下,锁“stock_lock”会导致所有商品串行处理,性能极差。应该锁“stock_lock:product_1001”。但也要避免过度细化,导致需要管理海量的锁 key。
  2. 非阻塞锁与快速失败:在高并发场景下,如果获取锁失败,不要一直重试(除非是公平锁场景),这会给 Redis 和网络带来巨大压力。通常采用“快速失败”策略,直接给用户返回“系统繁忙”等友好提示。或者可以使用tryLock(timeout, unit)设置一个合理的等待超时时间。
  3. 锁与事务的结合:注意,在 Spring 的@Transactional注解方法中使用分布式锁时,锁的释放 (unlock) 应该在事务提交之前还是之后?如果之后,事务提交耗时较长,锁持有时间变长。如果之前,可能出现锁释放了但事务还没提交,其他线程读到旧数据。一个常见的做法是,将锁的范围控制在事务之外,或者使用编程式事务精细控制。
  4. 监控与告警:对分布式锁的关键指标进行监控是必不可少的。
    • 锁等待时间:从尝试获取锁到成功获取的时间。如果这个时间持续增长,说明竞争激烈。
    • 锁持有时间:业务在锁内执行的时间。如果远超过你设置的过期时间,就需要预警。
    • 锁获取失败率:失败率异常升高往往是系统出现问题的前兆。
    • 可以在加锁和释放锁的地方通过 AOP 或手动埋点,将 metrics 发送到 Prometheus、SkyWalking 等监控系统。

7. 方案对比总结与选型建议

最后,我将三种方案的核心区别整理成表格,并给出我的选型建议。

特性维度基础命令方案 (SET NX PX)Redisson 方案Lua 脚本方案
实现复杂度极低低(开箱即用)中(需编写和维护脚本)
可靠性低(释放锁非原子,有竞态风险)高(看门狗、原子操作)高(核心操作原子)
功能丰富度仅有基本互斥极高(可重入、公平锁、联锁、红锁)可自定义,但需自实现
自动续期不支持支持(看门狗机制)不支持,需自实现
性能中(有后台线程开销)高(脚本原子执行,RTT少)
依赖仅 Redis 客户端Redisson 客户端仅 Redis 客户端(支持Lua)
适用场景简单的、非核心的同步控制,快速原型验证中大型分布式系统,对锁可靠性、功能要求高的生产环境对性能和可控性有要求,且愿意投入开发维护的中型项目

我的个人选型经验:

  • 对于绝大多数 Java 后端项目,我首推 Redisson。它几乎解决了分布式锁的所有痛点,社区活跃,文档丰富,虽然引入了一个依赖,但带来的可靠性和开发效率的提升是巨大的。除非你的应用对 jar 包大小和依赖数量有极其苛刻的要求。
  • 如果你在一个非 Java 技术栈,或者团队对 Redis 和 Lua 非常熟悉,且锁模型简单固定,Lua 脚本方案是一个很棒的选择。它轻量、高效、可控。
  • 基础命令方案,我只会在写一些临时脚本、Demo 或者对一致性要求极低的场景下使用,并且一定会使用原子的SET ... NX PX命令,并在释放时进行 value 校验。

分布式锁没有银弹,选择哪种方案,最终取决于你的业务场景、团队技术栈和运维能力。理解每种方案的原理和 trade-off,才能做出最适合当前项目的决策。希望这篇近万字的深度解析,能帮你彻底掌握 Redis 分布式锁,在分布式系统的世界里,稳稳地锁住你的关键资源。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询