创建和更新订单时数据一致性

1,813 阅读4分钟

一、订单系统核心功能

  1. 创建订单;
  2. 随着购物流程更新订单状态;
  3. 查询功能,包括用订单数据生成各种报表。

为了支持这些功能,理论上来说数据库层面必须如如下几张表,订单主表和其他几个子表都是一对多的关系,且通过外键关联:

  • 订单主表:保存订单的基本信息;
  • 订单商品表: 保存订单中的商品信息;
  • 订单支付表: 保存订单的支付和退款信息;
  • 订单优惠表: 保存定案使用的优惠信息。

二、保证幂等,避免重复下单

场景:

对于一个订单系统,会给前端提供一个HTTP接口,用户在浏览器页面点击“提交订单”功能时,浏览器就会发送给后端一个创建订单的请求,订单系统的后端服务就会往数据库里面插入一条订单数据,创建订单成功。
假如网络抖动导致重传,或者RPC框架带有自动重试机制,后端就可能会产生多条一样的订单。

解决方案:

保证服务幂等。
可以利用数据库“主键具有唯一性”这个特性,在订单系统中增加一个“生成订单号”的服务,这个服务不需要请求参数,返回值是一个新的、全局唯一的订单号。在用户进入创建订单的页面,首先调用这个订单号服务生成一个订单号,然后在用户提交页面的时候,在创建订单的请求中带上这个订单号。此时,无论重试多少次,只要是同一个订单号,只会插入一次,保证了幂等性。
需要注意一点的是,假如在重试的过程中,发现是因为重复insert到数据库失败,此时,订单服务应该返回订单创建成功的页面。

三、解决ABA问题

场景:

假设用户和商户协商之后要修改商品的价格,想把价格修改为5200,第一次刚提交完,发现不小心写成了520,赶紧再修改成5200。对于订单系统来说,会收到两个更新请求。正常情况下,第一个请求把价格更新为520,第二个请求再把价格更新为5200,是OK的。但是假设第一个请求因为网络原因超时了,然后第二个请求先到,此时价格会被修改为5200,但是第一个请求因为超时、调用方触发了自动重试机制,然后价格又会被更新为520,此时金额就有问题了,会给商家带来资损。这就是ABA问题。

解决方案:

通用的解决方案是给订单表增加version版本号这个列。每次查询订单的时候,这个版本号和订单号一起返回给调用方。然后再更新操作的时候,也一起把这个版本号传过来。在具体更新的时候,就会比较当前数据库里订单的版本号和传参中的版本是否一致,如果不一致,就拒绝更新。如果一致,就同时更新订单号和版本号+1。

四、注意点

  1. 因为网络、服务器等等不确定的因素,请求重试是不可避免的,所以接口幂等性是解决此类问题的关键;
  2. 生成全局唯一的订单号,如何是小规模系统可以用MySQL的主键或者Redis incr实现自增。大规模系统可以使用twitter snowflake或者美团的leaf。但是订单号又不能单纯递增,否则竞对很容易根据订单号看出GMV。最好在订单号融入业务规则。
  3. 对于创建订单的页面,如果黑产不调用订单号的服务,而是随意填写一些订单号,假如小部分订单号正好符合业务规则,那么就有可能造成后续这部分订单无法正常生成,解决方法是在Redis中缓存一下生成的订单号,收到客户端请求的时候,先查询一下订单是否在缓存中。
  4. 如果无法控制前端在下单前调用生成订单号服务,理论上重试导致的重复请求之间的时间间隔都比较大,那么可以考虑“1秒内每个用户只允许最多下1单”来判重。