2019独角兽企业重金招聘Python工程师标准>>>
需求
为什么需要唯一ID
让分布式系统中的需要辨别的元素,都能有唯一的辨识标志。 几乎所有的业务系统,都有生成一个记录标识的需求,例如:
- 消息标识:message-id
- 订单标识:order-id
- 帖子标识:tiezi-id
为什么需要趋势有序
记录标识上的查询,往往又有分页或者排序的业务需求,例如:
- 拉取最新的一页消息:select message-id order by time limit 100
- 拉取最新的一页订单:select order-id order by time limit 100
- 拉取最新的一页帖子:select tiezi-id order by time limit 100
所以往往要有一个time字段,并且在time字段上建立普通索引(non-cluster index)。
普通索引存储的是实际记录的指针,其访问效率一般会比聚集索引慢,如果记录标识在生成时能够基本按照时间有序,则可以省去这个time字段的索引查询:select message-id (order by message-id) limit 100
但是,能这么做的前提是,message-id的生成基本是趋势时间递增的。
怎么实现唯一ID
UUID
UUID就是为了要在分布式环境中产生唯一标示符而发布的一个标准。标准中规定UUID长度为16Bytes(128Bits),一般将其表示为550e8400-e29b-41d4-a716-446655440000
这种16进制格式,同时将其分为5部分,每部分用-
分割,各部分长度分别为8,4,4,12。现在使用的UUID算法有5个版本,分别使用5种不同的算法计算产生。
- UUID1: 依据当前计算机的MAC地址和时钟来生成uuid。
- UUID2: 和版本1类似,不过使用域标示符和本地UID代替了版本1中的时钟信息。
- UUID3: 根据url,域标示符等标示符做MD5 Hash产生的。
- UUID4: 根据产生的随机数来生成。
- UUID5: 和版本3类似,只不过替换成了SHA-1算法。
优点:
- 本地生成,不需要控制中心管理,成本低
- 性能好
缺点:
- id共128Bits太长
- id间没有次序关系,不能隐含信息
mogodb ObjectId
MongoDB中每一条记录都有一个id
字段用来唯一标示本记录。如果用户插入数据时没有显示提供id
字段,那么系统会自动生成一个。ObjectID一共12Bytes,设计的时候充分考虑了分布式环境下使用的情况,所以能保证在一个分布式MongoDB集群中唯一。ObjectID格式如下:
0 4 7 9 12
+--------+------+----+------+
|time |pc |pid |inc |
+--------+------+----+------+
0~4 Byte是Unix Timestamp。 4~7 Byte是当前机器“hostname/mac地址/虚拟编号”其中之一的MD5结果的前3个字节。 7~9 Byte是当前进程的PID。 9~12Byte是累加计数器或是一个随机数(只有当不支持累加计数器时才用随机数)。 最后生成的仍然是一个用16进制表示的串,如47cc67093475061e3d95369d。这里MongoDB的ObjectID相对UUID有个很大的优点就是ObjectID是时间上有序的。另外还有ObjectID本身也包含了很多其它有用的信息,通过直接解码ObjectID即可直接获得这些信息。
优点:
- 时间有序
- 隐含信息,可在业务中结合加以利用。
缺点:
- 当time段一样,由于MD5只取前3Byte,有可能造成pc段一样,这样就有可能有重复的id。
- ID 间隙较大(当某一段时间不生成id,那么这个time段浪费很多空间)
snowflack
Snowflake是twitter开源的一款独立的适用于分布式环境的ID生成服务器。生成的ID是64Bits,同时满足高性能(>10K ids/s),低延迟(<2ms)和高可用。与MongoDB ObjectID类似这里生成的ID也是时间上有序的。编码方式也和ObjectID类似,如下:
0 41 51 64
+-----------+------+------+
|time |pc |inc |
+-----------+------+------+
前41bits是以微秒为单位的timestamp。 接着10bits是事先配置好的机器ID。 最后12bits是累加计数器。
有缺点与MongoDB ObjectId类似。但是只要机器ID不重复,应该不会出现重复的ID。
Instagram采用的方式
Instagram要将其中存储的图片分片到多个PostgreSQL中,其中生成ID的方案和MongoDB ObjectID类似。整个ID的长度为64Bits,设定为这个长度是为了优化在redis中的存储。ID的编码格式如下:
41bits以微秒为单位的timestamp,时间起点从2011-01-01开始。 13bits表示进行逻辑分片的Shard ID。 10bits表示一个累加计数器。 ID的生成逻辑用PL/PGSQL语言写到PostgreSQL数据库中,当每次插入数据时由数据库自动计算生成。 与上面优缺点类似。
Leaf
主要参考:http://wiki.sankuai.com/pages/viewpage.action?pageId=465861190。 利用step设置每个服务能从数据库拿到的号段大小,能充分的利用id的空间,能保证号段内各个id的时间顺序,但是不能保证号段间时间上的顺序。
主要优点是id占用字节少(64bits),能充分利用空间,几乎没有间隙(按作者说,除非服务器宕机,这种可能会比较小)。
我的想法:
假设应用生命周期为30年(一般极少有应用生命周期30年,linux系统到现在也不超过30年,就算30年到时候也该换方案和架构了),如果时间的精确度是微秒,30年需要通过12位整数保存,使用二进制保存所有12位整数需要大约40位二进制;如果是秒,需要9位整数保存,使用大约30位二进制。假设63位中(除最高位,最高位应该是符号位。)
- 使用微秒方案:前40位给时间,那么还有23位可以给step区间(可表示8百万个整数,相当于容量为1微秒8百万个id)。
- 使用秒方案:前30位给时间,那么还有33位可以给step区间(每秒产生id数量与使用微妙方案一秒产生的id数量相同)。
- 使用X秒方案:以此类推
对比秒方案和微秒方案,(X)秒方案可能由于时间对系统能表述的id空间的浪费更少,而且整体能表述的id数量不变,但是递增趋势更弱(使用微妙,递增趋势更强)。
总结
一般在分布式系统中,与生成唯一ID有关的因素可以来自:
- 时间(基于某一时刻到现在的相对时间,更节约空间)
- 机器逻辑区分ID(如:机器ID,存储的分片)
- 机器的硬件信息(如:MAC地址等)
- 局部自增