账户余额更新优化

2022-04-06  本文已影响0人  little多米

业务场景
用户预存一定余额,可以用余额在平台购买套餐商品,支付扣除余额需控制并发,当前采用的是乐观锁方式。即每个用户的余额记录都有一个版本号,更新记录时,需要带上版本号。版本号采用整数递增。
问题
当有两个扣减余额的操作同时发生时,其中一个有几率失败。失败结果直接返回给用户,此时用户操作重试即可,但会影响用户体验。如果一直处于高并发状态,用户可能会连续操作失败多次。主要针对此扣款失败场景进行优化。
方案演进
增加失败重试

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++;
}

进行上面重试修改之后,仍然存在失败日志


image.png

通过分析日志可知,失败时确实有三次重试,说明我们修改的代码是生效的。问题在于,失败后重新获取的记录值仍然是老的数据,版本号expectTxVersion没有变化。实际获取上次更新记录值如下。


image.png

怀疑是可能存在缓存,该方法使用的是mybatis框架,由于我们没有人为增加缓存,会不会是mybatis的缓存。经研究,mybatis默认是开启二级缓存的,于是通过在select方法上增加flushCache="true" useCache="false"配置去除缓存。


image.png

然后更新上线了,本以为就此结束,然而。。。还是一样的失败日志。
重新分析:更新失败说明版本号已经变更了,意味着其他修改已经提交入库了。
为什么没有读到其他事务的最新数据呢,研究一下事务的隔离级别。
查看mysql默认的隔离级别:

select @@transaction_isolation;
image.png

默认为:可重复读,看下该级别的定义。

一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。

因为获取记录操作是在事务中,所以重复获取不能得到最新数据。
因此,可以将数据获取排除到事务之外,主要用spring的事务传递管理,设置为Propagation.NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

image.png

再看日志,虽然也有失败,但基本重试一次之后就成功。


image.png

至此,问题解决。
总结
本以为是一个简单的重试优化,逐渐引出mybatis二级缓存和数据库的事务管理。任何一个点的遗漏都达不到想要的效果。平时的知识储备是必要的,否则遇到问题时将花费成倍的时间。

上一篇下一篇

猜你喜欢

热点阅读