You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在基于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无需锁定:步骤1只是查询可用时段,属于读操作,不需要加锁,否则会导致资源长期占用。
  2. 事务原子性:步骤2的「校验+保存」必须在同一个事务中执行,不能拆分,否则会出现中间状态的并发问题。
  3. 重复会议的特殊处理:对于RecurrentMeeting,你需要根据规则逻辑判断是否和已有预订重叠——比如如果是每月9日的重复规则,需要检查已有单次会议是否在每月9日的对应时段,以及已有重复规则是否会在同一时段生成冲突的会议。

内容的提问来源于stack exchange,提问作者Vasya Rogov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 10:22:27