Spring Boot@Transactional传播隔离级别下并发更新丢数据问题
问题分析与解决方案
你的问题核心是并发更新同一行数据的不同字段时出现更新丢失,虽然配置了REQUIRES_NEW和SERIALIZABLE事务,但未解决问题,主要原因可能有以下几点:
可能的原因
- 全量更新导致覆盖:你的
updateBook方法应该是把整个Book对象转换成BookDBEntity后全量更新所有字段。当多个并发事务同时读取到旧的Book数据,各自修改不同的作者字段,最后提交时,后提交的事务会覆盖先提交事务的修改。 - 事务隔离级别与数据库实现差异:
SERIALIZABLE隔离级别理论上会让事务串行执行,但部分数据库(如PostgreSQL)的SERIALIZABLE是基于快照隔离实现的,并非严格的串行执行,依然可能出现更新覆盖。 - 缓存干扰:如果
bookRepository.getBookById读取的是内存缓存(比如Spring Data JPA的一级缓存),即使开启REQUIRES_NEW事务,也可能拿到旧数据,导致更新基于过期状态。
解决方案
方案1:使用乐观锁(推荐)
通过版本号机制实现乐观锁,每次更新时校验版本号,冲突时触发重试,从根本上避免更新丢失。
步骤:
- 在
BookDBEntity中添加版本号字段:
public class BookDBEntity { private String id; private String title; // 作者字段... @Version // JPA乐观锁注解,自动管理版本号 private int version; }
- 在Service中添加重试逻辑,处理乐观锁冲突:
@Service @Transactional(propagation = Propagation.REQUIRES_NEW) public class EventSaveBookUseCase implements EventSaveBookPort { private final BookRepository bookRepository; private final RetryTemplate retryTemplate; // 注入Spring Retry模板 public EventSaveBookUseCase(BookRepository bookRepository, RetryTemplate retryTemplate) { this.bookRepository = bookRepository; this.retryTemplate = retryTemplate; } @Override public void updateBook(EventUpdateBookCommand command) { retryTemplate.execute(context -> { // 每次重试都从数据库读取最新数据 Book book = bookRepository.getBookById(command.getBookId()); book.setAuthor(book, command.getAuthor()); bookRepository.updateBook(book); return null; }); } }
- 配置RetryTemplate(Spring Boot示例):
@Configuration public class RetryConfig { @Bean public RetryTemplate retryTemplate() { RetryTemplate retryTemplate = new RetryTemplate(); SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(); retryPolicy.setMaxAttempts(3); // 最多重试3次 retryTemplate.setRetryPolicy(retryPolicy); return retryTemplate; } }
方案2:按需更新+行级锁
避免全量更新,只修改需要更新的字段,同时通过行级锁强制串行读取,防止并发读取旧数据。
步骤:
- 在Repository中定义按需更新的方法(以JPA为例):
public interface BookRepository extends JpaRepository<BookDBEntity, String> { @Query("UPDATE BookDBEntity b SET b.author1Name = :name, b.author1Age = :age WHERE b.id = :id") @Modifying @Transactional void updateAuthor1(@Param("id") String id, @Param("name") String name, @Param("age") int age); // 同理实现author2、author3、author4的更新方法 }
- 修改Service逻辑,根据命令类型调用对应的更新方法:
@Service @Transactional(propagation = Propagation.REQUIRES_NEW) public class EventSaveBookUseCase implements EventSaveBookPort { private final BookRepository bookRepository; public EventSaveBookUseCase(BookRepository bookRepository) { this.bookRepository = bookRepository; } @Override public void updateBook(EventUpdateBookCommand command) { Author author = command.getAuthor(); String bookId = command.getBookId(); // 假设command有authorType字段标识要更新的是哪个作者 switch(command.getAuthorType()) { case AUTHOR1: bookRepository.updateAuthor1(bookId, author.getName(), author.getAge()); break; case AUTHOR2: bookRepository.updateAuthor2(bookId, author.getName(), author.getAge()); break; case AUTHOR3: bookRepository.updateAuthor3(bookId, author.getName(), author.getAge()); break; case AUTHOR4: bookRepository.updateAuthor4(bookId, author.getName(), author.getAge()); break; } } }
- 如果需要更严格的串行,可以在查询时加行锁:
@Query("SELECT b FROM BookDBEntity b WHERE b.id = :id FOR UPDATE") BookDBEntity getBookByIdForUpdate(@Param("id") String id);
这样其他事务必须等待当前事务提交后才能读取该行数据,彻底避免并发读取旧数据。
方案3:排查缓存问题
确保bookRepository.getBookById每次都从数据库读取最新数据:
- 如果使用Spring Data JPA,
REQUIRES_NEW事务会创建新的事务上下文,默认不会复用旧的EntityManager缓存; - 检查Repository是否自定义了静态缓存逻辑(比如用Map缓存数据),如果有,移除或改为直接从数据库读取。
内容的提问来源于stack exchange,提问作者newbee
相关产品推荐
相关产品推荐

