在分布式系统存在多个 Shard 的场景中, 同时在各个 Shard 插入数据时, 怎么给这些数据生成全局的 unique ID? 在单机系统中 (例如一个 MySQL 实例), unique ID 的生成是非常简单的, 直接利用 MySQL 自带的自增 ID 功能就可以实现.

但在一个存在多个 Shards 的分布式系统 (例如多个 MySQL 实例组成一个集群, 在这个集群中插入数据), 这个问题会变得复杂, 所生成的全局的 unique ID 要满足以下需求:

  • 唯一性,保证生成的 ID 全局唯一
  • 今后数据在多个 Shards 之间迁移不会受到 ID 生成方式的限制
  • 有序性,生成的 ID 中最好能带上时间信息, 例如 ID 的前 k 位是 Timestamp, 这样能够直接通过对 ID 的前 k 位的排序来对数据按时间排序
  • 生成的 ID 最好不大于 64 bits
  • 可用性,生成 ID 的速度有要求. 例如, 在一个高吞吐量的场景中, 需要每秒生成几万个 ID (Twitter 最新的峰值到达了 143,199 Tweets/s, 也就是 10万+/秒)
  • 整个服务最好没有单点

在要满足前面 6 点要求的场景中, 怎么来生成全局 unique ID 呢?

数据库自增ID

数据库单表,使用 auto increment 来生成唯一全局递增ID。

优势是无需额外附加操作,定长增长,单表结构中唯一性,劣势是高并发下性能不佳,生产的上限是数据库服务器单机的上限,水平扩展困难,分布式数据库下,无法保证唯一性。

UUID

如果没有上面这些限制, 问题会相对简单, 例如: 直接利用 UUID.randomUUID() 接口来生成 unique ID (http://www.ietf.org/rfc/rfc4122.txt). 但这个方案生成的 ID 有 128 bits, 另外, 生成的 ID 中也没有带 Timestamp 一般编程语言中自带 UUID 实现, Java 中 UUID.randomUUID().toString() 产生的ID 不依赖数据库实现。

优势是,本地生成ID,无需远程调用,全局唯一,水平扩展能力好。劣势是,ID 有 128 bits 长,占空间大,生成字符串类型,索引效率低,生成的 ID 中没有带 Timestamp 无法保证时间递增。

Flickr 全局主键

Flickr 的做法1 是使用 MySQL 的自增ID, 和 replace into 语法。但他这个方案 ID 中没有带 Timestamp, 生成的 ID 不能按时间排序

创建64位自增ID,首先创建表

CREATE TABLE `Tickets64` (
  `id` bigint(20) unsigned NOT NULL auto_increment,
  `stub` char(1) NOT NULL default '',
  PRIMARY KEY  (`id`),
  UNIQUE KEY `stub` (`stub`)
) ENGINE=MyISAM

SELECT * from Tickets64 假设表中有一行

+-------------------+------+
| id                | stub |
+-------------------+------+
| 72157623227190423 |    a |
+-------------------+------+

那么如果需要产生一个新的全局 64 bits 的ID,只要执行 SQL:

REPLACE INTO Tickets64 (stub) VALUES ('a');
SELECT LAST_INSERT_ID();

SQL 返回的ID就是要产生的全局唯一ID。使用 REPLACE INTO 代替 INSERT INTO 的好处是避免表行数太多。 stub 要设为唯一索引。

Flickr 内部运行两台 ticket servers,通过两台机器做主备和负载均衡。

TicketServer1:
auto-increment-increment = 2
auto-increment-offset = 1

TicketServer2:
auto-increment-increment = 2
auto-increment-offset = 2

Twitter Snowflake

Twitter 利用 Zookeeper 实现一个全局的 ID 生成服务 Snowflake: https://github.com/twitter/snowflake

Snowflake 生成的 unique ID 的组成 (由高位到低位):

  • 41 bits: Timestamp 毫秒级
  • 10 bits: 节点 ID datacenter ID 5 bits + worker ID 5 bits
  • 12 bits: sequence number

一共 63 bits ,其中最高位是 0

unique ID 生成过程:

  • 41 bits 的 Timestamp: 每次要生成一个新 ID 的时候, 都会获取一下当前的 Timestamp, 然后分两种情况生成 sequence number:

    <div class="highlighter-rouge"><div class="highlight"><pre class="highlight prettyprint prettyprinted" style=""><code><span class="pln">  </span><span class="pun">-</span><span class="pln"> </span><span class="pun">如果当前的</span><span class="pln"> </span><span class="typ">Timestamp</span><span class="pln"> </span><span class="pun">和前一个已生成</span><span class="pln"> ID </span><span class="pun">的</span><span class="pln"> </span><span class="typ">Timestamp</span><span class="pln"> </span><span class="pun">相同</span><span class="pln"> </span><span class="pun">(在同一毫秒中),</span><span class="pln"> </span><span class="pun">就用前一个</span><span class="pln"> ID </span><span class="pun">的</span><span class="pln"> sequence number </span><span class="pun">+</span><span class="pln"> </span><span class="lit">1</span><span class="pln"> </span><span class="pun">作为新的</span><span class="pln"> sequence number </span><span class="pun">(</span><span class="lit">12</span><span class="pln"> bits</span><span class="pun">);</span><span class="pln"> </span><span class="pun">如果本毫秒内的所有</span><span class="pln"> ID </span><span class="pun">用完,</span><span class="pln"> </span><span class="pun">等到下一毫秒继续</span><span class="pln"> </span><span class="pun">(**这个等待过程中,</span><span class="pln"> </span><span class="pun">不能分配出新的</span><span class="pln"> ID</span><span class="pun">**)</span><span class="pln">
    

    - 如果当前的 Timestamp 比前一个 ID Timestamp 大, 随机生成一个初始 sequence number (12 bits) 作为本毫秒内的第一个 sequence number