Spring JpaRepository仅ID存在时删除如何规避竞态条件
你判断的竞态问题确实存在,这是典型的TOCTOU(检查时间与使用时间差)问题,应用层分两次执行查询、删除操作,根本没法彻底规避并发下的逻辑漏洞。而且完全不需要靠try-catch处理异常,最优方案是直接走数据库原子删除,比你列的所有写法都更简洁、性能更好。
最优实现方案:自定义原子删除语句
直接在IngredientRepository中声明自定义JPQL删除方法,让删除逻辑在数据库侧单次执行完成,从根源上避免竞态,同时不会抛出EmptyResultDataAccessException:
public interface IngredientRepository extends JpaRepository<Ingredient, String> { // 返回值为SQL执行影响的行数 @Modifying @Query("DELETE FROM Ingredient i WHERE i.id = :id") int directDeleteById(String id); }
业务层的deleteIngredient方法直接调用即可,不需要任何前置查询、异常捕获:
@Transactional public boolean deleteIngredient(String id) { // 影响行数大于0说明删除成功,对应ID不存在时返回0,无任何异常 return ingredientRepository.directDeleteById(id) > 0; }
这个方案的优势非常明显:
- 仅执行1次SQL交互,比先查再删少一次数据库请求,性能更高
- 删除操作是数据库层面原子执行的,执行时会对匹配的行加锁,不存在并发下被其他线程抢先删除导致报错的问题
- 不需要处理任何异常,直接通过返回值就能判断删除结果,完全符合你的需求
注意事项
如果你的同一个事务内,在执行删除前已经查询过对应Ingredient实体到JPA持久化上下文(一级缓存),可以给@Modifying注解加上clearAutomatically = true属性,删除后自动清空持久化上下文,避免缓存与数据库数据不一致:
@Modifying(clearAutomatically = true) @Query("DELETE FROM Ingredient i WHERE i.id = :id") int directDeleteById(String id);
为什么不推荐其他方案
- 你提到的两种先查后删写法:除了存在竞态风险,还额外多了一次无意义的数据库查询,哪怕没有并发场景也属于性能浪费,完全不推荐使用。
- try-catch包裹
deleteById的写法:Spring Data JPA自带的deleteById方法本身内部就会先执行一次findById查询,再执行删除,不仅多一次DB交互,当ID不存在时会抛出异常,靠捕获异常做业务逻辑判断本身就有不必要的性能开销(异常栈生成成本很高),只适合作为没办法改Repository定义时的兜底方案,远不如自定义删除语句优雅。
内容的提问来源于stack exchange,提问作者Veidt
相关产品推荐
相关产品推荐

