Redis分布式锁详解:原理、实现与实战案例
目录
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。 -
结合业务设置合理的锁超时时间。
更多推荐



所有评论(0)