Redis分布式锁详解:原理、实现与实战案例
目录
1. 什么是分布式锁?
分布式锁的核心要求
2. 基于Redis的分布式锁实现方案
(1)基础方案:SETNX + EXPIRE
(2)优化方案:SET NX PX(原子性加锁)
(3)进阶方案:RedLock(Redis官方推荐)
3. 实战案例
案例1:防止重复下单
1. 加锁阶段
2. 业务逻辑阶段
3. 释放锁阶段
案例2:秒杀库存扣减
案例3:分布式定时任务调度
4. 常见问题与解决方案
(1)锁过期但业务未执行完?
(2)锁被其他客户端误删?
(3)Redis主从切换导致锁丢失?
5. 总结
1. 什么是分布式锁?
在分布式系统中,多个服务实例可能同时访问共享资源(如数据库、缓存等),为了避免并发问题(如超卖、重复提交等),我们需要一种跨JVM的锁机制——分布式锁。
分布式锁的核心要求
-
互斥性:同一时刻只有一个客户端能持有锁。
-
防死锁:即使客户端崩溃,锁也能自动释放。
-
高可用:锁服务必须高可用(如Redis集群)。
-
可重入性(可选):同一个客户端可以多次获取同一把锁。
2. 基于Redis的分布式锁实现方案
Redis因其高性能和原子性操作(如SETNX
),成为实现分布式锁的常用方案。
(1)基础方案:SETNX + EXPIRE
// 加锁(错误示范,非原子性) Boolean locked = redisTemplate.opsForValue().setIfAbsent("lock:order123", "1"); if (locked) {redisTemplate.expire("lock:order123", 10, TimeUnit.SECONDS); // 设置过期时间// 执行业务逻辑...redisTemplate.delete("lock:order123"); // 释放锁 }
问题:SETNX
和EXPIRE
不是原子操作,如果加锁后客户端崩溃,锁永远不会释放!
(2)优化方案:SET NX PX(原子性加锁)
Redis 2.6+ 支持SET
命令的NX
(不存在才设置)和PX
(毫秒级过期时间)参数:
// 正确方式:原子性加锁 + 设置过期时间 Boolean locked = redisTemplate.opsForValue().setIfAbsent("lock:order123", "client1", 10, TimeUnit.SECONDS );if (locked) {try {// 执行业务逻辑...} finally {// 释放锁(需判断是否是自己加的锁)if ("client1".equals(redisTemplate.opsForValue().get("lock:order123"))) {redisTemplate.delete("lock:order123");}} }
关键改进:
-
使用
SET NX PX
保证原子性。 -
设置唯一标识(如
client1
),避免误删其他客户端的锁。
(3)进阶方案:RedLock(Redis官方推荐)
如果单点Redis不可靠,可以使用RedLock算法(需多个独立Redis实例):
// RedLock示例(使用Redisson客户端) RLock lock1 = redissonClient1.getLock("lock:order123"); RLock lock2 = redissonClient2.getLock("lock:order123"); RLock lock3 = redissonClient3.getLock("lock:order123");RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3); try {if (redLock.tryLock(10, 30, TimeUnit.SECONDS)) { // 最多等待10秒,锁30秒后自动过期// 执行业务逻辑...} } finally {redLock.unlock(); }
适用场景:对一致性要求极高的场景(如金融交易)。
3. 实战案例
案例1:防止重复下单
public String createOrder(String userId, String productId) {String lockKey = "lock:order:" + userId + ":" + productId;String clientId = UUID.randomUUID().toString(); // 唯一标识try {// 尝试加锁(等待5秒,锁10秒后自动释放)Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);if (!locked) {throw new RuntimeException("操作太频繁,请稍后再试!");}// 检查是否已下单if (orderService.hasOrder(userId, productId)) {throw new RuntimeException("请勿重复下单!");}// 创建订单...return orderService.create(userId, productId);} finally {// 释放锁(需校验clientId)if (clientId.equals(redisTemplate.opsForValue().get(lockKey))) {redisTemplate.delete(lockKey);}} }
这段代码实现了一个防并发重复下单的订单创建逻辑,核心是使用Redis分布式锁来保证同一用户对同一商品的订单操作是串行化的。下面逐部分解析:
1. 加锁阶段
String lockKey = "lock:order:" + userId + ":" + productId; String clientId = UUID.randomUUID().toString();
-
lockKey
:锁的键,格式为lock:order:{userId}:{productId}
,确保不同用户或不同商品的锁互不影响。 -
clientId
:生成唯一标识(UUID),用于后续校验锁的归属,防止误删其他客户端的锁。
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS );
-
setIfAbsent
:Redis的SETNX
命令(原子性操作),如果lockKey
不存在则加锁,并设置:-
值:
clientId
(锁的持有者标识)。 -
过期时间:10秒(防止死锁)。
-
-
返回值:
true
表示加锁成功,false
表示锁已被占用。
if (!locked) {throw new RuntimeException("操作太频繁,请稍后再试!"); }
-
如果加锁失败,直接抛出异常,提示用户"操作太频繁"(类似秒杀场景的限流)。
2. 业务逻辑阶段
if (orderService.hasOrder(userId, productId)) {throw new RuntimeException("请勿重复下单!"); }
-
检查是否已下单:在锁的保护下查询订单系统,防止重复下单(即使通过了前端校验,仍需后端保证幂等性)。
return orderService.create(userId, productId);
-
创建订单:执行业务逻辑(如扣减库存、生成订单等)。
3. 释放锁阶段
finally {if (clientId.equals(redisTemplate.opsForValue().get(lockKey))) {redisTemplate.delete(lockKey);} }
-
finally
块:确保锁一定会被释放,即使业务逻辑抛出异常。 -
校验
clientId
:-
只释放自己加的锁(避免误删其他客户端的锁)。
-
如果锁已自动过期(10秒后),
get
操作返回null
,不会执行删除。
-
-
原子性问题:
-
这里的
get
和delete
是两步操作,非原子性,极端情况下可能误删锁(如锁过期后,其他客户端加锁成功,但当前线程仍执行删除)。 -
改进方案:使用Lua脚本保证原子性(见下文补充)。
-
案例2:秒杀库存扣减
public boolean seckill(Long productId, Long userId) {String lockKey = "lock:seckill:" + productId;String stockKey = "stock:" + productId;String clientId = UUID.randomUUID().toString();try {// 加锁(防止超卖)Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 3, // 锁3秒(避免长时间阻塞)TimeUnit.SECONDS);if (!locked) {return false; // 抢锁失败}// 检查库存Integer stock = Integer.valueOf(redisTemplate.opsForValue().get(stockKey));if (stock <= 0) {return false; // 已售罄}// 扣减库存(原子操作)redisTemplate.opsForValue().decrement(stockKey);// 生成订单...orderService.createSeckillOrder(userId, productId);return true;} finally {// 释放锁if (clientId.equals(redisTemplate.opsForValue().get(lockKey))) {redisTemplate.delete(lockKey);}} }
案例3:分布式定时任务调度
多个服务实例同时运行定时任务时,需确保只有一个实例执行:
@Scheduled(cron = "0 */5 * * * ?") // 每5分钟执行一次 public void scheduledTask() {String lockKey = "lock:scheduled:report";String clientId = "server-" + System.getProperty("server.port"); // 用服务实例标识try {// 尝试加锁(锁5分钟)Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 5, TimeUnit.MINUTES);if (!locked) {return; // 其他实例已执行}// 执行业务逻辑(生成报表...)reportService.generateDailyReport();} finally {// 释放锁if (clientId.equals(redisTemplate.opsForValue().get(lockKey))) {redisTemplate.delete(lockKey);}} }
4. 常见问题与解决方案
(1)锁过期但业务未执行完?
-
问题:锁自动释放后,其他客户端可能获取锁,导致并发问题。
-
解决方案:使用看门狗机制(如Redisson的
lockWatchdogTimeout
),自动续期锁。
(2)锁被其他客户端误删?
-
问题:客户端A释放了客户端B的锁。
-
解决方案:加锁时设置唯一标识(如UUID),释放时校验。
(3)Redis主从切换导致锁丢失?
-
问题:主节点加锁后崩溃,从节点晋升但未同步锁数据。
-
解决方案:使用RedLock(多Redis实例)或ZooKeeper替代。
5. 总结
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
SETNX + EXPIRE | 简单高效 | 非原子性,可能死锁 | 低并发场景 |
SET NX PX | 原子操作 | 单点故障 | 一般分布式系统 |
RedLock | 高可用 | 实现复杂 | 金融级高一致性场景 |
最佳实践:
-
优先使用
SET NX PX
+ 唯一标识。 -
高可用场景选择Redisson的
RLock
或RedLock。 -
结合业务设置合理的锁超时时间。