TwitterSnowflake自增ID算法

32次阅读

共计 2977 个字符,预计需要花费 8 分钟才能阅读完成。

简介

Twitter 早期用 MySQL 存储数据,随着用户的增长,单一的 MySQL 实例没法承受海量的数据,后来团队就研究如何产生完美的自增 ID,以满足两个基本的要求:

  • 每秒能生成几十万条 ID 用于标识不同的 记录;
  • 这些 ID 应该可以有个大致的顺序,也就是说发布时间相近的两条记录,它们的 ID 也应当相近,这样才能方便各种客户端对记录 进行排序。

Twitter-Snowflake 算法就是在这样的背景下产生的。

核心

Twitter 解决这两个问题的方案非常简单高效:每一个 ID 都是 64 位数字,由时间戳、工作机器节点和序列号组成,ID 是由当前所在的机器节点生成的。如图:

下面先说明一下各个区间的作用。

  • 符号位:用于区分正负数。1 为负数,0 为整数。一般不需要负数,所以值固定为 0。
  • 时间戳 :一共预留 41bit 保存毫秒级时间戳。因为毫秒级时间戳长度是 13 位:41 位二进制最大值(T) 是:$2^{41}-1 = 2199023255551 $ , 刚好 13 位。可以表示的年份 = T / (3600 24 365 * 1000) = 69.7 年。换算成 Unix 时间也就是可以表示到:2039-09-07 23:47:35:

大家会觉得这个时间不够用啊,没关系,后面会讲如何优化。

  • 工作机器:预留了 10bit 保存机器 ID。只要机器 ID 不一样,每毫秒生成的 ID 是不一样的。一共可以支持多少台机器同时生成 ID 呢?答案是 1023 台($2^{10}-1$)。

    如果工作机器比较少,可以使用配置文件来设置这个 id,或者使用随机数。如果机器过多就得单独实现一共工作机器 ID 分配器了,比如使用 redis 自增,或者利用 Mysql auto_increment 机制也可以达到效果。

  • 序列号:序列号一共是 12bit,为了处理在同一机器同一毫秒内需要给多条消息分配 id 的情况,一共可以产生 4095 个序列号(0~4095,$2^{12}-1$)。

综上:同一台机器 1 毫秒内可产生 4095 个 ID,全部机器 1 毫秒内可产生 4095 * 1023 个 ID。由于全是在各个机器本地生成,效率非常高。

简单实现

下面是一个简单实现:仅有时间戳,机器位为 0,序列号为 0:

#include <stdio.h>

int main()
{
    long long id;
    id = 1572057648000 << 22; // 相当于 id = 1572057648000 << 22 | 0 << 12 | 0;
    printf("id=%lld\n", id);
   
   return 0;
}

输出:

id=6593687681236992000

代码实现主要用到了左移和或位运算(或运算),各个语言类似。上面的实现输出的结果是一个 19 位长度的整数。

优化

1、时间戳优化

如果时间戳取当前毫秒级时间戳,那么只能表示到 2039 年,远远不够。我们发现,1970 到当前时间这个区间其实是永远都不会用了,那么,为何不使用偏移量呢?也就是时间戳部分不直接取当前毫秒级时间戳,而是在此基础上减去一个过去时间:

id = (1572057648000 - 1569859200000) << 22; 

输出:

id=9220959240192000

上面代码中,第一个时间戳是当前毫秒级时间戳,第二个则是一个过去时间戳(1569859200000 表示 2019-10-01 00:00:00)。这样我们可以表示的年大概是 当前年份(例如 2019) + 69 = 2088 年,很长一段时间内都够用。

2、序列号

序列号默认取 0,如果已经使用了则自增。若自增到 4096,也就是同一毫秒内的序列号用完了,怎么办呢?需要等待至下一毫秒。部分代码示例:

// 同一毫秒并发调用
if (ts == (iw.last_time_stamp)) {
    // 序列号自增
    iw.sequence = (iw.sequence+1) & MASK_SEQUENCE;

    // 序列号自增到最大值 4096,4095 & 4096 = 0
    if (iw.sequence == 0) {
        // 等待至下一毫秒
        ts = time_re_gen(ts);
    }
} else { // 同一毫秒没有重复的
    iw.last_time_stamp = ts;
}

算法变种

1、53bits 版本:因为 js 只支持 53 位 bit 的数值

* 0 32 51 53
+-----------+------+------+
|0|time(32) |workid(8) |seq(12) |
+-----------+------+------+

2、其它版本

我们也可以根据自己的业务需求,将不同区间的 bit 位进行调整。机器位和序列号 ID 并不是必须的,可以合并。或者拆分出更多的区间表示更多的意义。例如订单号:

* 0 41 47 59  64
+-----------+------+------+------+------+
|0|time(41) |workid(6) |seq(12) | uid(4)
+-----------+------+------+------+------+

我们对订单分 16 个(2^4)表,每次将 uid & 0xF(也就是 uid & 15)的结果放到后四位,这样以后根据 uid 查订单的时候,uid mod 16 就能得到数据在哪个分表;同时根据订单 ID 本身也能找到对应的分表。示例:

php > echo 1572070381000 << 22 | 1 << 16 | 0 << 4 | (1820 & 15);
6593741087309889548
php > echo 1572070381000 << 22 | 1 << 16 | 0 << 4 | (5177331 & 15);
6593741087309889539

验证测试:

php > echo 1572070381000 << 22 | 1 << 16 | 0 << 4 | (5177331 & 15);
6593741087309889539
php > echo 6593741087309889548 % 16;
12
php > echo 1820 % 16;
12
php > echo 6593741087309889539 % 16;
3
php > echo 5177331 % 16;
3

从上面的结果可以看出来,uid、订单号都能定位到相同的分表。

对一个 2 的 n 次幂的数 num 取模(2^n),本质就是 num 对应二进制的末尾 n 个 bit 的和取模。

代码实现

参考网上其它语言的版本,自己写了 C 和 PHP 版本的:

  • snowflake-c/snowflake.c at master · 52fhy/snowflake-c
    https://github.com/52fhy/snow…
  • 52fhy/IDWork: Twitter 的 Snowflake 的 PHP 版
    https://github.com/52fhy/IDWork

github 上其它版本:

  • go 语言版本:已用于生产环境,稳定
    https://github.com/bwmarrin/s…
  • php c 扩展版:未使用过

    fgy58963/php_snowflake: 推特分布式主键生成算法的 php 扩展
    https://github.com/fgy58963/p…

参考

1、Twitter-Snowflake,64 位自增 ID 算法详解 – 漫漫路
https://www.lanindex.com/twit…

2、多 key 业务,数据库水平切分架构一次搞定
https://mp.weixin.qq.com/s/PC…

本文首发于公众号 ” 飞鸿影的博客(fhyblog)”,欢迎关注。博客地址:https://52fhy.cnblogs.com。

(本文完)

正文完
 0