黑马点评中的分布式锁设计与实现(Redis + Redisson)
在高并发秒杀场景中,分布式锁是保证"一人一单、库存不超卖"的核心手段之一 。
黑马点评中一人一单单机部署 通过锁实现的解析博客内容:黑马点评中 VoucherOrderServiceImpl 实现类中的一人一单实现解析(单机部署)
📚 目录(点击跳转对应章节)
[1. 为什么秒杀系统必须使用分布式锁?](#1. 为什么秒杀系统必须使用分布式锁?)
[2. 分布式锁在本项目中的作用点](#2. 分布式锁在本项目中的作用点)
[3. 方案一:基于 Redis 的自定义分布式锁(SimpleRedisLock)](#3. 方案一:基于 Redis 的自定义分布式锁(SimpleRedisLock))
[4. 方案二:使用 Redisson 实现分布式锁(推荐)](#4. 方案二:使用 Redisson 实现分布式锁(推荐))
[5. 两种分布式锁方案对比](#5. 两种分布式锁方案对比)
[6. 总结](#6. 总结)
一、为什么秒杀系统必须使用分布式锁?
在秒杀场景中,系统通常具备以下特征:
- 并发极高(瞬时成千上万请求)
- 库存极少
- 严格的一人一单规则
如果不加锁,会出现以下问题:
1. 超卖问题
多个线程同时读取库存 stock > 0,然后同时扣减,导致库存变成负数。
问题本质说明:
超卖的根本原因并不是代码写错,而是:
库存判断与库存扣减不是一个原子操作
即使使用了数据库事务,在高并发写场景下:
- MySQL 默认隔离级别为
REPEATABLE READ - 多个事务可能同时读取到相同库存值
- 仅依赖数据库事务会导致大量锁竞争,性能急剧下降
因此,在秒杀系统中:
- 锁必须前置到业务逻辑之前
- 不能依赖数据库作为第一道并发控制防线
2. 一人多单问题
同一用户在极短时间内发送多个请求,绕过数据库层面的校验。
虽然可以在数据库中通过 (user_id, voucher_id) 唯一索引兜底,但这种方式存在明显问题:
- 唯一索引只能保证最终一致性
- 并发请求仍然会打到数据库
- 产生大量无效 insert、回滚和异常日志
分布式锁的价值在于:
在请求进入数据库之前,就直接拦截无效并发请求,从源头削峰
3. 单机锁失效
synchronized 或 ReentrantLock 只能保证单 JVM 生效,在集群环境下完全无效。
在秒杀系统中:
- 服务通常是多实例部署
- 请求会被负载均衡分发到不同 JVM
这类锁无法跨进程、跨机器生效,因此必须使用分布式锁。
二、分布式锁在本项目中的作用点

在 seckillVoucher() 方法中,分布式锁的作用是:
保证同一个秒杀券,在同一时刻只能被一个线程创建订单
关键代码位置:
java
RLock lock = redissonClient.getLock(RedisConstants.LOCK_ORDER_KEY + voucherId);
boolean isLock = lock.tryLock();
锁设计说明补充
- 锁粒度:
voucherId - 锁范围:下单逻辑整体
- 锁目的:防止并发创建订单导致的一人多单与库存竞争
这种设计属于偏保守但极其安全的方案:
- 同一券严格串行下单
- 不同券互不影响
- 逻辑清晰,适合生产系统和教学项目
三、方案一:基于 Redis 的自定义分布式锁(SimpleRedisLock)
1. 核心思想
使用 Redis 的:
SETNX + 过期时间 + Lua 脚本
来保证:
- 加锁的原子性
- 不误删他人锁
- 防止死锁
补充说明:
-
SETNX\](file:///d:/CodingFiles/MarkDown/c.md#L15-L40) 保证只有一个线程能成功设置锁
- Lua 脚本保证解锁操作的原子性
2. 自定义锁接口
java
public interface ILock {
boolean tryLock(Long timeoutSec);
void unlock();
}
接口设计刻意保持简洁:
- 只关注最核心的加锁与释放
- 方便后续替换不同实现
3. SimpleRedisLock 完整实现
java
package com.hmdp.utils;
import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock {
private StringRedisTemplate stringRedisTemplate;
private String name;
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
@Override
public boolean tryLock(Long timeoutSec) {
String threadId = ID_PREFIX + Thread.currentThread().threadId();
String key = KEY_PREFIX + name;
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(key, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().threadId()
);
}
}
4. 为什么释放锁一定要用 Lua?
如果使用 Java 代码:
java
if (threadId.equals(redisValue)) {
delete(key);
}
这并不是原子操作,可能发生:
- 判断锁归属成功
- 线程发生上下文切换
- 锁过期并被其他线程获取
- 当前线程误删他人锁
Lua 脚本可以保证:
判断锁归属 + 删除锁 是一个不可分割的原子操作
5. 自定义分布式锁的局限性补充
该实现仍然存在以下问题:
- 不支持可重入
- 无自动续期机制
- 业务执行时间超过锁 TTL 会导致锁提前释放
因此更适合作为:
- 原理学习
- 面试讲解
- 简单业务场景
四、方案二:使用 Redisson 实现分布式锁(推荐)

1. Redisson 的优势
- 内置 Watch Dog 自动续期机制
- 支持可重入锁
- 支持公平锁、读写锁、联锁
- 实现成熟,经过大量生产验证
Redisson 本质上是:
对 Redis 分布式能力的一层高质量工程封装
2. Redisson 配置类
java
@Configuration
public class RedissonConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private String port;
@Value("${spring.data.redis.password}")
private String password;
@Value("${spring.data.redis.database:2}")
private String database;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
String address = "redis://" + host + ":" + port;
config.useSingleServer()
.setAddress(address)
.setPassword(password)
.setDatabase(Integer.parseInt(database));
return Redisson.create(config);
}
}
3. 秒杀中使用 Redisson 锁
java
RLock lock = redissonClient.getLock(RedisConstants.LOCK_ORDER_KEY + voucherId);
boolean isLock = lock.tryLock();
if (!isLock) {
return Result.fail("不允许重复下单");
}
try {
IVoucherOrderService proxy =
(IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(userId, voucherId);
} finally {
lock.unlock();
}
补充说明:
tryLock()默认不阻塞,符合秒杀快速失败的业务语义finally块释放锁,防止异常导致死锁- Watch Dog 会在业务执行期间自动续期
4. 为什么锁要放在事务外?
分布式锁与事务职责不同:
- 分布式锁:控制并发入口
- 事务:保证数据一致性
如果锁放在事务内,可能出现:
- 事务尚未提交
- 锁已经释放
- 其他线程读取到旧数据
正确顺序:
先加分布式锁 → 再进入事务 → 提交事务 → 最后释放锁
五、两种分布式锁方案对比
| 对比项 | SimpleRedisLock | Redisson |
|---|---|---|
| 实现难度 | 较高 | 较低 |
| 安全性 | 较高 | 很高 |
| 自动续期 | 不支持 | 支持 |
| 可重入 | 不支持 | 支持 |
| 生产推荐 | 否 | 是 |
六、总结
- 秒杀系统中,分布式锁是必不可少的基础设施
- 自定义 Redis 锁有助于深入理解底层原理
- 生产环境应优先选择 Redisson 等成熟方案
- 锁负责并发控制,事务负责数据一致性,二者边界必须清晰
真正高并发系统的核心,不在于代码技巧,而在于对并发、原子性和时序问题的深入理解