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

