如何避免停车服务中含车辆-停车场ID对的数据库表出现竞态条件
解决停车服务并发竞态条件的方案
这个并发抢车位的坑我之前在做园区停车系统的时候踩过!乐观锁确实不适合你这种关联配对表的场景,给你几个实用的解决方案,从简单到复杂都有:
1. 数据库层面原子操作(最推荐,无额外依赖)
核心思路是把「查询剩余车位」和「插入配对记录」变成一个原子性的操作,让数据库帮你保证并发安全,彻底避免竞态。
方案A:利用INSERT ... SELECT条件插入
直接写一条带判断逻辑的插入SQL,只有当停车场还有空位时才会执行插入,数据库会自动保证这个操作的原子性。
示例SQL(适配你的表结构):
INSERT INTO cars_on_parkings_ids (ID, ID_CAR, ID_PARKING) SELECT :idOfPair, :carID, :parkingID FROM dual -- Oracle用dual,MySQL可以省略或用FROM (SELECT 1) AS tmp WHERE ( -- 统计非电动车占用的车位数量 SELECT COUNT(*) FROM CarOnParkingEnity cp INNER JOIN CarEntity c ON c.idCar = cp.idCar WHERE c.powerType NOT LIKE 'ELECTRIC' AND cp.idParking = :parkingID ) < ( -- 获取停车场总车位数 SELECT numberOfParkingSlots FROM ParkingEntity WHERE idParking = :parkingID );
在代码里执行这条SQL后,判断影响行数:
- 如果影响行数为1,说明插入成功(车位抢到了),直接抬杆放行;
- 如果影响行数为0,说明车位已满,返回提示。
方案B:行锁+事务
在同一个事务里,先锁定对应的停车场记录,再执行查询和插入操作,这样其他并发请求会被阻塞,直到当前事务完成。
示例Spring Data JPA代码:
@Transactional public ResponseEntity<?> handleCarEntry(String parkingId, CarAtGateModel carAtGateModel) { // 锁定停车场记录,防止并发修改(Hibernate可改用findById(parkingId).withLock(LockModeType.PESSIMISTIC_WRITE)) ParkingEntity parking = parkingRepository.findById(parkingId).lock(); // 统计已占用车位 Integer takenSlots = carsOnParkingsRepository.findAmountOfTakenSlotsOnParking(parkingId); if (takenSlots < parking.getNumberOfParkingSlots()) { // 插入配对记录,此操作必须在同一事务内 return setCarToParking(carAtGateModel); } else { return new ResponseEntity<>(responseMessage.everySlotIsTaken(), HttpStatus.OK); } }
2. 应用层分布式锁(适合多实例部署场景)
如果你的服务是多实例部署的,JVM级别的锁没用,这时候可以用Redis或ZooKeeper实现分布式锁,保证同一时间只有一个请求能处理某停车场的进场逻辑。
以Redis的Redisson客户端为例:
@Autowired private RedissonClient redissonClient; public ResponseEntity<?> handleCarEntry(String parkingId, CarAtGateModel carAtGateModel) { // 针对每个停车场生成唯一锁key RLock lock = redissonClient.getLock("parking:entry:lock:" + parkingId); try { // 尝试获取锁:最多等待5秒,持有锁10秒(根据实际业务调整超时时间) if (lock.tryLock(5, 10, TimeUnit.SECONDS)) { Integer takenSlots = carsOnParkingsRepository.findAmountOfTakenSlotsOnParking(parkingId); ParkingEntity parking = parkingRepository.findById(parkingId).orElseThrow(); if (takenSlots < parking.getNumberOfParkingSlots()) { return setCarToParking(carAtGateModel); } else { return new ResponseEntity<>(responseMessage.everySlotIsTaken(), HttpStatus.OK); } } else { // 获取锁失败,返回"请稍后重试"或者直接拒绝 return new ResponseEntity<>(responseMessage.tryAgainLater(), HttpStatus.TOO_MANY_REQUESTS); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); return new ResponseEntity<>(responseMessage.systemError(), HttpStatus.INTERNAL_SERVER_ERROR); } finally { // 确保锁被正确释放 if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }
3. 优化表结构,增加原子计数字段
在ParkingEntity表中新增一个occupied_non_electric_slots字段,专门统计非电动车占用的车位数量。每次车辆进场时,先原子性地更新这个计数(只有有空位时才会成功),再插入配对记录。
步骤:
- 修改停车场表,新增字段:
occupied_non_electric_slots INT DEFAULT 0 - 编写原子更新的Repository方法:
@Modifying @Query(value = "UPDATE ParkingEntity p SET p.occupiedNonElectricSlots = p.occupiedNonElectricSlots + 1 " + "WHERE p.idParking = :parkingId AND p.occupiedNonElectricSlots < p.numberOfParkingSlots") int incrementNonElectricOccupiedSlots(@Param("parkingId") String parkingId);
- 业务逻辑代码:
@Transactional public ResponseEntity<?> handleCarEntry(String parkingId, CarAtGateModel carAtGateModel) { // 原子性更新计数,只有有空位时才会返回1 int updatedRows = parkingRepository.incrementNonElectricOccupiedSlots(parkingId); if (updatedRows > 0) { // 更新成功,插入配对记录 return setCarToParking(carAtGateModel); } else { return new ResponseEntity<>(responseMessage.everySlotIsTaken(), HttpStatus.OK); } }
- 车辆离场时,记得原子性减少这个计数:
@Modifying @Query(value = "UPDATE ParkingEntity p SET p.occupiedNonElectricSlots = p.occupiedNonElectricSlots - 1 " + "WHERE p.idParking = :parkingId AND p.occupiedNonElectricSlots > 0") int decrementNonElectricOccupiedSlots(@Param("parkingId") String parkingId);
内容的提问来源于stack exchange,提问作者Lulex97
相关产品推荐
相关产品推荐

