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

Hibernate多对多关联场景并发插入触发唯一约束违反问题解决方案咨询

问题根因分析
  • 典型的查询-插入竞态条件:你实现的findBy校验和实际插入操作之间存在时间差,并发场景下两个事务同时查询到id=1的project不存在,都会触发新建逻辑,第二个事务插入时就会触发唯一键冲突。
  • 多对多级联配置错误:你给Project侧的@ManyToMany加了CascadeType.PERSIST,User侧还加了CascadeType.ALL,关联已有项目时会触发级联插入已存在的项目记录,直接触发冲突。
  • 业务逻辑存在笔误:你代码里的ProjectRepository.save(user)是明显错误,应该传入project对象保存,这也会导致保存逻辑完全不符合预期。
  • 现有重试逻辑未适配事务特性:如果重试操作在原事务内执行,在MySQL默认的可重复读隔离级别下,事务内查询会复用第一次的快照,还是会读到项目不存在的旧结果,重试也无法解决问题。
修复方案

1. 修正实体映射配置

移除不必要的级联操作,多对多关联仅需要操作中间表,不需要级联修改主表数据:

// User侧修改,移除CascadeType.ALL
@ManyToMany(mappedBy = "usersSet", fetch = FetchType.LAZY)
private Set<Project> projectsSet;

// Project侧修改,移除PERSIST级联
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "users_projects", schema = "abcd", 
    joinColumns = {@JoinColumn(name = "_projectId")},
    inverseJoinColumns = {@JoinColumn(name = "_userId")})
private Set<User> usersSet;

2. 修正业务逻辑,新增Upsert兼容并发场景

调整逻辑顺序,先处理关联用户,再通过插入冲突兜底逻辑避免并发问题:

@Transactional
public void saveProject(ProjectDTO projectDTO, Set<UserDTO> userDTOSet) {
    // 1. 先处理所有关联用户:不存在则新建,存在则取已有
    Set<String> userIds = userDTOSet.stream().map(UserDTO::getUserId).collect(Collectors.toSet());
    List<User> existUsers = getUsersFromDB(userIds);
    Set<String> existUserIds = existUsers.stream().map(User::getUserId).collect(Collectors.toSet());
    // 新建不存在的用户
    List<User> newUsers = userDTOSet.stream()
        .filter(dto -> !existUserIds.contains(dto.getUserId()))
        .map(this::convertToModel)
        .collect(Collectors.toList());
    if (!newUsers.isEmpty()) {
        userRepository.saveAll(newUsers);
        existUsers.addAll(newUsers);
    }

    // 2. 处理项目:插入冲突兜底避免并发问题
    Project project = getProjectFromDB(projectDTO.getProjectId())
        .orElseGet(() -> {
            try {
                // 优先尝试插入
                Project newProject = new Project();
                BeanUtils.copyProperties(newProject, projectDTO);
                return projectRepository.save(newProject);
            } catch (ConstraintViolationException e) {
                // 插入冲突说明其他线程已插入,直接查询已有数据返回
                return getProjectFromDB(projectDTO.getProjectId())
                    .orElseThrow(() -> new CustomException("项目查询失败", e));
            }
        });

    // 3. 关联项目和用户,仅更新中间表
    project.getUsersSet().addAll(existUsers);
    projectRepository.save(project);
}

3. 额外优化建议

  • 把事务隔离级别调整为读已提交,避免可重复读隔离级别下事务内读快照导致的查询结果过期问题
  • 如果需要保留重试逻辑,要在新事务中执行重试,不要复用旧事务
  • 并发量很高的场景,可以在查询项目前针对projectId加分布式锁,从源头减少冲突概率

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 01:30:00