一、前言
在我们日常的开发中经常会遇到需要生成业务订单号的情况,例如订单编号、入库单号、投诉建议单号等。
订单命名的几种规则总结:
- 不重复:这点我相信大家都懂,必须全局唯一
- 安全性:订单号需要做到不容易被人为的猜测或者推测出来,例如订单号就是流水号的话,那么别人就很容易从订单号推测出公司的整体运营情况。
- 禁用随机码:很多人分析生成订单号的时候,第一个念头肯定是不重复唯一性,那么第二个念头可能就是安全性,想要同时满足前两者,很容易想到使用随机码,随机码从一定程度来说,更安全、不重复性更高,但是可读性差,有概率会发生重复。
- 防止并发:针对系统的并发业务场景(如秒杀),需要做到并发场景下,订单编号生成快速、不重复等要求
- 控制位数:订单号的位数尽量在 10 位 ~ 18 位之间。太短的情况下,如果交易量过大,很难做到防止重复,太长可读性差、意义也不大。
二、方案研究
1.UUID
UUID 是Universally Unique Indentifier
的缩写,翻译为通用唯一识别码,顾名思义 UUID 是一个用于记录唯一标识一条的数据,其按照开放软件基金会(OSF)指定的标准进行计算,用到了以太网卡地址(MAC)、纳秒级时间、芯片 ID 码和许多可能的数字。
总的来说,UUID 码由以下三部分组成:
- 当前日期和时间
- 时钟序列
- 全局唯一的 IEEE 机器识别码(如果有网卡从网卡获得,没有网卡则通过其他方式获得)
UUID 的标准形式包含 32 个 16 进制数字,以连字号分为五段,示例:00000191-adc6-4314-8799-5c3d737aa7de
。
示例代码如下:
package com.example.springbootdemo.test;
import java.util.UUID;
public class UUIDTest {
public static void main(String[] args) {
UUID uuid = UUID.randomUUID();
System.out.println(uuid);
}
}
运行程序:
这种方案,虽然实现简单、方便;但是数据库查询效率非常差,而且内容长,在实际的项目场景开发中,一般用于于记录用户的手机设备ID等硬件信息!
2.数据库自增
所谓数据库自增,意思是在数据库中给某个列设置为自增列,并且给该列设置一个初始值,代码层面无需任何特殊处理,以 Mysql 的用户表 ID 列为例,可以通过如下方式在创建表的时候生产。
create table t_user(
id bigint not null auto_increment,
name varchar(100) not null,
age int not null,
primary key(id))
这种通过数据库自增方式实现唯一值,在单体服务下是没有问题,但是在大流量分布式服务环境下,并发性能很低。
以后数量大的时候,需要对 mysql 进行分库分表,此时订单号会重复,因此不推荐采用!
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,是完全没有问题的!
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);
这种方式生成的自增长序列号,非常的快,可以很好的满足大流量环境下的编号要求唯一的特性!
剩下的主要工作就是我们如何去设计一个订单号规则!
示例代码如下:
//获取当前时间
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');
此订单编号可以保证大流量环境下全局唯一、生成速度非常的快、支持高并发环境,同时还支持按时间排序!
三、总结
通过上面的示例演示,我们可用做一个详细的总结!