记录JPA并发save时遇到的坑

本文分析了一个在JPA中使用save方法时遇到的问题,当多个线程同时尝试更新同一用户钱包时,可能导致并发问题。通过详细描述业务场景和伪代码,解释了在用户钱包流水变化时,由于save方法导致的更新覆盖。提出了解决方案,即自定义save方法以确保insert操作,避免并发更新冲突,并利用幂等性更新钱包余额。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

在JPA中,使用save方法时是这样的:如果我们save的对象指定了主键,那么会根据主键先进行一次查询,如果查询记录不存在则执行insert语句,如果查询记录存在则执行update语句。

问题现象

业务场景是这样的:当某个用户钱包流水发生变化时,我们会先查询用户钱包是否存在(新注册的用户一开始没有钱包),如果存在则直接更新钱包余额,并添加一条流水,如果用户钱包不存在,则新生成钱包,再更新钱包余额,最后添加流水。

当同一个用户同时产生多条流水时,现在的流程则可能出现问题。

伪代码

// 查询用户钱包是否存在
User u = select(userId);
if(u == null){
	// 不存在则生成钱包,初始化钱包余额为0
	jpa.save(u);
}
// 添加流水
addFlow(u);
// 更新钱包余额
update(u);

问题分析

在这里插入图片描述

问题就发生在当第一个线程save成功后,第二个线程再执行时,save就会变成update,并且会覆盖第一个线程所执行的操作。

解决方法

只要让自定义save方法,就是insert操作就可以了,当第2个线程save时,会报主键冲突,然后第2个线程再重试一次,再次查询钱包是否存在时,就可以查询到了,然后就不用再生成钱包了,直接更新余额即可(更新余额幂等性)。

### JPA `save` 和 `saveAll` 方法的用法及区别 #### 使用场景与功能描述 在 Spring Data JPA 中,`save` 和 `saveAll` 是用于持久化实体到数据库的方法。这两个方法都属于 `CrudRepository` 接口的一部分。 - **`save` 方法**:此方法用于保存单个实体实例。如果该实体已存在,则更新现有记录;如果不存在,则插入新记录[^1]。 ```java MyEntity entity = new MyEntity(); // 设置属性... myEntityRepository.save(entity); ``` - **`saveAll` 方法**:此方法接受一个可迭代对象(如列表),并批量保存多个实体实例。对于每个实体,逻辑同 `save` 方法一致——即根据是否存在来决定是执行更新还是插入操作。 ```java List<MyEntity> entities = Arrays.asList(new MyEntity(), new MyEntity()); // 批量设置属性... myEntityRepository.saveAll(entities); ``` 需要注意的是,虽然 `save` 属于原始 Hibernate 提供的功能,但在现代版本中它已经被很好地集成到了 JPA 的标准流程里,并且遵循了 JPA 对事务管理的要求。而 `saveAll` 则是为了方便处理多条数据的一次性写入需求而设计,在性能上可能优于逐个调用 `save` 进行多次单独的操作。 当遇到并发冲突或其他异常情况JPA 或者底层实现框架如 Hibernate 并不会自动返回最新的失败对象版本给用户端程序去决策如何继续下一步动作[^4]。因此开发者应当考虑捕获特定类型的异常并通过查询机制获取最新状态来进行适当响应。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码拉松

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值