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

Spring+Hibernate+SQL Server下serializable隔离级并发保存配置出现多余插入问题

问题根源

你遇到的是SQL Server Serializable隔离级别的经典特性:Serializable级别会为查询的范围加键范围锁,当两个事务都读取同一个空范围(A1关联的B列表为空)时,两者会同时持有该空范围的共享键范围锁,后续两个事务各自插入属于该范围的B数据时,都不会违反对方的锁规则,因此两个事务都能成功提交,最终出现超出预期的B实例。这个行为符合ANSI Serializable隔离级别的定义,不属于数据库实现bug。

可行解决方案

下面给出几种落地成本从低到高的方案,你可以根据业务并发量级选择:

  • 方案1:查询A实例时加悲观写锁
    直接在查询A实体的DAO方法上添加@Lock(LockModeType.PESSIMISTIC_WRITE)注解,事务读取A实例时会直接对A行加独占锁,后续并发事务读取同一个A实例时会被阻塞,直到前一个事务提交释放锁,从根源避免两个事务同时操作同一个A的关联B列表的问题。该方案改动极小,适合绝大多数业务场景。
  • 方案2:给A实体加乐观锁
    给A实体添加@Version注解的版本号字段,每次修改A的关联B列表时,Hibernate会自动校验A的版本号:如果两个并发事务拿到的是同一个版本的A实例,第一个提交的事务会将版本号+1,第二个提交的事务会触发乐观锁异常,你捕获异常后做业务重试即可。该方案无锁阻塞,适合并发量不高的场景。
  • 方案3:前置写操作抢占锁
    事务启动后先执行一个针对A实例的无业务副作用的更新操作,比如更新A的last_modify_time字段为当前时间,该操作会直接给A行加独占锁,后续并发事务更新同一个A时会被阻塞,效果和悲观锁一致,适合不方便修改查询加锁逻辑的场景。
  • 方案4:调整查询逻辑加范围排他锁
    如果你一定要保留Serializable隔离级别,可以在读取A的关联B列表前,先执行原生SQL:SELECT * FROM b WHERE a_id = ? FOR UPDATE,手动给该A对应的B范围加排他锁,避免两个事务同时持有共享范围锁。

注意:无论用哪种锁方案,业务逻辑都要遵循「先查全量现有未删除B -> 比对配置标记删除不在配置内的旧B -> 新增配置内不存在的新B」的顺序,不要直接清空原有B列表再全量插入,避免不必要的IO和数据冗余。

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

相关产品推荐
方舟 Agent Plan

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

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