问题描述
一个更新计费参数接口,按钮连点导致数据未更新问题。
背景
接口内容逻辑,在一个事物内,先保存更新计费参数,再根据计费参数,重新计算费用,并刷新计费单,结算单,支付单等单据金额信息。
按理来讲,这个接口是具备幂等性的,因为即便多次更新,也只是重新计算一遍,数据结果不会改变。
但这个问题现象是两次并发操作导致数据不发生变化,相当于一次操作更新了,但另一次操作给还原了。
过程分析
事物1 | 事物2 |
---|---|
select * from table where id = ?; // 对数据做校验 | select * from table where id = ?; // 对数据做校验 |
// 事物2 更新语句先执行,锁住当前行,等待事物2结束 | update table set fee = ?,update_time = ? where id = ?; //保存费用配置并通过更新当前事物数据为最新行数据 //调用统一刷新费用方法 select * from table where id = ?; // 查询最新数据 update table set fee = ?, amount = ?, update_time = ? where id = ?; //更新业务数据(根据fee新值,计算出新值 amount 并更新) //事物结束,释放行锁 |
update table set fee = ?, amount = ?,update_time = ? where id = ?; //保存费用配置并通过更新当前事物数据为最新行数据,(因另一事物已更新,SQL执行成功,但因数据一样,未实际更新数据) | |
//调用统一刷新费用方法 select * from table where id = ?; // 查询最新数据(MVCC 旧数据) |
|
update table set fee = ?, amount = ?,update_time = ? where id = ?; //更新业务数据(导致根据fee旧值,计算出旧值 amount 并更新覆盖新值 ) |
因操作连点,两个事物在1秒内完成,因此更新时间的差额在毫秒级,数据库更新时间字段使用 timestamp 类型精确到秒,导致事物2执行完后,事物1第一次更新时,更新数据和数据库行数据是一致的,所以更新sql执行了,但没有实际更新数据,导致当前事物还是快照读,因此当前事物看不到最新行数据,还是使用事物1原本的数据版本号,也就是老数据。
所以在统一刷新费用方法中,重新根据id查询时,还是老数据,导致费用数据又备更新为老数据。
解决办法
- 数据库更新时间字段使用 timestamp(3),精确到毫秒级,保证更新成功,事物当前读,可以获取到最新行数据。
- 事物开头主动锁行,使用当前读,避免使用快照读:select * from table where id = ? for update,保证获取最新数据并阻塞其他事物。
- 操作增加分布式锁,锁粒度为当前订单,避免同时操作同一订单的修改操作。
- 前端按钮增加连点控制。