场景描述
最近,生产系统遇到了一个mysql数据更新的并发问题,业务场景是这样的:
- WMS每次退货商品入库后,通知支付系统退款,支付系统退款成功后,向退款事件表中插入退款信息
- 退款处理作业轮询退款成功事件,获取待处理的任务
- 根据退货单号,查询已退款信息,并将支付事件中的金额与已退金额相加,进行更新,如果已退金额达到了退货申请金额,则更新退货单状态为“已完成”。
生产问题
支付系统推送统一退货单的两次退款事件的时间非常接近,几乎同时插入到事件表中,导致作业在执行时将两次时间也几乎在同一时间进行处理,而数据库此时并没有加锁,两次作业读取的已退金额都是0,然后进行相加计算后,更新数据库记录时就导致后更新的数据覆盖了先更新的记录,导致数据丢失,退货单状态无法变更到“已完成”。
如上图示意,最终正确结果本应该是refund_amount = 30 ,但是因为被覆盖了,所以最终会丢失一次入库的数据。
解决方法
数据库进行更新时对该记录进行加锁。
1 悲观锁
每次执行查询时,通过for update来对需要更改的记录进行锁定,这样下次更改只能等待第一次修改完成后才能进行第二次读取,因此计算出的结果可以保证正确性。具体实现:将原来的语句
select * from refund_order where id=#{id}
更改为
select * from refund_order where id=#{id} for update
因为当系统的流量其实并不高,为了快速解决此问题,暂时采用这种快速修复的方法,当然也可以考虑用乐观锁进行更新。
2 乐观锁
乐观锁就是在进行更新是,对当期读取到的值与数据库值进行比较,例如更新前读取数据库的值为0,更新时也需要加上where条件refund_amount=0, 这样如果当前更新操作被其他线程抢先执行了,则会更新失败,然后通过作业重新执行来避免并发问题。乐观锁适合并发发生情况比较少的情况,因为 如果一直更新失败,效率反而不如使用悲观锁高。还有要注意的问题是当前的场景下因为都是整数更新,所以不会产生aba的问题,如果有负数更新, 则需要考虑aba问题。