业务场景
用户预存一定余额,可以用余额在平台购买套餐商品,支付扣除余额需控制并发,当前采用的是乐观锁方式。即每个用户的余额记录都有一个版本号,更新记录时,需要带上版本号。版本号采用整数递增。
问题
当有两个扣减余额的操作同时发生时,其中一个有几率失败。失败结果直接返回给用户,此时用户操作重试即可,但会影响用户体验。如果一直处于高并发状态,用户可能会连续操作失败多次。主要针对此扣款失败场景进行优化。
方案演进
增加失败重试
int i = 0, max = 3;//最多尝试3次
while (i < max && !success) {
//获取余额记录
AgentRechargeEntity arEntity = agentRechargeService.findByAgentId(context.getAdminUserEntity().getAgentId());
//版本记录值,用于控制并发操作
Integer exceptTxVersion = arEntity.getTxVersion();
//修改金额计算
//更新余额
success = agentRechargeService.updateMoneyByExpectTxVersion(id, exceptTxVersion, money);
i++;
}
进行上面重试修改之后,仍然存在失败日志
![](https://img.haomeiwen.com/i10134940/1054d78dc8c42b81.png)
通过分析日志可知,失败时确实有三次重试,说明我们修改的代码是生效的。问题在于,失败后重新获取的记录值仍然是老的数据,版本号expectTxVersion没有变化。实际获取上次更新记录值如下。
![](https://img.haomeiwen.com/i10134940/bfb16f7f992d0f24.png)
怀疑是可能存在缓存,该方法使用的是mybatis框架,由于我们没有人为增加缓存,会不会是mybatis的缓存。经研究,mybatis默认是开启二级缓存的,于是通过在select方法上增加flushCache="true" useCache="false"配置去除缓存。
![](https://img.haomeiwen.com/i10134940/3d8f35e9e159d284.png)
然后更新上线了,本以为就此结束,然而。。。还是一样的失败日志。
重新分析:更新失败说明版本号已经变更了,意味着其他修改已经提交入库了。
为什么没有读到其他事务的最新数据呢,研究一下事务的隔离级别。
查看mysql默认的隔离级别:
select @@transaction_isolation;
![](https://img.haomeiwen.com/i10134940/66fd1cc2cd837605.png)
默认为:可重复读,看下该级别的定义。
一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。
因为获取记录操作是在事务中,所以重复获取不能得到最新数据。
因此,可以将数据获取排除到事务之外,主要用spring的事务传递管理,设置为Propagation.NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
![](https://img.haomeiwen.com/i10134940/a2cf3e0c263e226e.png)
再看日志,虽然也有失败,但基本重试一次之后就成功。
![](https://img.haomeiwen.com/i10134940/6a015891f49ad815.png)
至此,问题解决。
总结
本以为是一个简单的重试优化,逐渐引出mybatis二级缓存和数据库的事务管理。任何一个点的遗漏都达不到想要的效果。平时的知识储备是必要的,否则遇到问题时将花费成倍的时间。
网友评论