Spring Boot JPA更新行时的表访问锁问题及优化建议
解决建议
1. 替换表锁为用户维度的行级锁
表锁会锁定整张表,导致所有用户的交易请求排队,性能必然暴跌。针对同一用户的并发冲突,只需要锁定该用户的交易记录即可:
- 用JPA悲观锁查询用户最后一条交易记录:
在Repository的查询方法上添加@Lock(LockModeType.PESSIMISTIC_WRITE),示例:
该查询会锁定该用户的最后一条交易记录,其他事务要操作该用户的交易必须等待当前事务完成,从根源避免并发读取旧余额的问题。@Lock(LockModeType.PESSIMISTIC_WRITE) Transaction findTopByUserIdOrderByIdDesc(Long userId); - 或者用原生SQL/JPQL手动加锁:
@Query(value = "SELECT t FROM Transaction t WHERE t.userId = :userId ORDER BY t.id DESC FOR UPDATE", nativeQuery = false) Transaction findLatestTransactionForUpdate(@Param("userId") Long userId);
2. 新增用户余额字段(最彻底的解决方案)
放弃依赖交易记录的最后一条余额,在用户表中新增balance字段:
- 每次交易时,在同一个事务内先更新用户余额,再插入交易记录:
这种方式的并发性能远高于查询交易记录,数据一致性也更容易保证,乐观锁冲突的处理成本极低。@Transactional public void createTransaction(Long userId, BigDecimal amount) { User user = userRepository.findById(userId).orElseThrow(); // 给User类加version字段,JPA自动处理乐观锁冲突,冲突时只需重试 user.setBalance(user.getBalance().add(amount)); userRepository.save(user); Transaction transaction = new Transaction(); transaction.setUserId(userId); transaction.setAmount(amount); transaction.setBalance(user.getBalance()); transactionRepository.save(transaction); }
3. 原子化插入交易记录(不新增余额字段的替代方案)
如果必须保留现有表结构,用数据库原子操作实现余额计算和插入,避免"先查询后插入"的间隙:
- 用原生SQL的
INSERT ... SELECT语句,结合行级锁:
这条SQL会先锁定该用户的所有交易记录(防止其他事务插入),然后原子计算当前余额并插入新交易,完全避免并发冲突。@Modifying @Transactional @Query(value = "INSERT INTO transactions (user_id, amount, balance) " + "SELECT :userId, :amount, COALESCE(MAX(balance), 0) + :amount " + "FROM transactions WHERE user_id = :userId FOR UPDATE", nativeQuery = true) void insertTransactionWithBalance(@Param("userId") Long userId, @Param("amount") BigDecimal amount);
4. 检查事务配置有效性
之前加表锁无效,大概率是事务范围或锁时机不对:
- 确保锁操作和后续的插入操作在同一个
@Transactional方法内执行,否则查询结束后锁会立即释放,无法起到并发控制作用。 - Spring Boot 1.4.4默认事务隔离级别为
REPEATABLE_READ,足够覆盖这类场景,无需强行提升到SERIALIZABLE(会进一步降低性能)。
内容的提问来源于stack exchange,提问作者Carmine
相关产品推荐
相关产品推荐

