在我的智慧校园项目中,多个用户同时预约同一时间段这是不可避免的一个高并发场景,本文介绍如何使用Redisson分布式锁来解决这个问题。
什么是分布式锁?
分布式锁是控制分布式系统间同步访问共享资源的技术手段,用于协调不同系统或主机对资源的互斥访问,防止数据冲突并保证一致性在分布式系统中。如何保证同一时刻只有一个进程(或服务器)能操作某个共享资源——分布式锁解决了这个问题。————百度百科
听着很高大上,其实目的就是为了解决并发问题,不过环境是在集群模式下。
为什么要使用Redisson分布式锁?
在一般的项目当中,并发问题是不可避免的,比如当多个学生同时预约同一资源的同一时间段时,就会出现超卖等并发问题。
如果没有分布式锁保护,当极短时间内发生并发请求时,数据库的常规查询将失效,导致严重的数据不一致问题(即“超卖”)。
请求到达服务器。
执行 SELECT count 检查时间是否被占用。
请求几乎同时到达服务器。
执行 SELECT count 检查时间是否被占用。
查询结果返回:该时段空闲。
准备执行 INSERT 插入预约记录。
查询结果返回:该时段空闲(因为A的记录还未插入)。
准备执行 INSERT 插入预约记录。
成功写入数据库,预约成功!🎉
成功写入数据库,预约成功!🎉
为了解决这个问题,可以引入锁,在单机部署下,我们可以使用 Java 自带的 synchronized来解决。但为了应对高并发,项目通常是集群部署的。此时,本地锁将失效,我们必须引入分布式锁。
为什么要用Redisson来实现分布式锁?可以看一下几种分布式锁方式的对比。
| 特性 | Redisson | SET NX + Lua | ZooKeeper |
|---|---|---|---|
| 可重入 | ✅ | ❌ | ✅ |
| 锁续期 | ✅ 自动续期 | 需手动实现 | ❌ |
| 等待锁 | ✅ | 需循环实现 | ✅ |
| 性能 | 高 | 最高 | 低 |
| 集群支持 | ✅ | 需额外处理 | ✅ |
可见Redisson实现的分布式锁对比其他例如ZooKeeper,Redis原生实现的分布式锁的优势很大,所以我们一般选择Redisson来实现分布式锁。
分布式锁功能实现
分布式锁基本实现
引入Redisson的依赖
<!-- pom.xml -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.6</version>
</dependency>核心代码实现
@Service
@Slf4j
public class UserAppointmentServiceImpl implements UserAppointmentService {
@Autowired
private RedissonClient redissonClient;//注入Redisson
@Autowired
private UserAppointmentMapper userAppointmentMapper;
// 其他注入...
@Override
public void submitAppointment(AppointmentDTO dto) {
// 1. 构造锁的 key(粒度:资源 + 日期 + 开始时间 + 结束时间)
String lockKey = "lock:appointment:" + dto.getResourceId()
+ ":" + dto.getAppointDate()
+ ":" + dto.getStartTime()
+ ":" + dto.getEndTime();
// 2. 获取 Redisson 锁
RLock lock = redissonClient.getLock(lockKey);
try {
// 3. 尝试获取锁
// 参数1:等待时间 5秒 - 超过5秒放弃获取
// 参数2:锁自动释放时间 10秒 - 防止死锁
// 参数3:时间单位
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
try {
// 4. 检查时间冲突
Integer count = userAppointmentMapper.countConflict(
dto.getResourceId(),
dto.getAppointDate(),
dto.getStartTime(),
dto.getEndTime()
);
if (count > 0) {
throw new RuntimeException(
"手慢了!该时间段刚刚已被抢占,请刷新后重试。");
}
// 5. 提交预约
Appointment appointment = new Appointment();
BeanUtils.copyProperties(dto, appointment);
appointment.setUserId(BaseContext.getCurrentId());
appointment.setStatus(0);
userAppointmentMapper.insert(appointment);
} finally {
// 6. 释放锁
lock.unlock();
}
} else {
// 获取锁失败
throw new RuntimeException("系统繁忙,请稍后再试");
}
} catch (InterruptedException e) {
throw new RuntimeException("系统繁忙,请稍后再试");
}
}
}锁的 Key 设计
- 锁的粒度要足够细,避免不必要的锁竞争
- 包含所有唯一标识:资源ID、日期、开始时间、结束时间
- 使用冒号分隔,便于 Redis 客户端可视化查看
核心机制解析
tryLock 参数说明
lock.tryLock(5, 10, TimeUnit.SECONDS)| 参数 | 含义 | 作用 |
|---|---|---|
| 5 | 等待时间 | 5秒内持续尝试获取锁,超过则放弃 |
| 10 | 锁持有时间 | 锁自动释放时间,防止业务未完成锁过期 |
| TimeUnit.SECONDS | 时间单位 | 秒 |
Redisson实现的分布式锁有其独特的看门狗机制
锁获取成功 → 启动看门狗线程 → 每 10 秒检查一次→ 锁还持有?续期 30 秒
释放锁
释放锁要放在finally块中保证能够成功释放,并且Redisson 会校验线程 ID,防止误释放。
避坑
我在通过分布式锁进行并发处理的时候往方法加了Transactional注解,结果我在Jmeter压测的时候发现分布式锁没有成功实现,我debug了很久,最后才发现是因为事务的原因——锁在方法内部释放了,但事务还没提交,这就导致数据库没有改变,也就不能解决并发的问题了。
如何解决呢?
一是可以直接在Controller层加锁然后Service层加事务。 二是在 Service 层的无事务方法中加锁,锁内部调用另一个有 @Transactional 的方法,可以通过注入自身或 AopContext.currentProxy()获取代理对象来解决。
总之,锁的边界必须大于事务的边界,这样锁才不会被事务影响。
总结
通过引入 Redisson 分布式锁,我们优雅地解决了智慧校园中的预约冲突问题。这也是并发问题在集群模式下的常规解决的最佳实践。






