Redis有 高性能,高并发和数据一致性的保证,以及断电数据不丢失,分布式扩展能力等优势。所以业界普遍使用Redis来存储并持久化序列和业务规则来维护Unique ID。
为了构建分布式的数据库集群,通常都会采用分库和分表(分片)的方式,在这种要求下,我们将规则的配置和序列都放在Redis,以便于提供独立的全局序列生成服务,而不用担心数据库伸缩带来的影响。
SnowFlake也是一个派号器,基于Thrift的服务,不过不是用redis简单自增,而是类似UUIDversion1,只有一个Long 64bit的长度,所以IdWorker紧巴巴的分配成:
- 时间戳(42bit),从2012年以来的毫秒数,能撑139年。
- 自增序列(12bit,最大值4096),毫秒之内的自增,过了一毫秒会重新置0。
- DataCenter ID (5 bit, 最大值32),数据中心ID,配置值。
- Worker ID ( 5 bit, 最大值32),派号器的ID,配置值,所以一个数据中心里最多32个派号器,还会在ZK里做下注册。
因为是派号器,把机器标识和进程标识都省出来了,所以能够只用一个Long表达。
但是,这种派号器,client每次只能一个ID,不能批量取,所以增加了额外的延时(网络、并发竞争)。
代码虽然露出来了,但其实已经不可用了,可能觉得用原生的Thirft做传输层不够好,snowflake后来又收回去了不对外开源了。
微博使用了秒级的时间,用了30bit,Sequence用了15位,理论上可以搞定3.2w/s的速度。用4bit来区分IDC,也就是可以支持16个 IDC,对于核心机房来说够了。剩下的有2bit 用来区分业务,由于当前发号服务是机房中心式的,1bit 来区分热备。是的,也没有用满64bit。
UUID(Universally Unique Identifier)全局唯一标识符,是指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的。UUID的目的,是让分布式系统中的所有元素,都能有唯一的辨识资讯,而不需要透过中央控制端来做辨识资讯的指定。如此一来,每个人都可以建立不与其它人冲突的 UUID。在这样的情况下,就不需考虑数据库建立时的名称重复问题。
按照开放软件基金会(OSF)制定的标准计算,用到了以太网卡地址、纳秒级时间(开头)、芯片ID码和许多可能的数字,UUID的唯一缺陷在于生成的结果串会比较长。
这是一个软件建构的标准,也是被开源软件基金会(Open Software Foundation, OSF)的组织在分布式计算环境(DistributedComputing Environment, DCE)领域的一部份。目前最广泛应用的UUID,即是微软的 Microsoft's Globally Unique Identifiers (GUIDs)
在Java使用UUID.randomUUID()来生成,结果是十六进制的字符串。
是一个128bit的数字,也可以表现为32个16进制的字符,中间用"-"分割。如:f81d4fae-7dec-11d0-a765-00a0c91e6bf6
- 时间戳+UUID版本号,分三段占16个字符(60bit+4bit),
- Clock Sequence号与保留字段,占4个字符(13bit+3bit),
- 节点标识占12个字符(48bit),
目前UUID定义有5个版本:
Ver Description
1 基于时间的版本(本标准)
2 使用嵌入式POSIX(DCE安全版本)
3 使用MD5哈希的基于名称的版本(本标准)
4 基于随机数的版本(本标准)
5 使用SHA-1的基于名称的版本(本标准)
其中Version1更适用于一般Unique ID的需求。
Version1严格守着原来各个位的规矩:
- UTC时间(60bit),以100纳秒为1,从1582年10月15日算起,能撑3655年。
- 节点Node标识(48bit),一般用MAC地址表达,如果有多块网卡就随便用一块。如果没网卡,就用随机数凑数,或者拿一堆尽量多的其他的信息,比如主机名什么的,拼在一起hash。
- 顺序号(16bit),仅用于避免前面的节点标示改变(如网卡改了),时钟系统出问题(如重启后时钟快了慢了),让它随机一下避免重复。
不过上面没考虑到一台机器的多进程并发问题,所以严格的Version1没人实现,就出现了下面各个变种。
Hibernate的CustomVersionOneStrategy.java,解决了之前version1的两个问题:
- 时间戳(6bytes, 48bit):毫秒级别 (之前是0.01纳秒),从1970年算起,能撑8925年。
- 顺序号(2bytes, 16bit, 最大值65535): 没有时间戳过了一秒要归零的事,各搞各的,short溢出到了负数就归0。
- 机器标识(4bytes, 32bit):拿localHost的IP地址,IPV4呢正好4个byte,但如果是IPV6要16个bytes,就只拿前4个byte。
- 进程标识(4bytes 32bit):用时间戳右移8位再取整数应付,不同线程一般不会同时启动。
值得留意就是,机器进程和进程标识组成的64bit Long几乎不变,只变动另一个Long就够了。
MongoDB的ObjectId.java,处理比Hibernate更加细致:
- 时间戳(4 bytes, 32bit):是秒级别的,从1970年算起,能撑136年。
- 自增序列(3bytes, 24bit, 最大值1600w):是一个从随机数开始(机智)的Int不断加一,也没有时间戳过了一秒要归零的事,各搞各的。因为只有3bytes,所以一个4bytes的Int还要截一下后3bytes。
- 机器标识(3bytes, 24bit): 将所有网卡的Mac地址拼在一起做个HashCode,同样一个int还要截一下后3bytes。搞不到网卡就用随机数混过去。
- 进程标识(2bytes, 16bits):从JMX里搞回来到进程号,搞不到就用进程名的hash或者随机数混过去。
可见,MongoDB的每一个字段设计都比Hibernate的更合理一点,比如时间戳是秒级别的。总长度也降到了12bytes,96bit,但如果果用64bit长的Long来保存有点不上不下的,只能表达成byte数组或16进制字符串。
上述的UUID已经能满足本地生成UniqueID的需求,只是占用了128bit,太长了,影响效率。如果能根据业务需求,缩减成64bit一个long的话,就Perfect了。
我们可以从UUID的3个组成部分来分析:时间戳 +顺序号(自增) + 程序号(本地标识)
合理地压缩字段:
l 时间戳,秒级别,1年要24位,两年要25位.....
l 顺序号(自增),6万QPS要16位,10万要17位...
l 程序号,剩下20-24位,百万分之一到一千六百万分之一的重复率。可以把网卡Mac(48bit) +进程号(15bit) 拼在一起再hash,取结果32个bit的后面20-24个bit。但这样还是避免不了重复。
l 程序号采用分配的形式,每个进程/线程启动的时候申请一个唯一的WorkerID,关闭的时候再回收。20bit能支持100w个WorkerID,应该也够用了。WorkerID可以通过Redis/ZK/MySQL/单独服务来管理。
转自@一乐和@江南白衣的博文:
http://ericliang.info/what-kind-of-id-generator-we-need-in-business-systems
http://calvin1978.blogcn.com/articles/uuid.html?from=groupmessage&isappinstalled=0