电商:创建和更新订单时,如何保证数据准确无误

订单系统是整个电商系统中最重要的一个子系统,订单数据也是电商企业最重要的数据资产。本节主要内容是在设计和实现一个订单系统的存储过程中有哪些问题是特别需要考虑的。

一个合格的订单系统,最基本的要求是什么?数据不能错

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

在这么复杂的情况下,保证订单数据一笔都不能错,是不是很难?实际上,只要掌握了方法,其实并不难。

  • 首先,代码必须是正确没有Bug的,如果是代码Bug而导致的数据错误,神仙难救
  • 然后,正确使用数据库的事务。比如,创建订单的时候,同时要在订单表和订单商品表中插入数据,那这些插入数据的INSERT必须在一个数据库事务中执行。

但是,还有一些情况下会引起数据错误,我们一起来看一下。不过在此之前,我们要明白,对于一个订单系统而言,它的核心功能和数据结构是怎样的。

因为,任何一个电商,它的订单系统的功能都是独一无二的,基于它的业务,有非常多的功能,并且都很复杂。我们在讨论订单系统的存储问题时,必须得化繁为简,只聚焦那些最核心的、共通的业务和功能上,并且以这个为基础来讨论存储技术问题。

订单系统的核心功能和数据

一个订单系统必备的功能,它包含但远远不限于:

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

为了支撑这些必备功能,在数据库中,我们至少需要有这样几张表:

  • 订单主表:也叫订单表,保存订单的基本信息。
  • 订单商品表:保存订单中的商品信息
  • 订单支付表:保存订单中的支付和退款信息
  • 订单优惠表:保存订单所使用的所有优惠信息

这几个表之间的关系是这样的:订单主表和后面几个子表都是一对多的关系,关联的外键就是订单主表的主键,也就是订单号

如何避免重复下单

场景:

  • 一个订单系统,提供创建订单的 HTTP 接口,用户在浏览器页面上点击“提交订单”按钮的时候,浏览器就会给订单系统发一个创建订单的请求,订单系统的后端服务,在收到请求之后,往数据库的订单表插入一条订单数据,创建订单成功。
  • 假如说,用户点击“创建订单”的按钮时手一抖,点了两下,浏览器发了两个 HTTP 请求,结果是什么?创建了两条一模一样的订单。这样肯定不行,需要做防重。

怎么办呢?能否在前端页面上防止用户重复提交表单

  • 可以是可以,但是网络错误会导致重传,很多RPC框架,网关都会有自动重试功能,所以对于订单服务来说,重复请求这个事情,是没有办法完全避免的。

解决方法:让订单服务具有幂等性。 什么是幂等呢?一个幂等操作的特点是,其任意多次执行所产生的影响均与一次执行的影响相同。也就是说,一个幂等的方法,使用同样的参数,它一次调度和多次调用,对系统产生产生的影响是一样的。所以,对于幂等的方法,不用担心重复执行会对系统造成任何改变。一个幂等的订单场景服务,无论创建订单的请求多少次,数据库中只会有一条新创建的订单记录

但是,这里有一个问题:对于订单服务来说,它怎么知道发过来的创建订单请求是不是重复请求呢?

  • 在插入订单数据之前,先查询一下订单表里面有没有重复的订单,行不行?不太行,因为你很难用 SQL 的条件来定义“重复的订单”,订单用户一样、商品一样、价格一样,就认为是重复订单么?不一定,万一用户就是连续下了两个一模一样的订单呢?所以这个方法说起来容易,实际上很难实现。
  • 解决思路:
    • 在数据库的最佳实践中有一条就是,数据库的每个表都要有主键,绝大部分数据表都遵循这个最佳实践。一般来说,我们在往数据库插入一条记录的时候,都不提供主键,由数据库在插入的同时自动生成一个主键。这样重复的请求就会导致插入重复数据
    • 我们知道,表的主键自带唯一约束,如果我们在一条INSERT语句中提供了主键,并且这个主键的值在表中已经存在,那么这条INSERT会执行失败,数据也不会被写入表中。我们可以利用数据库的这种“主键唯一约束”特性,在插入数据的时候带上主键,来解决创建订单服务的幂等性问题
  • 具体做法:
    • 我们给订单系统增加一个“生成订单号”的服务,这个服务没有参数,返回值就是一个新的,全局唯一的订单号。
    • 在用户进入创建订单的页面时,前端页面先调用这个订单号服务得到一个订单号,在用户提交订单的时候,在创建订单的请求中带着这个订单号
    • 这个订单号也是我们订单表的主键,这样,无论是用户手抖,还是各种情况导致的重试,这些重复请求中带的都是同一个订单号。订单服务在订单表中插入数据的时候,执行的这些重复INSERT语句中的主键,也都是同一个订单号。数据库的唯一约束就可以保证,只有一次INSERT语句是执行成功的,这样就实现了幂等性。

在这里插入图片描述

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

方法二

每次请求之前必须先生成一个唯一的请求id,服务端将该id暂时放入redis。客户端请求时必须携带上这个id,接口会首先到redis中查询,如何有的话就继续后续的处理逻辑,同时删除该id,灭有的话就退出,返回不能重复请求的错误到客户端。一句话总结:每次处理必须对应一个一次性的token。

生成订单号 服务的一般逻辑会是怎样的?思来想去,如果要想这个ID全局唯一,只能带上时间,可是如果带上时间,像那种,不小心点了两次按钮的情况,必然是两个不同的订单号;请问这个问题怎么解决?

  • 如果单纯是生成GUID(全局唯一ID)方法有很多,比如小规模系统完全可以用MySQL的Sequence或者Redis来生成。大规模系统中可以采用类似雪花算法之类的方法分布式生成GUID
  • 但是订单号这个东西又有点儿特殊要求,比如在订单号中最好包含一些品类、时间等信息,便于业务处理,再比如,订单号它不能是一个单纯自增的ID,否则别人很容易根据订单号计算出你大致的销量,所以订单号的生产算法在保证不重复的前提下,一般都会加入很多业务规则在里面,这个每家都不一样,算是商业秘密吧。

唯一的订单号是在进入“创建订单页面”时创建,用户如果对这个页面不小心同时开了多个tab页,每次打开一个tab页都会生成个新订单号,那就可以提交多个“一模一样”的订单了,而且用户本意应该是一个订单。

如果用户打开了多个tab页,只会产生多个订单号,这个时候还没有生成新订单呢。

只有他在每个tab也都点“下单”按钮,才会产生多个订单。这种情况不存在“误操作”的可能了。

如何解决ABA问题

同样,订单系统各种更新订单的服务一样也要具备幂等性。

这些更新订单服务,比如说支付、发货等这些步骤,最终落在订单库上,都是对订单主表的UPDATE操作。数据库的更新操作,天然具备幂等性。

那在实现这些更新订单服务时,还有什么问题需要特别注意的吗?有,在并发环境下,需要注意ABA问题

什么是ABA问题:

  • 比如说,订单支付之后,小二要发货,发货完成后要填个快递单号。假设说,小二填了一个单号666,刚填完,发现填错了,赶紧修改为888.对于订单来说,这就是2个订单更新的请求
  • 正常情况下,订单中的快递单号会先更新成666,再更新为888,这是没问题的。那不正常的情况呢?666请求到了,单号更新为666,然后888请求到了,单号有更新为888,但是666更新成功的响应丢了,调用方没收到成功响应,自动重试,再次发起666请求,单号又被更新为666了,这数据显然错了。这就是著名的ABA问题。

在这里插入图片描述
ABA问题怎么解决?一个通用的解决方法是:

  • 给你的订单主表增加一列,列名可以叫version,也就是“版本号”的意思。
  • 每次查询订单的时候,版本号需要随着订单数据返回给页面。
  • 页面在更新数据的请求中,需要把这个版本号作为更新请求的参数,再待会给订单更新服务。
  • 订单服务在更新数据的时候,需要比较订单当前数据的版本号,是否和消息中的版本号一致,如果不一致就拒绝更新数据。如果版本号一致,还需要再更新数据的同时,把版本号+1。“比较版本号、更新数据和版本号 +1”,这个过程必须在同一个事务里面执行。
UPDATE orders set tracking_number = 666, version = version + 1 WHERE version = 8;

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

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

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

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

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

在这里插入图片描述

小结

因为网络、服务器等等这些不确定的因素,重试请求是普遍存在并且不可避免的,具有幂等性的服务可以完美的克服重试导致的数据错误。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值