安全有序的互联网业务全局统一单号生成策略

常见的业务单号生成方式和优缺点

UUID

优点

代码简单、效率高、不重复、本地生成。

1
2
3
public static void main(String[] args) {
System.out.println(UUID.randomUUID());
}
1
78f653bf-5ef0-42bd-960b-58c227a1c468

缺点

1.UUID本身无序,对于像MySQL这种关系型数据库,使用B+树结构存储索引,如果使用UUID作为索引,UUID值的随机性会导致其在B+树结构中位置的不确定性,可能使索引频繁变动,影响了数据库执行性能。

2.UUID的生产算法可能会泄漏主机MAC地址,存在安全风险;

3.UUID长度太长不易存储;

依赖数据库生成ID

可以使用自增主键或者其他有序规则生成ID,可一次生成一个ID,也可以一次生成一组ID,用完再生成,降低数据库IO压力。

优点

有序、高效。

缺点

1.数据水平分表后,多表主键ID要维护全局唯一性,可维护性差;

2.依赖与数据库,数据库压力增大,数据库宕机整个服务不可用。

Twitter的雪花方案 Snowflake

用64位二级制数字,也就是Long型数字的长度表示唯一ID。

1位(符号,全部标记为1)+41位(表示毫秒时间,可包含69年)+10位(机器ID:数据中心ID+服务ID)+12位(每毫秒产生数字标识,可表示4096个ID)

每次把生成ID后的时间记录到缓存中,下次生成ID时要拿当前系统时间与上次生成ID的时间做比较,如果当前时间比上次记录的时间还要早,那么就可能是发生了时钟回拨,需要报警,并与原子时钟校准时间。PS:国际标准时间,每隔4年会闰秒1s。

优点

1.根据时间位和最后12位增序,ID整体有序

2.不依赖第三方的网络资源,稳定性好,性能高

3.各标识位的长度可以根据自身业务调整,满足个性化的业务需求

缺点

1.依赖系统时间,系统时间会因为闰秒或其他原因发生回拨,导致生成重复ID。

改进版的Snowflake方案

把生成ID的系统时间保存到缓存,并按照一定时间周期(例如3s)进行第三方依赖报备,如Zookeeper(美团Leaf-Snowflake方案),如果ID服务器重启,可以先从ZK中获取上次报备的系统时间,如果ZK重启,那么ID服务器可以从本地缓存中获取上次生成ID的系统时间。这样的架构设计,使的ID服务器与第三方依赖实现弱依赖,保证整个服务架构高可用。如果发生时钟回拨的范围较小,当前ID服务可以等待一会再生成ID;如果发生时钟回拨的范围较大,那么需要报警和人工介入。

优点

1.集成了Snowflake的优点,还最大限度克服系统时钟回拨的问题;

Demo实现

对于不同的业务需求,可以对64位ID的业务规则进行重新定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
package com.hledu.ecourse.cache.juc;

import java.util.HashMap;
import java.util.Map;

/**
* Twitter_Snowflake<br>
* SnowFlake的结构如下(每部分用-分开):<br>
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
* 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
* 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
* 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
* 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
* 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
* 加起来刚好64位,为一个Long型。<br>
* SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
*/
public class SnowflakeIdWorker {

public static final Map<String, Long> lstTime = new HashMap<>();

// ==============================Fields===========================================
/** 开始时间截 (2015-01-01) */
private final long twepoch = 1420041600000L;

/** 机器id所占的位数 */
private final long workerIdBits = 5L;

/** 数据标识id所占的位数 */
private final long datacenterIdBits = 5L;

/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

/** 支持的最大数据标识id,结果是31 */
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

/** 序列在id中占的位数 */
private final long sequenceBits = 12L;

/** 机器ID向左移12位 */
private final long workerIdShift = sequenceBits;

/** 数据标识id向左移17位(12+5) */
private final long datacenterIdShift = sequenceBits + workerIdBits;

/** 时间截向左移22位(5+5+12) */
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);

/** 工作机器ID(0~31) */
private long workerId;

/** 数据中心ID(0~31) */
private long datacenterId;

/** 毫秒内序列(0~4095) */
private long sequence = 1024L;

/** 上次生成ID的时间截 */
private long lastTimestamp = -1L;

//==============================Constructors=====================================
/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowflakeIdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}

// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
lastTimestamp = (lstTime.get("lstTime")==null? -1L:lstTime.get("lstTime"));
//时钟发生了回拨,此刻时间小于上次发号时间
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
//时钟回拨在5ms内,则等待两倍offset后重试获取时间
if (offset <= 5) {
try {
//时间偏差大小小于5ms,则等待两倍时间
Thread.sleep(offset << 1);
timestamp = timeGen();
if (timestamp < lastTimestamp) {
//还是小于,抛异常并上报
//throwClockBackwardsEx(timestamp);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
//throwClockBackwardsEx(timestamp);
}
}

//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}

//上次生成ID的时间截
lastTimestamp = timestamp;
lstTime.put("lstTime",lastTimestamp);

//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (datacenterId << datacenterIdShift) //
| (workerId << workerIdShift) //
| sequence;
}

/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}

/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}

//==============================Test=============================================
/** 测试 */
public static void main(String[] args) {
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
for (int i = 0; i < 1000; i++) {
long id = idWorker.nextId();
System.out.println(Long.toBinaryString(id));
System.out.println(id);
}
}
}
谢谢你请我吃糖果