订单系统的ABA问题

订单系统的ABA问题

Content #

那在实现更新订单服务时,在并发环境下,你需要注意 ABA 问题。

什么是 ABA 问题呢?比如说,订单支付之后,小二要发货,发货完成后要填个快递单号。假设说,小二填了一个单号 666,刚填完,发现填错了,赶紧再修改成 888。对订单服务来说,这就是 2 个更新订单的请求。

正常情况下,订单中的快递单号会先更新成 666,再更新成 888,这是没问题的。那不正常情况呢?666 请求到了,单号更新成 666,然后 888 请求到了,单号又更新成 888,但是 666 更新成功的响应丢了,调用方没收到成功响应,自动重试,再次发起 666 请求,单号又被更新成 666 了,这数据显然就错了。这就是非常有名的 ABA 问题。

具体的时序你可以参考下面这张时序图:

ABA 问题怎么解决?这里给你提供一个比较通用的解决方法。给你的订单主表增加一列,列名可以叫 version,也即是“版本号”的意思。每次查询订单的时候,版本号需要随着订单数据返回给页面。页面在更新数据的请求中,需要把这个版本号作为更新请求的参数,再带回给订单更新服务。

订单服务在更新数据的时候,需要比较订单当前数据的版本号,是否和消息中的版本号一致,如果不一致就拒绝更新数据。如果版本号一致,还需要再更新数据的同时,把版本号 +1。“比较版本号、更新数据和版本号 +1”,这个过程必须在同一个事务里面执行。

具体的 SQL 可以这样来写:

UPDATE orders set tracking_number = 666, version = version + 1
WHERE version = 8;

在这条 SQL 的 WHERE 条件中,version 的值需要页面在更新的时候通过请求传进来。

通过这个版本号,就可以保证,从我打开这条订单记录开始,一直到我更新这条订单记录成功,这个期间没有其他人修改过这条订单数据。因为,如果有其他人修改过,数据库中的版本号就会改变,那我的更新操作就不会执行成功。我只能重新查询新版本的订单数据,然后再尝试更新。

有了这个版本号,再回头看一下我们上面那个 ABA 问题的例子,会出现什么结果?可能出现两种情况:

  1. 第一种情况,把运单号更新为 666 的操作成功了,更新为 888 的请求带着旧版本号,那就会更新失败,页面提示用户更新 888 失败。

  2. 第二种情况,666 更新成功后,888 带着新的版本号,888 更新成功。这时候即使重试的 666 请求再来,因为它和上一条 666 请求带着相同的版本号,上一条请求更新成功后,这个版本号已经变了,所以重试请求的更新必然失败。

无论哪种情况,数据库中的数据与页面上给用户的反馈都是一致的。这样就可以实现幂等更新并且避免了 ABA 问题。下图展示的是第一种情况,第二种情况也是差不多的:

Viewpoints #

From #

01 | 创建和更新订单时,如何保证数据准确无误?