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

基于Spring Boot+Hibernate的网球场预约应用并发与防重叠方案问询

网球场预约系统并发时段冲突解决方案

针对你遇到的并发预约重叠问题,实际生产场景中常用以下几种Java代码层的解决方案,既避免全表锁的性能问题,又能保证数据一致性:

1. 精准范围行级锁(PostgreSQL专属)

利用PostgreSQL的SELECT ... FOR UPDATE SKIP LOCKED语法,只锁定可能与新预约冲突的时段行,而非全表。这样并发请求中,只有一个能获取到锁并执行后续逻辑,其他请求会跳过锁定的行,直接判断冲突并返回失败,避免长时间等待。

实现步骤:

  • 确保预约表包含court_id(球场ID,核心锁粒度,否则不同球场的预约会互相阻塞)、reservation_start、reservation_end字段。
  • 在Spring Data Repository中定义原生查询方法,锁定当日该球场的所有时段:
@Repository
public interface ReservationRepository extends JpaRepository<Reservation, Long> {
    @Query(value = "SELECT r FROM Reservation r WHERE r.courtId = :courtId AND DATE(r.reservationStart) = :date FOR UPDATE SKIP LOCKED")
    List<Reservation> findByCourtIdAndDateForUpdate(@Param("courtId") Long courtId, @Param("date") LocalDate date);
}
  • 在事务方法中执行逻辑:
@Service
@Transactional
public class ReservationService {
    @Autowired
    private ReservationRepository reservationRepo;

    public boolean createReservation(Reservation newRes) {
        // 1. 校验时长不超过1小时
        if (Duration.between(newRes.getReservationStart(), newRes.getReservationEnd()).toHours() > 1) {
            return false;
        }
        LocalDate date = newRes.getReservationStart().toLocalDate();
        // 2. 锁定当日该球场的所有时段,并发请求中只有一个能拿到锁
        List<Reservation> existingRes = reservationRepo.findByCourtIdAndDateForUpdate(newRes.getCourtId(), date);
        // 3. 检查时段是否重叠
        boolean isOverlap = existingRes.stream().anyMatch(r -> 
            newRes.getReservationStart().isBefore(r.getReservationEnd()) && 
            newRes.getReservationEnd().isAfter(r.getReservationStart())
        );
        if (isOverlap) {
            return false;
        }
        // 4. 插入新记录
        reservationRepo.save(newRes);
        return true;
    }
}

优势:锁粒度极小,仅影响同一球场同日的预约,性能损耗低;无需额外中间件。

2. 乐观锁+冲突重试机制

通过乐观锁标记结合重试逻辑,在并发冲突时自动重试几次,避免阻塞。适合并发量不是极高的场景。

实现步骤:

  • 给Reservation实体添加@Version字段,Hibernate会自动维护版本号:
@Entity
public class Reservation {
    // 其他字段
    @Version
    private Integer version;
}
  • 在服务层捕获OptimisticLockingFailureException,并设置重试次数:
@Service
public class ReservationService {
    @Autowired
    private ReservationRepository reservationRepo;
    private static final int MAX_RETRIES = 3;

    @Transactional
    public boolean createReservation(Reservation newRes) {
        int retryCount = 0;
        while (retryCount < MAX_RETRIES) {
            try {
                // 1. 校验时长
                if (Duration.between(newRes.getReservationStart(), newRes.getReservationEnd()).toHours() > 1) {
                    return false;
                }
                // 2. 查询同日同球场的时段
                LocalDate date = newRes.getReservationStart().toLocalDate();
                List<Reservation> existingRes = reservationRepo.findByCourtIdAndDate(newRes.getCourtId(), date);
                // 3. 检查重叠
                boolean isOverlap = existingRes.stream().anyMatch(r -> 
                    newRes.getReservationStart().isBefore(r.getReservationEnd()) && 
                    newRes.getReservationEnd().isAfter(r.getReservationStart())
                );
                if (isOverlap) {
                    return false;
                }
                // 4. 插入记录
                reservationRepo.save(newRes);
                return true;
            } catch (OptimisticLockingFailureException e) {
                retryCount++;
                if (retryCount >= MAX_RETRIES) {
                    return false;
                }
                // 短暂休眠后重试
                try {
                    Thread.sleep(100);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    return false;
                }
            }
        }
        return false;
    }
}

优势:无锁阻塞,性能好;实现简单,依赖Hibernate原生特性。

3. 分布式锁(集群部署场景)

如果你的应用是集群部署,行级锁或乐观锁可能无法跨节点生效,此时可以用分布式锁来控制同一球场同日的预约请求串行执行。常用Redis作为分布式锁实现(比如Redisson)。

实现步骤:

  • 引入Redisson依赖,配置Redis连接。
  • 生成锁的唯一Key,粒度为球场ID+日期:
@Service
public class ReservationService {
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private ReservationRepository reservationRepo;

    public boolean createReservation(Reservation newRes) {
        // 1. 校验时长
        if (Duration.between(newRes.getReservationStart(), newRes.getReservationEnd()).toHours() > 1) {
            return false;
        }
        LocalDate date = newRes.getReservationStart().toLocalDate();
        String lockKey = String.format("reservation:lock:%d:%s", newRes.getCourtId(), date.toString());
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 尝试获取锁,最多等待3秒,持有锁10秒
            if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
                // 2. 查询同日同球场的时段
                List<Reservation> existingRes = reservationRepo.findByCourtIdAndDate(newRes.getCourtId(), date);
                // 3. 检查重叠
                boolean isOverlap = existingRes.stream().anyMatch(r -> 
                    newRes.getReservationStart().isBefore(r.getReservationEnd()) && 
                    newRes.getReservationEnd().isAfter(r.getReservationStart())
                );
                if (isOverlap) {
                    return false;
                }
                // 4. 插入记录
                reservationRepo.save(newRes);
                return true;
            } else {
                // 获取锁失败,返回预约繁忙
                return false;
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

优势:支持集群部署,锁粒度精准,不影响其他球场或日期的预约性能。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 09:13:13