基于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
相关产品推荐
相关产品推荐

