关于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

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

原文地址:https://www.cnblogs.com/viczhang/p/8494514.html

时间: 2024-10-07 15:40:28

关于snowflake发号器算法简单学习的相关文章

分布式——分布式发号器

今天停电,所以springboot源码看不了,手头刚好有本书,学习了下分布式发号器 一.方案 1.UUID 2.数据库自增序列 3.Snowflake——雪花算法 二.自定义设计需求与实现 原文地址:https://www.cnblogs.com/wqff-biubiu/p/12578508.html

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

刘兵,花名玄靖,开源技术爱好者,高性能Redis中间件NRedis-Proxy作者,目前研究方向为java中间件,微服务等技术. 一.什么是分布式发号器 说起分布式发号器的前生今世,咱们应该感恩这个时代:随着互联网在中国越来越普及化,单机系统或者一个小系统已经无法满足需要,随着用户逐渐增多,数据量越来越大,单个应用或者单个数据库已经无法满足需求,在应用以至于微服务来临,在数据库存储方面分库分表来临,可以解决问题:但是新的问题产生,怎么样做到多个应用可以有唯一主键或者序号,防止数据重复呢?分布式发

发号器的设计

数据库中的每条记录都需要一个ID,即使在分库分表后这个ID需要全局唯一性.因此,分库分表后不能使用Mysql自带的自增ID了.因为不通的库之间的ID可能是一样的. 我们以记录海量的用户信息为例,可能会想到身份证号.电话号码或者email.但是这些信息是会变的.如果用户要修改这些信息,那么ID就失效了.无异于新增一条记录,删掉原来的记录. 基于 Snowflake 算法搭建发号器 雪花算法可以提供全局唯一的ID.雪花算法生成的ID一般是分几段的,下图就是典型的ID组成:41位时间戳(一般是毫秒级?

redis实现发号器

通过mysql的auto increment自增id值可能会泄漏一些敏感的数据. 例如用户表的user_id是自增的,在url中显示的id值可能就泄露了网站真实的用户数. 下面代码通过php及redis的incrby实现简单的发号器,代码如下: function get_id($type, $server_ip, $server_port, $key) { $init_num = 0; $redis= new Redis(); $redis->connect($server_ip, $serve

FAQ系列 | 用MySQL实现发号器

问题:用MySQL实现发号器功能,确保每次取到的ID号都是唯一的实现:下面是一个大致的思路,抛个砖,欢迎回帖.根据号段大小,决定是否分成多个表,每个表事先填充各个不同的号段.每个应用端取号时,设置事务隔离级别为:REPEATABLE READ,并且采用下面的方式读取数据 SELECT `ID` FROM `ID_RANGE_XX` ORDER BY ID LIMIT 1 FOR UPDATE 在上述情境中,只要选择某个ID号,那么其他终端也在读取该号时,会产生锁等待,而不会发生ID号被重用的情况

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

版权归作者所有,任何形式转载请联系作者. --生成ID的位数比较合理长度为52个bit,然后可以时间上有序的,包含项目和实例信息 -- 在redis客户端执行: redis-cli -h 127.0.0.1 -p 6379 EVAL "$(cat ticketID.lua)" 2 01 02-- EVAL 后面的参数解释 "$(cat ticketID.lua)"是我们执行的lua脚本文件,-- "2"是代表传入lua脚本的参数有两个KEY,--

JAVA学习Swing章节流布局管理器简单学习

package com.swing; import java.awt.Container; import java.awt.FlowLayout; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.WindowConstants; /** * 1:流(FlowLayout)布局管理器是布局管理器中最基本的布局管理器,流布局管理器在整个容器中 * 的布局正如其名,像流一样从左到右摆放组件,直到占据了这

机器学习算法--集成学习

1. 个体和集成 集成学习通过构建并结合多个"个体学习器"来完成学习任务.个体学习器通常由一个现有的学习算法从训练数据产生,若集成中只包含同种类型的个体学习器,称为同质集成:若包含不同类型的个体学习器,为异质集成.同质集成中的个体学习器也成为"基学习器". 如何产生并结合"好而不同"的个体学习器,恰是集成学习研究的核心. 根据个体学习器的生成方式,目前的集成学习方法大致分为两大类: (1)个体学习器间存在强依赖关系,必须串行生成的序列化方法,代表

算法导论学习---红黑树具体解释之插入(C语言实现)

前面我们学习二叉搜索树的时候发如今一些情况下其高度不是非常均匀,甚至有时候会退化成一条长链,所以我们引用一些"平衡"的二叉搜索树.红黑树就是一种"平衡"的二叉搜索树,它通过在每一个结点附加颜色位和路径上的一些约束条件能够保证在最坏的情况下基本动态集合操作的时间复杂度为O(nlgn).以下会总结红黑树的性质,然后分析红黑树的插入操作,并给出一份完整代码. 先给出红黑树的结点定义: #define RED 1 #define BLACK 0 ///红黑树结点定义,与普通