⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 潘志的研发笔记 「志哥聊技术」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

01、背景介绍

在实际的软件系统开发过程中,由于业务的需要,我们经常需要生成业务单号,例如订单编号、入库单号、投诉服务单号等等,针对这个问题也做了一些研究,有一些收获想分享给大家!

本文主要以讨论电商的订单编号规则为案例,其他类型的服务编号设计思路其实也是相似的。

不废话,直接干货!

订单命名的几种规则总结:

  • 不重复:这点我相信大家都懂,必须全局唯一
  • 安全性:订单号需要做到不容易被人为的猜测或者推测出来,例如订单号就是流水号的话,那么别人就很容易从订单号推测出公司的整体运营情况。
  • 禁用随机码:很多人分析生成订单号的时候,第一个念头肯定是不重复唯一性,那么第二个念头可能就是安全性,想要同时满足前两者,很容易想到使用随机码,随机码从一定程度来说,更安全、不重复性更高,但是可读性差,有概率会发生重复。
  • 防止并发:针对系统的并发业务场景(如秒杀),需要做到并发场景下,订单编号生成快速、不重复等要求
  • 控制位数:订单号的位数尽量在 10 位 ~ 18 位之间。太短的情况下,如果交易量过大,很难做到防止重复,太长可读性差、意义也不大。

02、方案实践

上面提到了订单编号生成的规则,那要实现这样的规则,该如何实现会比较好呢?

下面总结几种常见的处理方式,我们一一分析!

2.1、方案一:UUID

UUID 是Universally Unique Indentifier的缩写,翻译为通用唯一识别码,顾名思义 UUID 是一个用于记录唯一标识一条的数据,其按照开放软件基金会(OSF)指定的标准进行计算,用到了以太网卡地址(MAC)、纳秒级时间、芯片 ID 码和许多可能的数字。

总的来说,UUID 码由以下三部分组成:

  • 当前日期和时间
  • 时钟序列
  • 全局唯一的 IEEE 机器识别码(如果有网卡从网卡获得,没有网卡则通过其他方式获得)

UUID 的标准形式包含 32 个 16 进制数字,以连字号分为五段,示例:00000191-adc6-4314-8799-5c3d737aa7de。

以java为例,通过以下方式即可生成:

String uuid = UUID.randomUUID().toString();

这种方案,虽然实现简单、方便;但是数据库查询效率非常差,而且内容长,在实际的项目场景开发中,一般用于于记录用户的手机设备ID等硬件信息!

因此不推荐采用 uuid 来生成订单编号!

2.2、方案二:数据库自增

所谓数据库自增,意思是在数据库中给某个列设置为自增列,并且给该列设置一个初始值,代码层面无需任何特殊处理,以 Mysql 的用户表 ID 列为例,可以通过如下方式在创建表的时候生产。

CREATE TABLE `tb_user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

这种通过数据库自增方式实现唯一值,在单体服务下是没有问题,但是在大流量分布式服务环境下,并发性能很低。

以后数量大的时候,需要对 mysql 进行分库分表,此时订单号会重复,因此不推荐采用!

2.3、方案三:雪花算法

Snowflake(中文简称:雪花算法) 是 Twitter 内部的一个 ID 生算法,可以通过一些简单的规则保证在大规模分布式情况下生成唯一的 ID 号码。其内部结构如下:

可以很清晰的看出,Snowflake 由 4个部分组成:

  • 第一部分:bit 值,为未使用的符号位
  • 第二部分:由 41 位的时间戳(毫秒)构成,它的取值是当前时间相对于某一时间的偏移
  • 第三部分:表示工作机器 id,由服务节点 id 和数据中心 id 组合而成
  • 第四部分:表示每个工作机器每毫秒生成的序列号 ID,同一毫秒内最多可生成生产 4095 个 ID。

由于在 Java 中 64bit 的整数是 long 类型,因此在 Java 中 SnowFlake 算法生成的 id 就是 long 来存储的。

SnowFlake 算法可以保证:

  • 1.所有生成的 id 按时间趋势递增
  • 2.整个分布式系统内不会产生重复id(因为有服务节点 id 和数据中心 id 来做区分)

需要注意的是:

  • 在分布式环境中,5 个 bit 位的 datacenter 和 worker 表示最多能部署 31 个数据中心,每个数据中心最多可部署 31 台节点。
  • 41 位的二进制长度最多能表示2^41 -1毫秒即 69 年,所以雪花算法最多能正常使用 69 年,为了能最大限度的使用该算法,在使用的时候,应该为其指定一个开始时间,不然会发生重复!

在高并发的环境下,Snowflake 算法可以生成全局唯一的订单编号,但是他的长度达到21位,因此不推荐采用,但是可以用它来生成主键 ID,是完全没有问题的!

2.4、方案四:分布式组件

要想在分布式环境下生成一个唯一的订单编号,我们可以通过分布式组件的方式,来帮忙我们生成全局唯一的订单号,例如我们可以采用 redis 分布式缓存组件中的incr命令,来帮我们生成一个全局自增长的序列号!

实现逻辑如下:

// 基于某个key实现自增长
String res = jedis.get(key);
if (StringUtils.isBlank(res)) {
// 设置初始值,INIT_ID 是初始值
jedisClient.set(key, INIT_ID);
// 设置过期时间,seconds 是多少秒过期
jedisClient.expire(key, seconds);
}
//存在就生成+1的订单号
long orderId = jedis.incr(key);

这种方式生成的自增长序列号,非常的快,可以很好的满足大流量环境下的编号要求唯一的特性!

剩下的主要工作就是我们如何去设计一个订单号规则!

在设计规则之前,我们先来看看互联网几个大厂的订单号格式。

京东商城订单号格式:157444499

苏宁易购订单号格式:2000839647

凡客诚品订单号格式:213052230059

银泰网订单号格式:10030522161715

小米订单号格式:1111218032345170

我们先来分析一下凡客诚品和银泰网的订单号生成规则。

凡客诚品和银泰网订单号都含有 0522,这是因为这 2 张订单都是2013年5月22号下的订单。

基本猜测一下,凡客的订单规则是:业务编码+年的后2位+月+日+订单数;泰网的订单号规则:年的第三位数+业务编码+年的后1位+月+日+订单数;而京东商城和苏宁易购的订单号看不出规则。

最后我们来分析一下小米订单号1111218032345170,可以将其分解成四个部分1——111218—03234—5170。

  • 第一部分,1 表示购买,2 表示退货。
  • 第二部分,表示 2011 年 12 月 18 日下的单,前面两位省掉了。
  • 第三部分,时间戳对应00:53:54,换算成秒是03234秒。
  • 最后一部分,表示在同一秒内下的第 5170 单,也就是说,小米认为,在一秒内不会超过一万个订单。

总结起来,小米的订单规则是:业务编码+年的后 2 位+月+日+秒+订单数,固定长度为16,这种订单号规则可以保证 100 年不会重复!

同样的,借鉴小米的订单号规则,我们也可以生成同样的订单号,实现过程如下:

//获取当前时间
Date currentTime = new Date();
//格式化当前时间为【年的后2位+月+日】
String originDateStr = new SimpleDateFormat("yyMMdd").format(currentTime );
//计算当前时间走过的秒
Date startTime = new SimpleDateFormat("yyyyMMdd").parse(new SimpleDateFormat("yyyyMMdd").format(originDate));
long differSecond = (currentTime.getTime() - startTime.getTime()) / 1000;
//获取【年的后2位+月+日+秒】,秒的长度不足补充0
String yyMMddSecond = originDateStr + StringUtils.leftPad(String.valueOf(differSecond), 5, '0');

//获取【业务编码】 + 【年的后2位+月+日+秒】,作为自增key;
String prefixOrder = sourceType + "" + yyMMddSecond;
//通过key,采用redis自增函数,实现单秒自增;不同的key,从0开始自增,同时设置60秒过期
Long incrId = redisUtils.saveINCR(prefixComplaint, 60);
//生成订单编号
String orderNo = prefixOrder + StringUtils.leftPad(String.valueOf(incrId), 4, '0');

此订单编号可以保证大流量环境下全局唯一、生成速度非常的快、支持高并发环境,同时还支持按时间排序!

03、总结

通过上面的示例演示,我们可用做一个详细的总结!

综上所述,在大流量的环境下,我们可以通过 redis 的incr函数实现序列号自增的特性,同时搭配订单的设计规则,从而保证高并发的环境下,订单唯一性!

文章目录
  1. 1. 01、背景介绍
  2. 2. 02、方案实践
    1. 2.1. 2.1、方案一:UUID
    2. 2.2. 2.2、方案二:数据库自增
    3. 2.3. 2.3、方案三:雪花算法
    4. 2.4. 2.4、方案四:分布式组件
  3. 3. 03、总结