Spring Boot中先校验实体存在再删除的原子性与线程安全问题
我在Spring Boot应用中实现了Service层与Repository层,使用Spring Data、Spring MVC等技术。需求为删除数据库实体前先校验其是否存在,若不存在则抛出EntityNotFoundException。现有示例代码如下:
Repository代码
public interface RoomRepository extends CrudRepository<Room, Long> { @Query("from Room r left join fetch r.messages where r.id = :rId") Optional<Room> findByIdWithMessages(@Param("rId") long id); @Override List<Room> findAll(); }
Service代码
@Service @Loggable public class RoomService implements GenericService<Room> { private final RoomRepository roomRepository; private final RoomDtoMapper roomMapper; public RoomService(RoomRepository roomRepository, RoomDtoMapper roomMapper) { this.roomRepository = roomRepository; this.roomMapper = roomMapper; } @Override public Room getById(long id) { return roomRepository.findById(id).orElseThrow( () -> new EntityNotFoundException(String.format("room with id = %d wasn't found", id))); } @Override public void delete(Room room) { getById(room.getId()); roomRepository.delete(room); } }
当前delete方法先调用getById校验实体存在性,再执行删除操作,但该代码非线程安全、操作不原子——校验后可能有其他线程已删除该实体。请问是否需添加@Transactional注解实现原子性?是否需要设置事务隔离级别?
回答
1. 必须添加@Transactional保证原子性
当前代码中getById和delete是两个独立的数据库操作,中间存在时间窗口,其他线程完全可能在这两个操作之间删除目标实体,导致delete操作执行时实体已经不存在,但你的代码无法感知到这种情况(因为校验已经通过)。
给delete方法或者整个Service类添加@Transactional注解,能将这两个操作纳入同一个数据库事务,保证它们的原子性——要么两个操作都执行成功,要么都不执行,同时事务会锁定相关资源(取决于数据库的锁机制),避免其他线程在事务执行期间修改目标实体。
修改后的delete方法示例:
@Override @Transactional public void delete(Room room) { getById(room.getId()); roomRepository.delete(room); }
2. 更高效的原子性实现方案
其实可以优化逻辑,减少一次数据库查询,直接通过existsById校验+删除的方式实现原子性,同样在事务包裹下:
@Override @Transactional public void delete(long id) { if (!roomRepository.existsById(id)) { throw new EntityNotFoundException(String.format("room with id = %d wasn't found", id)); } roomRepository.deleteById(id); }
如果使用Spring Data JPA 2.0+,还可以直接利用deleteById的返回值(部分实现返回删除的行数)来判断:
@Override @Transactional public void delete(long id) { long deletedRows = roomRepository.deleteById(id); if (deletedRows == 0) { throw new EntityNotFoundException(String.format("room with id = %d wasn't found", id)); } }
这种方式不仅更高效,还能同样保证校验和删除的原子性。
3. 事务隔离级别无需额外调整
Spring默认的事务隔离级别是READ_COMMITTED,已经完全满足这个场景的需求:它能保证你读取到的是已经提交的有效数据,避免脏读,同时事务内的操作不会被其他事务的中间状态干扰。除非你的业务有特殊的并发需求(比如需要避免不可重复读、幻读),否则不需要修改隔离级别。
内容的提问来源于stack exchange,提问作者WantBecomeJavaCoder

