电商系统架构设计系列(三):关于「订单系统」有哪些问题是要特别考虑的?

news/2024/11/23 1:39:11/

订单系统是整个电商系统中最重要的一个子系统,订单数据也就是电商企业最重要的数据资产。

上篇文章中,我给你留了一个思考题:当系统在创建和更新订单时,如何保证数据准确无误呢?

今天这篇文章,主要聊一下,在设计和实现一个订单系统的存储过程中,有哪些问题是要特别考虑的。

引言

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

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

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

  • 首先,你的代码必须是正确没 Bug 的,如果说是因为代码 Bug 导致的数据错误,那谁也救不了你。
  • 然后,你要会正确地使用数据库的事务。比如,你在创建订单的时候,同时要在订单表和订单商品表中插入数据,那这些插入数据的 INSERT 必须在一个数据库事务中执行,数据库的事务可以确保:执行这些 INSERT 语句,要么一起都成功,要么一起都失败。

我相信这些“基本操作”对于你来说,应该不是问题。

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

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

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

我们先一起简单梳理一下一个订单系统必备的功能,它包含但远远不限于:

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

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

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

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

绝大部分订单系统它的核心功能和数据结构都是这样的。

如何避免重复下单?

接下来我们来看一个场景:

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

假如说,用户点击“创建订单”的按钮时手一抖,点了两下,浏览器发了两个 HTTP 请求,结果是什么?创建了两条一模一样的订单。这样肯定不行,需要做防重。

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

解决办法是,让你的订单服务具备幂等性。

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

这里面有一个不太好解决的问题:

对于订单服务来说,它怎么知道发过来的创建订单请求是不是重复请求呢?

在插入订单数据之前,先查询一下订单表里面有没有重复的订单,行不行?不太行,因为你很难用 SQL 的条件来定义“重复的订单”,订单用户一样、商品一样、价格一样,就认为是重复订单么?不一定,万一用户就是连续下了两个一模一样的订单呢?所以这个方法说起来容易,实际上很难实现。

很多电商解决这个问题的思路是这样的。在数据库的最佳实践中有一条就是,数据库的每个表都要有主键,绝大部分数据表都遵循这个最佳实践。一般来说,我们在往数据库插入一条记录的时候,都不提供主键,由数据库在插入的同时自动生成一个主键。这样重复的请求就会导致插入重复数据。

我们知道,表的主键自带唯一约束,如果我们在一条 INSERT 语句中提供了主键,并且这个主键的值在表中已经存在,那这条 INSERT 会执行失败,数据也不会被写入表中。我们可以利用数据库的这种“主键唯一约束”特性,在插入数据的时候带上主键,来解决创建订单服务的幂等性问题

具体的做法是这样的,我们给订单系统增加一个“生成订单号”的服务,这个服务没有参数,返回值就是一个新的、全局唯一的订单号。在用户进入创建订单的页面时,前端页面先调用这个生成订单号服务得到一个订单号,在用户提交订单的时候,在创建订单的请求中带着这个订单号

具体实现方案,可参考:如果单纯是生成GUID(全局唯一ID),比如小规模系统完全可以用MySQL的Sequence或者Redis来生成,大规模系统可以采用类似雪花算法之类的方式分布式生成GUID。可参考美团开源的分布式ID生成服务

这个订单号也是我们订单表的主键,这样,无论是用户手抖,还是各种情况导致的重试,这些重复请求中带的都是同一个订单号。订单服务在订单表中插入数据的时候,执行的这些重复 INSERT 语句中的主键,也都是同一个订单号。数据库的唯一约束就可以保证,只有一次 INSERT 语句是执行成功的,这样就实现了创建订单服务幂等性。

时序图可供参考,方便你进一步理解:

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

正确的做法是,遇到这种情况,订单服务直接返回订单创建成功就可以了。

如何解决 ABA 问题?

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

这些更新订单服务,比如说支付、发货等等这些步骤中的更新订单操作,最终落到订单库上,都是对订单主表的 UPDATE 操作。数据库的更新操作,本身就具备天然的幂等性,比如说,你把订单状态,从未支付更新成已支付,执行一次和重复执行多次,订单状态都是已支付,不用我们做任何额外的逻辑,这就是天然幂等。

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

还真有,在并发环境下,你需要注意 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 id = 主键id and version = 8;

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

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

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

可能出现两种情况:

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

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

总结 

以上的内容,主要就是讨论了实现订单操作的幂等的方法。

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

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

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

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

感谢阅读,如果你觉得这篇文章对你有一些启发,也欢迎把它分享给你的朋友。

思考题

流量大、数据多的商品详情页系统该如何设计?

期待、欢迎你留言或在线联系,与我一起讨论交流,“一起学习,一起成长”。

上一篇文章

电商系统架构设计系列(二):电商系统的技术选型要怎么做才更有效呢?


推荐阅读

  • 架构师:不想当架构师的程序员不是好程序员
  • 架构师技能修炼图
  • 技术破局,业绩狂飙十倍:亿级电商平台重构大揭秘
  • 当我们聊高并发时,到底是在聊什么?如何真正地掌握高并发设计能力?
  • 【总结】我的十二个架构设计原则
  • 微服务架构实战 - 我的经验分享总结2019(系统架构师)架构演进过程-从信息流架构到电商中台架构​​​​​​

系列分享

  • 高可用高并发实战专栏
  • DevOps实战专栏
  • SpringBoot系列专栏
  • 微服务架构实战
  • 架构思维成长系列

------------------------------------------------------

------------------------------------------------------

我的CSDN主页

关于我(个人域名,更多我的信息)

我的开源项目集Github

期望和大家 一起学习,一起成长,共勉,O(∩_∩)O谢谢

如果你有任何建议,或想学习的知识,可与我一起讨论交流

欢迎交流问题,可加个人QQ 469580884,

或者,加我的群号 751925591,一起探讨交流问题

不讲虚的,只做实干家

Talk is cheap,show me the code


http://www.ppmy.cn/news/389736.html

相关文章

减少Next-Lock的锁的

在RR(Read-Repeat)级别的并发控制中,Next-Lock是用于检查事务是否能够继续执行的锁。减少Next-Lock的锁的方法可以通过以下几种方式来实现: 调整事务的隔离级别:Next-Lock的主要目的是确保事务的隔离性,因此…

【C/C++】引用()的概念和用法

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c系列专栏&#xff1a;C/C零基础到精通 &#x1f525; 给大…

安装声卡驱动报错,代码:0xe0000246

安装声卡驱动报错&#xff0c;代码&#xff1a;0xe0000246 打开注册表&#xff0c;找到&#xff1a;HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\DeviceInstall\Parameters 将&#xff1a;DeviceInstallDisabled 的值改为 0 重新安装驱动即可 参考&#xff1a;0x…

32908字长文理解Large CV Model:Segment Anything

作者&#xff1a;猛码Memmat 目录 Abstract1. IntroductionTaskModelData engineDatasetResponsible AIExperimentsRelease 2. Segment Anything TaskTaskPre-trainingZero-shot transferRelated tasksDiscussion 3. Segment Anything ModelImage encoderPrompt encoderMask de…

Win11声卡驱动如何更新?Win11声卡驱动更新方法

Win11声卡驱动怎么更新&#xff1f;声卡对电脑很重要&#xff0c;如果声卡驱动出现了问题&#xff0c;我们可以对声卡驱动进行更新&#xff0c;那么应该如何操作呢&#xff1f;今天我就为大家带来Win11声卡驱动更新方法&#xff0c;有需要的朋友们快来看看是如何操作的吧。 方法…

安卓声卡驱动:4.codec驱动

一 codec驱动简介 硬件上的Audio codec是一种能够对数字数据流进行编码或解码的设备。 codec的功能非常多&#xff0c;常见的有 数模转换&#xff0c;解码时把数字信息解码成原本的模拟信号&#xff0c;编码时把模拟信号转为数字信号音频处理&#xff0c;EQ&#xff0c;混音&…

usb声卡驱动(一):USB描述符

usb声卡驱动&#xff08;一&#xff09; 前面看了内核的启动&#xff0c;接下来就是驱动的学习。 正好手边有一个USB声卡&#xff0c;就准备以此为基础&#xff0c;进行usb声卡驱动的学习。 因此&#xff0c;在学些usb声卡之前&#xff0c;先看看usb驱动。然后再是alsa驱动&…

Java实训第八天——2023.6.14

文章目录 一、vue的环境搭建&#xff1a;二、文本数据绑定三、属性数据绑定四、事件绑定五、案例1——全选/全不选六、案例2——切换图片主要内容&#xff1a; v-if 、v-show指令 七、表单数据绑定八、综合练习总结步骤&#xff1a; 一、vue的环境搭建&#xff1a; 官方文档&a…