关于订单号的生成,一些比较简单的方案:
1、数据库自增长ID
- 优势:无需编码
- 缺陷:
- 大表不能做水平分表,否则插入删除时容易出现问题
- 高并发下插入数据需要加入事务机制
- 在业务操作父、子表(关联表)插入时,先要插入父表,再插入子表
2、时间戳+随机数
- 优势:编码简单
- 缺陷:随机数存在重复问题,即使在相同的时间戳下。每次插入数据库前需要校验下是否已经存在相同的数值。
3、时间戳+会员ID
- 优势:同一时间,一个用户不会存在两张订单
- 缺陷:会员ID也会透露运营数据,鸡生蛋,蛋生鸡的问题
4、GUID/UUID
- 优势:简单
- 劣势:用户不友好,索引关联效率较低。
今天要分享的方案:来自twitter的SnowFlake
Twitter-Snowflake算法产生的背景相当简单,为了满足Twitter每秒上万条消息的请求,每条消息都必须分配一条唯一的id,这些id还需要一些大致的顺序(方便客户端排序),并且在分布式系统中不同机器产生的id必须不同.Snowflake算法核心把时间戳,工作机器id,序列号(毫秒级时间41位+机器ID 10位+毫秒内序列12位)组合在一起。
在上面的字符串中,第一位为未使用(实际上也可作为long的符号位),接下来的41位为毫秒级时间,然后5位datacenter标识位,5位机器ID(并不算标识符,实际是为线程标识),然后12位该毫秒内的当前毫秒内的计数,加起来刚好64位,为一个Long型。
除了最高位bit标记为不可用以外,其余三组bit占位均可浮动,看具体的业务需求而定。默认情况下41bit的时间戳可以支持该算法使用到2082年,10bit的工作机器id可以支持1023台机器,序列号支持1毫秒产生4095个自增序列id。下文会具体分析。
Snowflake – 时间戳
这里时间戳的细度是毫秒级,具体代码如下,建议使用64位linux系统机器,因为有vdso,gettimeofday()在用户态就可以完成操作,减少了进入内核态的损耗。
1
2
3
4
5
6
|
uint64_t
generateStamp
(
)
{
timeval
tv
;
gettimeofday
(
&
tv
,
0
)
;
return
(
uint64_t
)
tv
.
tv_sec *
1000
+
(
uint64_t
)
tv
.
tv_usec
/
1000
;
}
|
默认情况下有41个bit可以供使用,那么一共有T(1llu << 41)毫秒供你使用分配,年份 = T / (3600 * 24 * 365 * 1000) = 69.7年。如果你只给时间戳分配39个bit使用,那么根据同样的算法最后年份 = 17.4年。
Snowflake – 工作机器id
严格意义上来说这个bit段的使用可以是进程级,机器级的话你可以使用MAC地址来唯一标示工作机器,工作进程级可以使用IP+Path来区分工作进程。如果工作机器比较少,可以使用配置文件来设置这个id是一个不错的选择,如果机器过多配置文件的维护是一个灾难性的事情。
这里的解决方案是需要一个工作id分配的进程,可以使用自己编写一个简单进程来记录分配id,或者利用Mysql auto_increment机制也可以达到效果。
工作进程与工作id分配器只是在工作进程启动的时候交互一次,然后工作进程可以自行将分配的id数据落文件,下一次启动直接读取文件里的id使用。这个工作机器id的bit段也可以进一步拆分,比如用前5个bit标记进程id,后5个bit标记线程id之类:D
Snowflake – 序列号
序列号就是一系列的自增id(多线程建议使用atomic),为了处理在同一毫秒内需要给多条消息分配id,若同一毫秒把序列号用完了,则“等待至下一毫秒”。
1
2
3
4
5
6
7
8
|
uint64_t
waitNextMs
(
uint64_t
lastStamp
)
{
uint64_t
cur
=
0
;
do
{
cur
=
generateStamp
(
)
;
}
while
(
cur
<=
lastStamp
)
;
return
cur
;
}
|
总体来说,是一个很高效很方便的GUID产生算法,一个int64_t字段就可以胜任,不像现在主流128bit的GUID算法,即使无法保证严格的id序列性,但是对于特定的业务,比如用做游戏服务器端的GUID产生会很方便。另外,在多线程的环境下,序列号使用atomic可以在代码实现上有效减少锁的密度。
该项目地址为:https://github.com/twitter/snowflake 是用Scala实现的。核心代码:
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
|
/** Copyright 2010-2012 Twitter, Inc.*/
package
com
.
twitter
.
service
.
snowflake
import
com
.
twitter
.
ostrich
.
stats
.
Stats
import
com
.
twitter
.
service
.
snowflake
.
gen
.
_
import
java
.
util
.
Random
import
com
.
twitter
.
logging
.
Logger
/**
* An object that generates IDs.
* This is broken into a separate class in case
* we ever want to support multiple worker threads
* per process
*/
class
IdWorker
(
val
workerId
:
Long
,
val
datacenterId
:
Long
,
private
val
reporter
:
Reporter
,
var
sequence
:
Long
=
0L
)
extends
Snowflake
.
Iface
{
private
[
this
]
def
genCounter
(
agent
:
String
)
=
{
Stats
.
incr
(
"ids_generated"
)
Stats
.
incr
(
"ids_generated_%s"
.
format
(
agent
)
)
}
private
[
this
]
val
exceptionCounter
=
Stats
.
getCounter
(
"exceptions"
)
private
[
this
]
val
log
=
Logger
.
get
private
[
this
]
val
rand
=
new
Random
val
twepoch
=
1288834974657L
private
[
this
]
val
workerIdBits
=
5L
private
[
this
]
val
datacenterIdBits
=
5L
private
[
this
]
val
maxWorkerId
=
-
1L
^
(
-
1L
<<
workerIdBits
)
private
[
this
]
val
maxDatacenterId
=
-
1L
^
(
-
1L
<<
datacenterIdBits
)
private
[
this
]
val
sequenceBits
=
12L
private
[
this
]
val
workerIdShift
=
sequenceBits
private
[
this
]
val
datacenterIdShift
=
sequenceBits
+
workerIdBits
private
[
this
]
val
timestampLeftShift
=
sequenceBits
+
workerIdBits
+
datacenterIdBits
private
[
this
]
val
sequenceMask
=
-
1L
^
(
-
1L
<<
sequenceBits
)
private
[
this
]
var
lastTimestamp
=
-
1L
// sanity check for workerId
if
(
workerId
>
maxWorkerId
||
workerId
<
0
)
{
exceptionCounter
.
incr
(
1
)
throw
new
IllegalArgumentException
(
"worker Id can't be greater than %d or less than 0"
.
format
(
maxWorkerId
)
)
}
if
(
datacenterId
>
maxDatacenterId
||
datacenterId
<
0
)
{
exceptionCounter
.
incr
(
1
)
throw
new
IllegalArgumentException
(
"datacenter Id can't be greater than %d or less than 0"
.
format
(
maxDatacenterId
)
)
}
log
.
info
(
"worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d"
,
timestampLeftShift
,
datacenterIdBits
,
workerIdBits
,
sequenceBits
,
workerId
)
def
get_id
(
useragent
:
String
)
:
Long
=
{
if
(
!
validUseragent
(
useragent
)
)
{
exceptionCounter
.
incr
(
1
)
throw
new
InvalidUserAgentError
}
val
id
=
nextId
(
)
genCounter
(
useragent
)
reporter
.
report
(
new
AuditLogEntry
(
id
|