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

