更多内容,前往IT-BLOG

一、前言


  如何在分布式集群中生产全局唯一的 ID?
【方案一】
UUID:UUID是通用唯一识别码 (Universally Unique Identifier),在其他语言中也叫GUID,可以生成一个长度 32位的全局唯一识别码。UUID 虽然可以保证全局唯一,但是占32位而且无序,入库性能比较差

1 //例如:44e128a5-ac7a-4c9a-be4c-224b6bf81b20
2 String uuid = UUID.randomUUID().toString()

举个例子:MySQL 使用 InnoDB 存储引擎,底层索引使用的B+树索引。索引是按照顺序在叶子节点分布的,如果按顺序插入索引,则会使用顺序IO依次写入磁盘,也不会出现页分裂和浪费等问题,性能也比较高。但如果使用无序的 UUID,就会导致磁盘臂不按顺序移动,且会导致页分裂和不饱和的节点导致数据库插入性能降低。
方案二数据库自增主键:在分布式系统中可以用DB proxy请求不同的分库,每个分库设置不同的初始值,步长和分库数量相等。如下:DB1生成的ID是1,4,7,10,13....,DB2生成的ID是2,5,8,11,14.....

这样也不是很好,ID 的生成对数据库严重依赖,不但影响性能,而且一旦数据库挂掉,服务将变的不可用。

二、SnowFlake


SnowFlake 是 Twitter最初把存储系统从 MySQL迁移到 Cassandra时,因为 Cassandra没有顺序ID生成机制,所以开发了这样一套开源的分布式全局唯一的 ID生成算法,结果是一个 long型的ID。这种方案把 64-bit分别划分成多段,分开来标识机器ID、时间等。其核心思想是:使用 41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号,最后还有一个符号位,永远是0。SnowFlake  的结构图如下所示:

【1】第一位:1位标识占用1bit,其值始终是0,表示符号位,正数是0,负数是1,id一般是正数,在这里没有实际作用。
【2】第二位:时间戳占用41bit,精确到毫秒,总共可以容纳约69 年的时间。
【3】第三位:工作机器id占用10bit,其中高位5bit是数据中心ID(datacenterId),低位5bit是工作节点ID(workerId),做多可以容纳1024个节点。10-bit机器可以分别表示1024台机器。如果我们对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,可以根据自身需求定义。
【4】第四位:序列号占用12bit,这个值在同一毫秒同一节点上从0开始不断累加,最多可以累加到4095。12个自增序列号可以表示2^12个ID。
SnowFlake算法在同一毫秒内最多可以生成多少个全局唯一ID呢?只需要做一个简单的乘法:同一毫秒的ID数量 = 1024 X 4096 = 4194304(QPS)这个数字在绝大多数并发场景下都是够用的。

  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 }

三、SnowFlake的优势和劣势


【SnowFlake算法的优点】:整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
【1】生成ID时不依赖于DB,完全在内存生成,高性能高可用;
【2】ID呈趋势递增,后续插入索引树的时候性能较好;
SnowFlake算法的缺点】:依赖于系统时钟的一致性。如果某台机器的系统时钟回拨,有可能造成ID冲突,或者ID乱序。针对此,美团做出了改进:链接

雪花算法 SnowFlake 内部结构【分布式ID生成策略】的更多相关文章

  1. 分布式ID生成策略 · fossi

    分布式环境下如何保证ID的不重复呢?一般我们可能会想到用UUID来实现嘛.但是UUID一般可以获取当前时间的毫秒数再加点随机数,但是在高并发下仍然可能重复.最重要的是,如果我要用这种UUID来生成分表 ...

  2. 图解Janusgraph系列-分布式id生成策略分析

    JanusGraph - 分布式id的生成策略 大家好,我是洋仔,JanusGraph图解系列文章,实时更新~ 本次更新时间:2020-9-1 文章为作者跟踪源码和查看官方文档整理,如有任何问题,请联 ...

  3. java 雪花算法实现获取分布式id

    import java.lang.management.ManagementFactory; import java.net.InetAddress; import java.net.NetworkI ...

  4. 分布式ID生成策略之ZK

    import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.CuratorFra ...

  5. 分布式ID生成策略

    策略一.UUID 策略二.数据库自增序列 策略三.snowflake算法 策略四.基于redis自增 思路:利用增长计数API,业务系统在自增长的基础上,配合其他信息组装成为一个唯一ID. redis ...

  6. Disruptor分布式id生成策略

    需要的pom文件: <!-- 顺序UUID --> <dependency> <groupId>com.fasterxml.uuid</groupId> ...

  7. 业务ID 生成策略

    业务ID 生成策略,从技术上说,基本要借助一个集中式的引擎来帮忙实现. 为了扩大业务ID生成策略的并发问题,还有更为技巧性的提升. 先来介绍普遍的分布式ID生成策略: 1. 利用DB的自增主键 这里又 ...

  8. 分布式ID生成系统 UUID与雪花(snowflake)算法

    Leaf——美团点评分布式ID生成系统 -https://tech.meituan.com/MT_Leaf.html 网游服务器中的GUID(唯一标识码)实现-基于snowflake算法-云栖社区-阿 ...

  9. 一秒可生成500万ID的分布式自增ID算法—雪花算法 (Snowflake,Delphi 版)

    概述 分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的. 有些时候我们希望能使用一种 ...

  10. 分布式唯一ID生成方案选型!详细解析雪花算法Snowflake

    分布式唯一ID 使用RocketMQ时,需要使用到分布式唯一ID 消息可能会发生重复,所以要在消费端做幂等性,为了达到业务的幂等性,生产者必须要有一个唯一ID, 需要满足以下条件: 同一业务场景要全局 ...

随机推荐

  1. 使用C++进行冒泡排序

    #include "pch.h" #include <iostream> using namespace std;手动输入10个数,进行冒泡排序 int main() ...

  2. bzoj 3309

    奇怪的莫比乌斯反演... 题意:定义$f(n)$表示将$n$质因数分解后质因子的最高幂次,求$\sum_{i=1}^{a}\sum_{j=1}^{b}f(gcd(i,j))$ 首先肯定是反演嘛... ...

  3. 参与一个组织npm包的发布

    1.github桌面提交新建一个fork 2.github网站打开fork,new 一个pull request 3.管理员审核成功 npm npm login npm publish --acces ...

  4. vscode php 方法无法跳转问题

    安装扩展 PHP IntelliSense #路径感知 PHP Intelephense  #自动补全 PHP DocBlocker #在类或属性.方法上面敲/**回车自动生成参数注释等 PHP Na ...

  5. Java断言语句

    ​ 断言语句在调试代码阶段非常有用,断言语句一般用于 程序不准备通过捕获异常来处理的错误,例如,当发生某个错误时,要求程序必须立即停止执行.在调试代码阶段让断言语句发挥作用,这样就可以发现一些致命的错 ...

  6. PDF转置

    import pdfplumber #全是None with pdfplumber.open(r"C:\Users\yjiang3\Desktop\Inv.pdf") as pdf ...

  7. java调用c++的几种方式

    jni 类似c#调用c++的方式,定义java端的c++代码接口. package crayon.jni; public class JNITest { public native static vo ...

  8. Spring Boot统一日志框架

    一.日志框架的选择 市面上常见的日志框架有很多,它们可以被分为两类:日志门面(日志抽象层)和日志实现,如下表.  日志分类 描述 举例 日志门面(日志抽象层) 为 Java 日志访问提供一套标准和规范 ...

  9. Linux让部署在服务器上的项目一直保持运行状态…&跑多个项目

    在idea通过package得到的.jar包或者.war包可通过 java -jar xxx.jar/xxx.war 命令直接在linux或者windows系统运行:   将打好包的项目放在linux ...

  10. NOI 顺序查找——查找特定的值

    描述 在一个序列(下标从1开始)中查找一个给定的值,输出第一次出现的位置. 输入 第一行包含一个正整数n,表示序列中元素个数.1 <= n <= 10000.第二行包含n个整数,依次给出序 ...