订单系统设计

此文主要参考乐视订单系统设计,每秒处理10万高并发订单的乐视集团支付系统架构分享 ,写此文时,北京的乐视大楼已经挂到中介了。

对于电商来说,订单业务为第一核心业务。除了订单业务外,关键业务还有支付、商品、库存、用户等相关业务。订单业务关键的是后端数据库的设计,下面分几个问题分析下。

分库分表

分表可以纵向分表,与HBase的设计的理念有些类似,业务上的假设,很多的操作不是需要所有的列,而只是需要部分列。订单可能有的字段有时间、商品信息、价格信息、状态、物流、售后处理信息等,其中有些字段后面经常使用,而且会同时经常使用,可以把这中列单独放到一张表,其他如售后、物流的也可单独放在一张表,多张表使用主键id关联起来。

唯品会的一片相关文章中介绍了对订单库的垂直分表。参考:唯品会订单分库分表的实践总结以及关键步骤

从本质上看,分库与分表(横向分),区别不大,都是逻辑空间上的划分。分库,逻辑上来说,库与库之间,是没有任何关系的,独立性比较好。而对于一个库中的表进行横向切分,一是可以减少单表的大小,从而可以减少索引的树的大小。二是,在一个库中,一个单独的表与其他表物理存储上,一般会分离,例如,可以给不同的表配置不同的磁盘,这样,在并发写入的时候IO的冲突就少了,数据库这东西,最耗时的就是IO了。

分表后的大小,引用唯品会订单分库分表的实践总结以及关键步骤中的一句话:

订单分表是首先考虑的,分表的目标是保证每个数据表的数量保持在1000~5000万左右,在这个量级下,数据表的大小与性能是最理想的。

乐视的扩容是2的整数倍方式。分库分表实现方式如下:

伸缩性

如果分表分库了,那么就存在路由问题,当然也存在一致性hash的问题了,这里可以参考Redis的集群的方式,采用一种预分片技术,比如,目前的业务使用8台机器就能解决2到3年内的增长问题,那么可以采用分成32个片的方式,每个机器上先部署上4个片,等业务增长了,可以上16台机器,修改下路由扩容工作就能完成了。

这里如果业务增长飞快,到了32个机器都解决不了的时候,就需要重新对数据进行hash了,这里可以参考java中的HashMap的方式,这也是实例(片)的数目为什么一直是2的次幂个,Redis中,实例个数也是2的次幂个,在扩容的时候,最好是直接扩大一倍的实例数目(不一定是物理主机数目),这样数据进行rehash的时候有一半的数据不用动,而只迁移一半的数据就可以了。

扩容问题,一般也会按照时间进行分库,例如5年前的订单,用户很少查看了,对于配置的主从配置对于读的性能要求会低些,例如一主二从的备份就可以了。

乐视的路由规则(8个库,每个库10个表)如下:

根据uid(user id)计算数据库编号:

数据库编号 = (uid / 10) % 8 + 1

根据uid计算表编号:

表编号 = uid % 10

还有个问题,订单号里面有分库分表的信息,如果分库信息是1到8,那如果扩容怎么办?修改以前的订单记录可不是什么好的方式。解决方式如下,假设以后最大的容量可以使用主库(64)个,有点像Redis中的预分片技术,先估计出最大的可能容量。然后订单中的路由信息计算是按照64个库计算,订单号中也存储的是这个值。那么就有个问题了,实际的库是8个,按照64计算,可能计算出的库的编号是63,存储在哪个库呢?解决方式也很简单,在取一次余么,就是7了。

这里,与Java中的HashMap一样,大小都是2的次幂大小,原因也差不多。在扩容的时候,只对以前的部分数据迁移即可。例如从8个库扩容到16个库,只需要对以前每个库中的一半的数据进行迁移即可。

唯品会使用的方式就是预分片技术,分好后,一个主机上跑多个库。

每组服务器容纳4个库,如果将来单服务器达到性能、容量等瓶颈,可以直接把数据库水平扩展为2倍服务器集群,还可以继续扩展为4倍服务器集群。水平扩展可以支撑公司在未来3~5年的快速订单增长。

高可用

如果不考虑跨数据中心,那么简单的方式就是主从,主写从读,可以有多个从数据库。一般而言,像分布式系统中,都有自己的选举机制,master挂掉后,立刻从slave中选举出来一个当作主数据库,但是,Mysql这方面支持的并不是很好,乐视的解决方式如下,从库读取使用负载均衡软件,主库的写只有一份,从用keepalive的方式进行故障转移。当然,Mysql也是不支持的,还是需要写点脚本实现的。这种方式,相对来说比较简单,定位问题相对容易,而且可以针对主库和从库做定制性的硬件。给主库点可靠的,磁盘也可以是固态的。如果是按照一般分布式的套路,主库挂掉后,是从从库选举出一个当作主节点。

订单号设计

订单号设计需要考虑的点自己想到的如下:

先参考下Mongodb的生成方式,共12个字节,从前到后是时间戳4字节+机器的hash地址3字节+PID 2字节+计数器3字节,满足单调,纯数字,能多进程生成,速度应该也是比较快的。

第三点,为什么订单号里面要包括用户ID的信息呢?主要考虑分库的时候,用户和此用户对应的订单,最好在一个库中,这样进行操作的时候速度会快,如果要在一个库中,由于用户与订单是一对多的关系,那分库的时候,应该是根据用户的id(也可以hash后)进行取余运算,那如何保证此用户生成的订单,分库路由也定位到用户的库中呢?最简单的就是直接把用户的id加在订单号的前面,存储的时候,从订单id中拿出用户的id,重复进行下计算路由的操作,就可以了么。但是这样,订单号的位数一定很长。在进一步,订单id中需要的只是用户的id的分库路由信息,那么可以直接把用户id的分库路由信息保存下来,例如有8个库,一个用户根据id计算出的路由规则是在第7个库,那么可以把这个用户生成的订单号的第一位直接写成7,这样存储的时候就会路由到对应用户所在的库中了。

乐视的订单ID规则:分库分表信息,时间戳,机器号,自增序列。与Mongodb的相比,没有PID,这就会有锁竞争了,但是对于一个关系型的数据库,即使有锁竞争速度也是够用的了。比如乐视的设计是峰值每秒10w的订单量,订单处理的机器假设也是有10个,那么每秒1w的id生成,对于单机,并不是很高的要求,对于这种交易型的需要落地的业务,数据库才是瓶颈,而不是这订单号生成的速度。

唯品会中,直接加了个中间表,用户id与订单id的缓存表,为了提高速度放到了缓存,但这数据量其实蛮大的。

根据用户编号进行哈希分库分表,可以满足创建订单和通过用户编号维度进行查询操作的需求,但是根据统计,按订单号进行查询的占比达到80%以上,所以需要解决通过订单号进行订单的CURD等操作,所以需要建立订单号索引表。

订单号索引表是用于用户编号与订单号的对应关系表,根据订单号进行哈希取模,放到分库里面。根据订单号进行查询时,先查出订单号对应的用户编号,再根据用户编号取模查询去对应的库查询订单数据。

订单号与用户编号的关系在创建订单后是不会更改的,为了进一步提高性能,引入缓存,把订单号与用户编号的关系存放到缓存里面,减少查表操作,提升性能,索引不命中时再去查表,并把查询结果更新到缓存中。

数据分级

直接引用乐视相关的原文中的一段话:

第1级:订单数据和支付流水数据;这两块数据对实时性和精确性要求很高,所以不添加任何缓存,读写操作将直接操作数据库。
第2级:用户相关数据;这些数据和用户相关,具有读多写少的特征,所以我们使用redis进行缓存。
第3级:支付配置信息;这些数据和用户无关,具有数据量小,频繁读,几乎不修改的特征,所以我们使用本地内存进行缓存。

上面中,第一级的数据,不添加任何缓存指的是没有在额外应用层缓存,这些数据是不断变化的,而且读少,所以缓存意义不大。

参考

每秒处理10万高并发订单的乐视集团支付系统架构分享
解析各大电子商务网站订单号的生成方式 唯品会订单分库分表的实践总结以及关键步骤

Table of Contents