如何在基于Spring的预订类应用中避免竞态条件问题?
解决方案:防范会议室预订的并发冲突
这是个典型的并发资源抢占问题,在会议室预订这类场景里非常常见。结合你现有的领域模型,我给你几个实用的解决方案,从应用层到数据库层都有,你可以根据自己的并发量和业务复杂度选择:
1. 悲观锁:在预订事务中锁定会议室
既然你担心步骤1锁定会占用资源,那我们只在步骤2的确认阶段加锁——也就是用户真正要预订的时候,在同一个事务里完成「锁定会议室→校验时段→创建预订记录」的操作,这样其他并发请求必须等当前事务完成才能操作同一个会议室,从根源上避免同时校验通过的情况。
具体实现(Spring Data JPA)
给RoomRepository添加带悲观写锁的查询方法:
@Repository public interface RoomRepository extends JpaRepository<Room, Long> { // 对目标会议室加排他写锁,其他事务无法同时操作该房间 @Lock(LockModeType.PESSIMISTIC_WRITE) Optional<Room> findById(Long id); }
然后在预订服务的事务方法里,按顺序执行操作:
@Service @Transactional public class MeetingBookingService { private final RoomRepository roomRepo; private final MeetingRepository meetingRepo; private final RecurrentMeetingRepository recurrentMeetingRepo; // 构造注入略 public void bookSingleMeeting(Long roomId, Instant start, Instant end) { // 1. 锁定目标会议室,阻塞其他并发操作 Room targetRoom = roomRepo.findById(roomId) .orElseThrow(() -> new IllegalArgumentException("会议室不存在")); // 2. 校验当前时段是否和已有单次/重复预订冲突 boolean hasOverlap = checkMeetingOverlap(roomId, start, end); if (hasOverlap) { throw new IllegalStateException("该时段已被预订"); } // 3. 创建并保存新的单次预订 Meeting newMeeting = new Meeting(); newMeeting.setRoom(targetRoom); newMeeting.setStart(start); newMeeting.setEnd(end); meetingRepo.save(newMeeting); } // 辅助方法:检查时段冲突 private boolean checkMeetingOverlap(Long roomId, Instant start, Instant end) { // 检查单次会议是否重叠 boolean singleOverlap = meetingRepo.existsByRoomIdAndStartLessThanAndEndGreaterThan(roomId, end, start); if (singleOverlap) return true; // 检查重复会议的规则是否会生成重叠时段(这里需要根据你的Rule逻辑实现,比如判断规则的周期、日期是否和当前时段有交集) return recurrentMeetingRepo.existsByRoomIdAndRuleOverlapsWith(roomId, start, end); } }
优缺点:
- ✅ 逻辑直观,用户无需重试,体验好
- ❌ 高并发场景下会出现锁等待,可能影响系统吞吐量,适合并发量中等的业务
2. 乐观锁:用版本号检测冲突
如果你的系统并发量很高,悲观锁的阻塞会成为性能瓶颈,可以用乐观锁方案:给Room实体加版本号,在事务结束时自动检测是否有其他并发操作修改了该会议室的状态,若有则抛出异常,提示用户重新尝试预订。
具体实现
修改Room实体,添加版本字段:
@Entity class Room { @Id private Long id; @Version // JPA自动维护版本号,每次更新递增 private Long version; // 其他字段 }
然后在预订服务中,正常执行校验和保存操作,捕获乐观锁失败的异常:
@Service @Transactional public class MeetingBookingService { // ... 注入略 public void bookSingleMeeting(Long roomId, Instant start, Instant end) { try { Room targetRoom = roomRepo.findById(roomId) .orElseThrow(() -> new IllegalArgumentException("会议室不存在")); if (checkMeetingOverlap(roomId, start, end)) { throw new IllegalStateException("该时段已被预订"); } Meeting newMeeting = new Meeting(); newMeeting.setRoom(targetRoom); newMeeting.setStart(start); newMeeting.setEnd(end); meetingRepo.save(newMeeting); } catch (OptimisticLockingFailureException e) { // 捕获版本冲突异常,提示用户重试 throw new IllegalStateException("当前时段预订冲突,请重新尝试"); } } }
优缺点:
- ✅ 无锁阻塞,性能好,适合高并发场景
- ❌ 冲突时需要用户重试,需要在前端做友好提示
3. 数据库层约束:最底层的防护
不管应用层逻辑怎么写,数据库层面的约束是最可靠的兜底方案。你可以通过触发器或者自定义约束,让数据库自动检查新插入的预订是否和已有记录重叠,直接拒绝冲突的写入。
具体实现(以PostgreSQL为例)
先创建一个检查时段重叠的函数,再绑定到meeting表的触发器上:
-- 定义检查冲突的函数 CREATE OR REPLACE FUNCTION check_meeting_overlap() RETURNS TRIGGER AS $$ BEGIN -- 检查新预订是否和已有单次会议重叠 IF EXISTS ( SELECT 1 FROM meeting m WHERE m.room_id = NEW.room_id AND m.start < NEW.end AND m.end > NEW.start ) THEN RAISE EXCEPTION '会议室 % 的时段 [% - %] 已被预订', NEW.room_id, NEW.start, NEW.end; END IF; -- 这里可以扩展检查重复会议的规则是否冲突(根据你的Rule逻辑实现) RETURN NEW; END; $$ LANGUAGE plpgsql; -- 创建触发器,插入/更新会议时自动检查 CREATE TRIGGER trigger_meeting_overlap_check BEFORE INSERT OR UPDATE ON meeting FOR EACH ROW EXECUTE FUNCTION check_meeting_overlap();
然后在应用层捕获数据库抛出的异常,提示用户即可。
优缺点:
- ✅ 最可靠,即使应用层出现逻辑漏洞,数据库也能保证数据一致性
- ❌ 规则复杂的重复会议冲突判断会比较难写,触发器逻辑维护成本高
关键注意事项
- 步骤1无需锁定:步骤1只是查询可用时段,属于读操作,不需要加锁,否则会导致资源长期占用。
- 事务原子性:步骤2的「校验+保存」必须在同一个事务中执行,不能拆分,否则会出现中间状态的并发问题。
- 重复会议的特殊处理:对于
RecurrentMeeting,你需要根据规则逻辑判断是否和已有预订重叠——比如如果是每月9日的重复规则,需要检查已有单次会议是否在每月9日的对应时段,以及已有重复规则是否会在同一时段生成冲突的会议。
内容的提问来源于stack exchange,提问作者Vasya Rogov
相关产品推荐
相关产品推荐

