关于snowflake发号器算法简单学习

Posted viczhang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于snowflake发号器算法简单学习相关的知识,希望对你有一定的参考价值。

概述

  在分布式系统中,有一些需要使用全局唯一的ID编号,最常使用的方法是在每个系统间传递和保存一个统一唯一流水号,通过系统间两辆核对或者第三方核对唯一流水号来保证各个系统之间步伐一致,没有掉队的行为,也就是系统间状态一致,在互联网的世界里,产生唯一流水号的服务系统俗称发号器。

  当前业务系统的ID使用数据库的自增字段,自增字段完全依赖于数据库,这在数据库移植,扩容,洗数据,分库分表等操作时带来了很多麻烦。

在数据库分库分表时,有一种办法是通过调整自增字段或者数据库sequence的步长来达到跨数据库的ID的唯一性,但仍然是一种强依赖数据库的解决方案,有诸多的限制,并且强依赖数据库类型,我们并不推荐这种方法。

  UUID虽然能够保证ID的唯一性,但是,它无法满足业务系统需要的很多其他特性,例如:时间粗略有序性,可反解和可制造型。另外,UUID产生的时候使用完全的时间数据,性能比较差,并且UUID比较长,占用空间大,间接导致数据库性能下降,更重要的是,UUID并不具有有序性,这导致B+树索引在写的时候会有过多的随机写操作(连续的ID会产生部分顺序写),另外写的时候由于不能产生顺序的append操作,需要进行insert操作,这会读取整个B+树节点到内存,然后插入这条记录后写整个节点回磁盘,这种操作在记录占用空间比较大的情况下,性能下降比较大。

需求分析和整理

1) 解决分库分表中唯一序号的问题
2) 解决分布式应用或者微服务框架中唯一序号的问题
3) 提供可定制化生成规则,根据业务需求可自定义扩展
4) 性能高效且系统简单稳定
5) 系统可任意扩展

结构

snowflake的结构如下(每部分用-分开):

0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000

  • 1位,不用。二进制中最高位为1的都是负数,但是我们生成的id一般都使用整数,所以这个最高位固定是0
  • 41位,用来记录时间戳(毫秒)。

    • 41位可以表示241?1个数字,
    • 如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 至 241?1,减1是因为可表示的数值范围是从0开始算的,而不是1。
    • 也就是说41位可以表示241?1个毫秒的值,转化成单位年则是(241?1)/(1000?60?60?24?365)=69
  • 10位,用来记录工作机器id。

    • 可以部署在210=1024个节点,包括5位datacenterId5位workerId
    • 5位(bit)可以表示的最大正整数是25?1=31,即可以用0、1、2、3、....31这32个数字,来表示不同的datecenterId或workerId
  • 12位,序列号,用来记录同毫秒内产生的不同id。

    • 12位(bit)可以表示的最大正整数是212?1=4096,即可以用0、1、2、3、....4095这4096个数字,来表示同一机器同一时间截(毫秒)内产生的4096个ID序号

由于在Java中64bit的整数是long类型,所以在Java中SnowFlake算法生成的id就是long来存储的。

JAVA源码

  1 /**
  2  * Twitter_Snowflake<br>
  3  * SnowFlake的结构如下(每部分用-分开):<br>
  4  * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
  5  * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
  6  * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
  7  * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
  8  * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
  9  * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
 10  * 加起来刚好64位,为一个Long型。<br>
 11  * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
 12  */
 13 public class SnowflakeIdWorker {
 14 
 15     // ==============================Fields===========================================
 16     /** 开始时间截 (2015-01-01) */
 17     private final long twepoch = 1420041600000L;
 18 
 19     /** 机器id所占的位数 */
 20     private final long workerIdBits = 5L;
 21 
 22     /** 数据标识id所占的位数 */
 23     private final long datacenterIdBits = 5L;
 24 
 25     /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
 26     private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
 27 
 28     /** 支持的最大数据标识id,结果是31 */
 29     private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
 30 
 31     /** 序列在id中占的位数 */
 32     private final long sequenceBits = 12L;
 33 
 34     /** 机器ID向左移12位 */
 35     private final long workerIdShift = sequenceBits;
 36 
 37     /** 数据标识id向左移17位(12+5) */
 38     private final long datacenterIdShift = sequenceBits + workerIdBits;
 39 
 40     /** 时间截向左移22位(5+5+12) */
 41     private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
 42 
 43     /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
 44     private final long sequenceMask = -1L ^ (-1L << sequenceBits);
 45 
 46     /** 工作机器ID(0~31) */
 47     private long workerId;
 48 
 49     /** 数据中心ID(0~31) */
 50     private long datacenterId;
 51 
 52     /** 毫秒内序列(0~4095) */
 53     private long sequence = 0L;
 54 
 55     /** 上次生成ID的时间截 */
 56     private long lastTimestamp = -1L;
 57 
 58     //==============================Constructors=====================================
 59     /**
 60      * 构造函数
 61      * @param workerId 工作ID (0~31)
 62      * @param datacenterId 数据中心ID (0~31)
 63      */
 64     public SnowflakeIdWorker(long workerId, long datacenterId) {
 65         if (workerId > maxWorkerId || workerId < 0) {
 66             throw new IllegalArgumentException(String.format("worker Id can‘t be greater than %d or less than 0", maxWorkerId));
 67         }
 68         if (datacenterId > maxDatacenterId || datacenterId < 0) {
 69             throw new IllegalArgumentException(String.format("datacenter Id can‘t be greater than %d or less than 0", maxDatacenterId));
 70         }
 71         this.workerId = workerId;
 72         this.datacenterId = datacenterId;
 73     }
 74 
 75     // ==============================Methods==========================================
 76     /**
 77      * 获得下一个ID (该方法是线程安全的)
 78      * @return SnowflakeId
 79      */
 80     public synchronized long nextId() {
 81         long timestamp = timeGen();
 82 
 83         //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
 84         if (timestamp < lastTimestamp) {
 85             throw new RuntimeException(
 86                     String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
 87         }
 88 
 89         //如果是同一时间生成的,则进行毫秒内序列
 90         if (lastTimestamp == timestamp) {
 91             sequence = (sequence + 1) & sequenceMask;
 92             //毫秒内序列溢出
 93             if (sequence == 0) {
 94                 //阻塞到下一个毫秒,获得新的时间戳
 95                 timestamp = tilNextMillis(lastTimestamp);
 96             }
 97         }
 98         //时间戳改变,毫秒内序列重置
 99         else {
100             sequence = 0L;
101         }
102 
103         //上次生成ID的时间截
104         lastTimestamp = timestamp;
105 
106         //移位并通过或运算拼到一起组成64位的ID
107         return ((timestamp - twepoch) << timestampLeftShift) //
108                 | (datacenterId << datacenterIdShift) //
109                 | (workerId << workerIdShift) //
110                 | sequence;
111     }
112 
113     /**
114      * 阻塞到下一个毫秒,直到获得新的时间戳
115      * @param lastTimestamp 上次生成ID的时间截
116      * @return 当前时间戳
117      */
118     protected long tilNextMillis(long lastTimestamp) {
119         long timestamp = timeGen();
120         while (timestamp <= lastTimestamp) {
121             timestamp = timeGen();
122         }
123         return timestamp;
124     }
125 
126     /**
127      * 返回以毫秒为单位的当前时间
128      * @return 当前时间(毫秒)
129      */
130     protected long timeGen() {
131         return System.currentTimeMillis();
132     }
133 
134     //==============================Test=============================================
135     /** 测试 */
136     public static void main(String[] args) {
137         SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
138         for (int i = 0; i < 1000; i++) {
139             long id = idWorker.nextId();
140             System.out.println(Long.toBinaryString(id));
141             System.out.println(id);
142         }
143     }
144 }

 

我不是java出身,但是对java 代码的理解,翻译成php代码如下

(注意:有些朋友反映结果不正确,原因应该是你用了32位的php,虽然你是用64位windows系统,但如果wamp,phpstudy之类的集成的可能是32位php,我使用的是64位Linux运行正常,

请用64位的php,这算法就是64bit 的嘛)

 

 1 class IdWork {
 2     //开始时间,固定一个小于当前时间的毫秒数即可
 3     const twepoch = 1519837200000;//2018/3/1 0:0:0
 4     //时间最大位数
 5     const timestampBits = 41;
 6     //数据库标识占的位数
 7     const workerIdBits = 10;
 8     //毫秒内自增数点的位数
 9     const sequenceBits = 12;
10 
11     protected $workId = 0;
12 
13     static $lastTimestamp = -1;
14     static $sequence = 0;
15     
16     static $signBits = 1; //解析开始位数
17 
18     function __construct($workId=0){
19         //机器ID范围判断
20         $maxWorkerId = -1 ^ (-1 << self::workerIdBits);
21         if($workId > $maxWorkerId || $workId< 0){
22             throw new Exception("数据库标识数不能大于".$maxWorkerId."或者至少是0");
23         }
24         
25         //赋值
26         $this->workId = $workId;
27     }
28 
29     //生成一个ID
30     public function nextId(){
31         $timestamp = $this->timeGen();
32         $lastTimestamp = self::$lastTimestamp;
33         //判断时钟是否正常 ,服务器时钟被调整了,ID生成器停止服务
34         if ($timestamp < $lastTimestamp) {
35             throw new Exception("服务器时钟被调整了,ID生成器停止服务", ($lastTimestamp - $timestamp));
36         }
37         //生成唯一序列
38         if ($lastTimestamp == $timestamp) {
39             $sequenceMask = -1 ^ (-1 << self::sequenceBits);
40             self::$sequence = (self::$sequence + 1) & $sequenceMask;
41             if (self::$sequence == 0) {
42                 $timestamp = $this->tilNextMillis($lastTimestamp);
43             }
44         } else {
45             self::$sequence = 0;
46         }
47         self::$lastTimestamp = $timestamp;
48         
49         
50         //时间毫秒/数据中心ID/机器ID,要左移的位数
51         $timestampLeftShift = self::sequenceBits + self::workerIdBits;
52         $workerIdShift = self::sequenceBits;
53         //组合3段数据返回: 时间戳.工作机器.序列
54         $nextId = (($timestamp - self::twepoch) << $timestampLeftShift) | ($this->workId << $workerIdShift) | self::$sequence;
55         return $nextId;
56     }
57 
58     //取当前时间毫秒
59     protected function timeGen(){
60         $timestramp = (float)sprintf("%.0f", microtime(true) * 1000);
61         return  $timestramp;
62     }
63 
64     //取下一毫秒
65     protected function tilNextMillis($lastTimestamp) {
66         $timestamp = $this->timeGen();
67         while ($timestamp <= $lastTimestamp) {
68             $timestamp = $this->timeGen();
69         }
70         return $timestamp;
71     }
72 }

 

 调用以上代码运行执行结果如下:

1 for($i=0;$i<5;$i++){
2             $work = new IdWork(42);
3             $id = $work->nextId();
4             echo  microtime(true).------.$id."<br>";
5         }

结果如下:

1 1520230938.6467------1651459582238720
2 1520230938.6467------1651459582238721
3 1520230938.6467------1651459582238722
4 1520230938.6468------1651459582238723
5 1520230938.6468------1651459582238724

扩展

在理解了这个算法之后,其实还有一些扩展的事情可以做:

1.根据自己业务修改每个位段存储的信息。算法是通用的,可以根据自己需求适当调整每段的大小以及存储的信息。

2.解密id,由于id的每段都保存了特定的信息,所以拿到一个id,应该可以尝试反推出原始的每个段的信息。反推出的信息可以帮助我们分析。比如作为订单,可以知道该订单的生成日期,负责处理的数据中心等等。

但是网上都没有给出如何进行对获取的ID 进行反解析,下面是给出反解析方法

 1 public function parseID($id) {
 2         $totalBits = 1 << 6;
 3         $signBits = self::$signBits;
 4         $timestampBits = self::timestampBits;
 5         $workerIdBits = self::workerIdBits;
 6         $sequenceBits = self::sequenceBits;
 7 
 8 
 9         $sequence = ($id << ($totalBits - $sequenceBits)) >> ($totalBits - $sequenceBits);
10         $workerid= ($id << ($timestampBits + $signBits)) >> ($totalBits - $workerIdBits);
11         $deltaSeconds = $id >> ($workerIdBits + $sequenceBits)
12         
13         $thatTime = self::twepoch + $deltaSeconds;
14         return $thatTime.___.$workerid."___.$sequence."<br/>";
15     }

调用方法如下:

1 for($i=0;$i<5;$i++){
2             $work = new IdWork(42);
3             $id = $work->nextId();
4             echo  microtime(true).------.$id."<br>";
5            $workid = $work->parseID($id);
6            echo $workid;
7         }

 

运行结果如果下:

 1 1520231313.1713------1653030449750016
 2 1520231313171___42___0
 3 1520231313.1713------1653030449750017
 4 1520231313171___42___1
 5 1520231313.1713------1653030449750018
 6 1520231313171___42___2
 7 1520231313.1713------1653030449750019
 8 1520231313171___42___3
 9 1520231313.1713------1653030449750020
10 1520231313171___42___4

如果有错误的地方,请大家提出更正

 

以上是关于关于snowflake发号器算法简单学习的主要内容,如果未能解决你的问题,请参考以下文章

发号器的设计

lua学习笔记之-----5行代码完成ID发号器

FAQ系列 | 用MySQL实现发号器

野谈系列之高性能可定制化分布式发号器

野谈系列之高性能可定制化分布式发号器

分布式ID生成器PHP+Swoole实现介绍