如何设计一个能扛住双11并发的订单号生成方案

说明:本文讲解的是如何设计订单号方案,而不是如何设计分布式ID。事实上订单号可以理解为一种特殊的分布式ID,它满足分布式ID所有特性。但是订单ID又有自己的一些特有的属性。如果你要了解一些分布式ID方案,可以参考笔者之前的几篇文章:《 leaf:美团开源的分布式ID生成系统剖析 》、《 UidGenerator:百度开源的分布式ID服务(解决了时钟回拨问题) 》。

要设计订单号首先需要订单号应该要具备的一些特性:

  1. 唯一性:这绝对是作为订单号最最最基本的特点;

  2. 高并发:并发能力越高越好;

  3. 趋势递增但是不能绝对递增:趋势递增会对现代数据库索引结构更友好,但是不要绝对递增是因为绝对递增的话,很容易暴露你系统每天产生的订单量;

  4. 利于以后的分库分表;

我的订单号方案

技术方案:
timestamp + 类用户ID + 随机数(可选)

实现代码:
Long userId = 235689102L;
String last6 = userId<1000000?String.format("%06d", userId):String.valueOf(userId%1000000);
String orderId = System.currentTimeMillis() + last6;

这种方案是否具备上面提到的几个特点呢,让我们一起看一看:

  1. 唯一性:这种方案的订单号只有在同一个用户在同一毫秒内下多个订单才会出现出现,很显然,对于正常的用户行为,是不可能出现重复的,所以满足唯一性。

  2. 高并发:这个设计方案完全不依赖任何第三方服务,只通过一定的规则就能生成。所以这种方案不但高并发,而且零消耗。

  3. 递增性:因为订单号的前一部分是时间戳,所以满足趋势递增。并且,也满足非绝对递增的特性。

  4. 分库分表:假设分库分表因子为订单号最后6位数,那么无论是根据订单ID查询,还是根据用户ID查询,都不会涉及跨库跨表,效率非常高。至于根据商户编号查询商户的订单号,可以参考笔者另一篇文章《 分库分表技术演进&最佳实践-修订篇 》,有提到解决方案。

通过上面的分析可知,这种方案还是很不错的!而且, 这种方案不需要依赖任何第三方服务,需要的硬件成本为零,老板最喜欢呀 !当然,如果担心用户ID信息通过订单号暴露,可以先通过某种函数例如hash(userId),然后再生成订单号即可。伪代码如下:

Long userId = 235689102L;
Long hashId = hash(userId);
String last6 = hashId<1000000?String.format("%06d", hashId):String.valueOf(hashId%1000000);
String orderId = System.currentTimeMillis() + last6;
System.out.println(orderId);
  • 为什么这种方案利于分库分表?

我们知道作为一个订单表,最核心的查询有3类: 根据订单号查询、根据用户ID查询、根据商户号查询 ,这三类查询占了订单表90%甚至更多的查询量。如果采用的是这里提到的时间戳+类用户ID的方案,那么根据订单号查询和根据用户ID查询都不涉及跨表,效率非常高。请看接下来的分析。

假设订单表按照订单号进行分表,并且分表算法为:hash(last6(orderId))%16,即根据订单号的最后6位数字hash后取模。这样的话,如下几个订单号就会分在同一个表假设t_order_5中,因为这几个订单号最后6位数字完全一样:

然后,这几个订单表都是用户ID为182141618生成的(我们假设hash(182141618)%1000000=141618)。那么用户ID为182141618所生成的订单也会都落在表t_order_5中,因为hash(182141618)%1000000=141618,也就是说,查询条件带有userId=182141618的查询目标表都是t_order_5。事实上,这种方案就是所谓的 因子分表法

接下来,我们可以看一下支付宝订单号又是如何设计的。

支付宝订单号方案

下面是3个真实的支付宝订单号(空格是为了更直观的看出支付宝订单号的特点):

分析这几个支付宝订单号,我们将其分解为3部分,从而得出支付宝的订单号的特点。你的支付宝订单号同样具备这样的特点:

时间戳 + 类用户ID + 递增的数值

通过对支付宝订单号的分解,我们很容易发现它的方案和前面提到的方案有异曲同工之妙: 都有时间戳部分和类用户ID部分 ,此乃英雄所见略同也,哈哈哈!