面试官再问你怎么修改订单,就把这篇甩给他

0 前言

全是干货的技术殿堂

文章收录在我的 GitHub 仓库,欢迎Star/fork:

Java-Interview-Tutorial

https://github.com/Wasabi1234/Java-Interview-Tutorial

从下单开始、支付、发货,收货,每一个环节,都少不了更新订单,每一次更新又需要同时更新好几张表。
这些操作可能被随机分布到很多台服务器上执行,服务器有可能故障,网络有可能出问题。

那么如何才能保证订单服务的数据一致性呢?

  • 正确使用数据库的事务
    eg.创建订单时,要同时往订单表和订单商品表中插入数据,那这些插入数据的INSERT必须在一个数据库事务中执行,数据库的事务可以确保:执行这些INSERT语句,共赴生死!

但还有很多难以发现的坑存在

1 基本功能和数据表

任何系统的订单服务都是独一无二的,基于不同业务,有很多个人限定.不过核心都大同小异,让我们研究其共同点.

基本功能

  • 创建订单
  • 随着购物流程更新订单状态
  • 查询订单,包括用订单数据生成各种报表

数据表

  • 订单主表:也叫订单表,保存订单的基本信息
    订单主表和后面的几个子表都是一对多,关联的外键就是订单主表的主键,也就是订单号
  • 订单商品表:保存订单中的商品信息
  • 订单支付表:保存订单的支付和退款信息
  • 订单优惠表:保存订单使用的所有优惠信息。

2 如何避免重复下单?

用户在浏览器页面上点击“提交订单”按钮的时候,浏览器就会给订单系统发一个创建订单的请求,订单系统的后端服务,在收到请求之后,往数据库的订单表插入一条订单数据,创建订单成功.

假如用户点击“创建订单”的按钮时手抖了,点了两下,结果是什么?创建了两条一模一样的订单.这可咋办呢?

有人说,前端页面上应该防止用户重复提交表单.没啥毛病,但是,网络错误会导致重传,很多RPC框架、网关都会有自动重试机制,所以对于订单服务来说,重复请求这个事儿,你是没办法完全避免的.

所以问题的本质其实是如何保证订单服务的幂等性.
简单来说就是一个幂等的方法,使用同样的参数,对它进行调用多次和调用一次,对系统产生的影响是一样的.
所以只有幂等的方法才能做到防重.
一个幂等的创建订单请求,不管发送多少次,结果都是数据库只有一条新创建的订单记录。

2.1 怎么判断请求是否重复

插入订单数据前,先查一下订单表里面有没有重复的订单?
这可不太行,因为你很难用SQL的条件来定义“重复的订单”
订单用户一样、商品一样、价格一样,就是重复订单?万一这搞笑用户就是连续下了俩一模一样订单?

2.2 最佳实践

在往数据库插入一条记录时,一般不提供主键,而由数据库在插入时自动生成一个主键。这样重复的请求就会导致插入重复数据。

表的主键自带唯一约束,如果我们在一条INSERT语句中提供了主键,并且这个主键的值在表中已经存在,那这条INSERT会执行失败.
因此可以利用数据库的这种“主键唯一约束”特性,在插入数据的时候带上主键,以此实现创建订单接口的幂等性.

给订单服务添加一个“订单号生成”的接口,无参,返回值就是一个全局唯一的订单号。在用户进入创建订单的页面时,前端页面先调用这个生成订单号接口得到一个订单号,在用户提交订单的时候,在创建订单的请求中带着这个订单号。

这个订单号也就是订单表的主键,如此这些重复请求中带的都是同一个订单号。订单服务在订单表中插入数据的时候,执行的这些重复INSERT语句中的主键,也都是同一个订单号。数据库的唯一约束就可以保证,只有一次INSERT语句是执行成功的

  • 幂等创建订单的时序图

如果因为重复订单导致插入订单表失败,订单服务不要把这个错误返回给前端页面.
否则,就可能出现用户点击创建订单按钮后,页面提示创建订单失败,而实际上订单却创建成功了.
正确的做法是,遇到这种情况,订单服务直接返回订单创建成功即可.

3 攻克ABA

3.1 什么是 ABA?

比如订单支付后,卖家要发货,发货完成后要填个快递单号。假设说,卖家填了个666,刚填完,发现填错了,赶紧再修改成888。对订单服务来说,这就是2个更新订单的请求。系统异常时666请求到了,单号更成666,接着888请求到了,单号又更新成888,但是666更新成功的响应丢了,调用方没收到成功响应,自动重试,再次发起666请求,单号又被更新成666了,这数据显然就错了.

  • 时序图

3.2 解决方案

通用的解决方案

订单主表增加一列version。每次查询订单的时候,版本号要随着订单数据返回给页面。
页面在更新数据的请求中,把这个版本号作为更新请求的参数,带回给订单更新接口。

订单服务在更新数据的时候,需要比较订单的版本号是否和消息中的一致:

  • 不一致 拒绝更新数据
  • 一致 还需要再更新数据的同时,把版本号+1。“比较版本号、更新数据和版本号+1”,这个过程必须在同一个事务里面执行。
UPDATE orders set tracking_number = 666, version = version + 1
WHERE version = 8;

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

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

有了这个版本号,前文的ABA即有两个 case

  • 把运单号更新为666的操作成功了,更新为888的请求带着旧版本号,那就会更新失败,页面提示用户更新888失败
  • 第二种情况,666更新成功后,888带着新的版本号,888更新成功。这时候即使重试的666请求再来,因为它和上一条666请求带着相同的版本号,上一条请求更新成功后,这个版本号已经变了,所以重试请求的更新必然失败

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

  • 下图展示case1

4 总结

  • 对于创建订单服务来说,可以通过预先生成订单号,然后利用数据库中订单号的唯一约束这个特性,避免重复写入订单,实现创建订单服务的幂等性
  • 对于更新订单服务,可以通过一个版本号机制,每次更新数据前校验版本号,更新数据同时自增版本号,这样的方式,来解决ABA问题,确保更新订单服务的幂等性。

两种幂等的实现方法,就可以保证,无论请求是不是重复,订单表中的数据都是正确的。

实现订单幂等的方法,你完全可以套用在其他需要实现幂等的服务中,只需要这个服务操作的数据保存在数据库中,并且有一张带有主键的数据表即可

参考

  • 后端存储实战
发布了387 篇原创文章 · 获赞 598 · 访问量 35万+

猜你喜欢

转载自blog.csdn.net/qq_33589510/article/details/104954137