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

仅依赖@Transactional能否保证码表并发安全?避免激活码重复分配

并发场景下激活码分配的问题分析与解决方案

仅靠@Transactional无法保证并发安全,很大概率会出现同一激活码被多个服务重复分配的情况。

问题根源

@Transactional默认使用**读已提交(READ_COMMITTED)**隔离级别,这个级别仅能保证读到的数据是其他事务已提交的结果,但阻止不了多个并发事务同时读取到状态为free的同一个激活码:

  1. 事务A和事务B同时执行findFirstByStatus("free"),都拿到了同一个Code对象;
  2. 两个事务各自修改该Code的状态为assigned并保存;
  3. 最终这个激活码会被绑定到两个不同的Service上,违反了"仅可使用一次"的规则。

可行的解决方案

1. 数据库悲观锁(最直接的方式)

在查询激活码时直接锁定记录,避免其他事务读取。修改CodeRepository的查询方法,使用select ... for update:

@Query("SELECT c FROM Code c WHERE c.status = 'free' ORDER BY c.id LIMIT 1 FOR UPDATE")
Optional<Code> findFirstFreeCodeForUpdate();

业务方法中调用该方法:

@Transactional
public void assignFreeCodeToService(Service service) {
    Code code = codeRepository.findFirstFreeCodeForUpdate()
        .orElseThrow(() -> new ApiException(ApiException.ErrorCode.NOT_FOUND, "No available Code was found"));
    code.setStatus("assigned");
    service.setCode(code.getId());
    // 无需手动调用save,事务提交时会自动同步修改
}

第一个事务拿到锁后,其他事务必须等待锁释放才能执行查询,从根源上避免重复读取。

2. 数据库乐观锁+重试机制

给Code表新增version字段(整数类型,默认值0),每次更新时校验版本号:

  • 修改Code实体类,添加JPA的@Version注解:
@Entity
public class Code {
    // 其他字段
    private String status;
    @Version
    private Integer version;
}
  • 业务方法中捕获乐观锁异常,重试分配逻辑:
@Transactional
public void assignFreeCodeToService(Service service) {
    int retryCount = 3; // 设置重试次数
    while (retryCount-- > 0) {
        try {
            Code code = codeRepository.findFirstByStatus("free")
                .orElseThrow(() -> new ApiException(ApiException.ErrorCode.NOT_FOUND, "No available Code was found"));
            code.setStatus("assigned");
            service.setCode(code.getId());
            return;
        } catch (OptimisticLockingFailureException e) {
            // 乐观锁冲突,重试
            continue;
        }
    }
    throw new ApiException(ApiException.ErrorCode.SYSTEM_ERROR, "Failed to assign code after retries");
}

当多个事务同时修改同一个Code时,只有一个会成功,其他会抛出OptimisticLockingFailureException,通过重试可获取下一个可用激活码。

3. 原子更新+循环重试

直接通过数据库的原子更新操作,确保只有一个事务能修改激活码状态:

  • 在CodeRepository中添加更新方法:
@Modifying
@Query("UPDATE Code c SET c.status = 'assigned' WHERE c.id = :id AND c.status = 'free'")
int updateCodeStatusToAssigned(@Param("id") Long id);
  • 业务方法中循环查找并尝试更新,直到成功:
@Transactional
public void assignFreeCodeToService(Service service) {
    while (true) {
        Code code = codeRepository.findFirstByStatus("free")
            .orElseThrow(() -> new ApiException(ApiException.ErrorCode.NOT_FOUND, "No available Code was found"));
        int updatedRows = codeRepository.updateCodeStatusToAssigned(code.getId());
        if (updatedRows == 1) {
            // 更新成功,绑定到服务
            service.setCode(code.getId());
            return;
        }
        // 更新失败,说明该码已被其他事务抢占,继续循环找下一个
    }
}

这种方式利用数据库行级锁,确保更新操作是原子的,只有第一个执行更新的事务会成功。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 03:42:52