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

如何避免停车服务中含车辆-停车场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字段,专门统计非电动车占用的车位数量。每次车辆进场时,先原子性地更新这个计数(只有有空位时才会成功),再插入配对记录。

步骤:

  1. 修改停车场表,新增字段:occupied_non_electric_slots INT DEFAULT 0
  2. 编写原子更新的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);
  1. 业务逻辑代码:
@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);
    }
}
  1. 车辆离场时,记得原子性减少这个计数:
@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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 13:37:32