订单系统 #
系统拆分 #
- 下单系统:用户选择商品进行下单操作。
- 订单系统:保存订单信息,提供订单查询功能。
- 履约系统:订单创建完成后的后续处理。
数据建模 #
表结构
- 订单主表:订单基本信息。
- 订单商品表:订单中的商品信息。商品信息需要关联完整的商品快照。
- 订单支付表:订单的支付和退款信息。
- 订单优惠表:订单使用的优惠信息。需要为每件商品单独保存使用到的优惠,否则用户支付后发起部分退款时按原价退款会造成资损。
字段设计
- 将订单号、卖家昵称、更新时间等需要被当做查询条件的字段抽取出独立字段存储,将其余数据结构当成json串存入一个大字段中。
- 将订单数据结构存储为hashcode字段,和其它系统进行增量数据同步时就只需对比hashcode即可。
订单号设计
- 淘宝订单编号(退款单编号):13位递增序号 + 买家编号最后6位 = 19位
- 支付宝流水号:yyyyMMdd + 12位支付宝序号 + 8位递增序号 = 28位
- 大众点评唯一ID:时间戳 + 用户ID后4位 + 随机数
- 异构分区键:订单号最后4位由买家ID后缀(2位)+卖家ID后缀组成(2位)
订单存储 #
分区方案
- 订单ID携带分区信息。买家库和商家库分别以买家ID和卖家ID为维度进行分区。
- 哈希分区,物理节点和逻辑节点分离。一开始就设计比实际服务器节点数量更多的分区,新增服务器节点时将部分分区移动到新节点上。
扩容步骤
- 数据库双写,查询走旧库;日终对账,补齐差异;
- 历史数据导入完毕并对账无误后,数据库依然双写,查询走新库;日终对账;
- 旧库不再同步写入,只有未完成的原订单需要终态时才异步写入;
订单查询 #
为保证读一致性,查询自己的商品走主库,浏览其它商品走从库。
下单流程 #
下单时需要确保下单时的价格为用户看到的价格,下单完成后需要清空购物车。
- 商家每次修改商品时,商品系统都需要生成商品快照,记录版本号。
- 用户下单时携带SKU_ID和版本号。订单服务检查请求中的商品版本号。
- 如果版本号不是最新的商品版本号,则提醒用户“商品信息已改变,请刷新页面重试”。
- 不使用版本号时直接判断商品信息最近30s内是否有改变,有改变则提示“请刷新页面重试”。
- 订单创建成功后,通过消息队列通知购物车系统清空购物车。
下单的幂等性保护 #
重新下单场景
- 用户以为没有操作成功再次单击按钮。
- 用户点击下单按钮后立即置灰,禁止再次点击。
- 黑客直接调用提单接口进行提交。
- 采用令牌机制。用户每次进入结算页,提单系统颁发一个令牌ID。当用户提交订单后,提单系统会检查令牌ID存在&令牌ID访问次数=1的话才会处理后续逻辑,否则直接返回。
- 网络超时导致的系统重试。
- 后端系统支持幂等性。
订单创建的幂等性
- 用户进入创建订单页面后,从服务端获取订单号,服务端将订单号保存到Redis中。
- 下单时发送订单号,服务端从Redis中删除订单号。
- 数据库用订单号作为唯一键进行兜底保护。
- 因重复订单导致的插入失败直接返回创建成功而不是创建失败。
订单更新的幂等性
ABA问题
对于同个资源的两个更新操作依次完成了更新操作,但第一个请求的响应在传输过程中丢了,导致调用方自动重试,结果最新的第二个请求的更新结果被重试的第一个请求覆盖了。
解决方法
- 增加一列 version。
- 每次查询订单的时候,version 需要随着订单数据返回给页面。
- 页面在更新数据的请求中,需要把这个version作为更新请求的参数,再带回给订单更新服务。
订单同步 #
下单时订单创建在买家库后异步同步到卖家库。
日千万订单系统 #
性能问题:单一下单服务时单库压力大。
解决方案:将服务拆分为接单服务、订单引擎、订单管道,对数据库进行分片。使用双写和数据补偿的方式处理缓存,使用缓存过期的方式控制数据量。将整个订单处理流程分解成一个一个的任务,逐个单独处理,来应对日千万级的订单处理压力。
具体步骤
- 用户在结算页面点击提交订单。
- 接单服务将订单插入订单表,并将首任务插入任务表,通知订单引擎。接单库采用多分区架构,每个分区一主多从,随机写入(扩展性强,新增数据库对写入逻辑是无感知的)。
- 订单引擎生成任务队列后,采用多线程调度任务,根据配置决定任务队列里的认为是串行还是并行执行,首任务完成后插入第二个任务。
- 订单引擎通过订单管道调用远程服务,订单管道通过线程池执行任务。
- 订单管道请求后将结果返回给任务引擎。
- 任务状态机定期检查任务状态,补偿重试失败任务。
- 接单服务在插入订单和任务后,调用订单中心。
- 订单中心将订单落库,插入缓存,在后续订单中心接收到台账的消息后,也会同时更新数据库和缓存,将订单状态更新为“订单完成”。
- 用户通过订单中心的缓存查询我的订单。