分布式全局ID生成器-雪花算法
在复杂的分布式系统中全局ID生成器,通常需要满足如下需求:
1》全局唯一
2》趋势递增
3》单调递增
4》信息安全
5》含时间戳
同时需要满足高可用、低延迟、高QPS(一次生成几万个ID)
1. 一般通用方案研究
1. UUID生成
如下:
UUID.randomUUID().toString()
结果:
cfa85940-ccf6-4069-90f2-b864e72a7603
生成的是36位长度的16进制的字符串,通常的做法是将中划线-替换为空格,也就是存到数据库的是32位的字符串。这样可以解决大部分的需求,但是不满足递增。
主要的缺点是:
(1) 无序: 无法预测他的生成顺序,不能生成递增有序的数字
(2) 作为主键某些场景存在问题。比如mysql中就不适合用UUID做主键
(3) 索引:B+ Tree索引的分裂
mysql的索引通过b+树实现的,每一次插入新的数据UUID作为主键,每次都会导致索引分裂。
2. 数据库自增主键
数据库的自增ID机制主要原理:数据库自增ID和mysql 数据库的replace into 实现(插入一条记录,如果表中唯一索引值遇到冲突,则替换老数据)。
测试:
CREATE TABLE t_test ( id BIGINT UNSIGNED NOT NULL auto_increment PRIMARY KEY, stub CHAR ( 1 ) NOT NULL DEFAULT '', UNIQUE KEY key_stub ( stub ) ) select * from t_test replace into t_test(stub) values('b') replace into t_test(stub) values('b') select * from t_test select LAST_INSERT_ID()
(1) 单机版用auto_increment
(2) 集群版
集群中根据机器数量和步长然后设置自增。这样不便于扩展,比如集群增加节点,需要重新计算集群步长,或者设置起始值远远大于其他机器。
3. 基于Redis生成全局ID策略
redis 是单线程的天生保证原子性,因此可以适应incr 和 incrby 命令实现。同样是根据机器来设置步长。
其策略如下:假设五台机器
A:1-6-11-16-21
B:2-7-12-17-22
C:3-8-13-18-23
D:4-9-14-19-24
E:5-10-15-20-25
127.0.0.1:6379> keys * (empty list or set) 127.0.0.1:6379> incr keya (integer) 1 127.0.0.1:6379> incrby keya 5 (integer) 6 127.0.0.1:6379> incrby keya 5 (integer) 11
唯一的缺点是维护麻烦,配置麻烦。因此引入了下面的雪花算法。
2. 雪花算法
Twitter 的分布式自增ID算法snowflake ,经过测试,snowflake 每秒钟能够产生26万个自增可排序的ID。
(1) 生成ID能够按照时间有序生成
(2) 生成的结果是一个64bit 大小的整数,为一个Long 型(转换成字符串长度最多19)
(3) 分布式环境不会产生碰撞(由datacenter 和 workId 做区分) 并且效率较高。
雪花算法的几个核心组成部分:
1bit: 不用,二进制中最高位表示符号位,1表示负数,0表示正数。生产的ID一般是正数,所以符号位一般都是0。
41bit 用来记录时间戳,用来记录时间戳,毫秒级。
10 bit -工作机器ID。 用来记录工作的机器ID。可以部署在1024个机器上,包括5位datacenter 和 5位 workId。 5bit 表示的最大整数是 31(2^{5}-1)。也就是可以用0-31 表示datacenter 和 workid。
12bit-系列号。用来记录同毫秒内产生的不同ID。12 bit 表示的最大整数是 (2^{12}-1) 4095。
git 地址: https://github.com/twitter-archive/snowflake
1. 测试如下:
package com.xm.ggn.utils; /** * Twitter_Snowflake<br> * SnowFlake的结构如下(每部分用-分开):<br> * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - * 000000000000 <br> * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br> * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截) * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。 * 41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br> * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br> * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br> * 加起来刚好64位,为一个Long型。<br> * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高, * 经测试,SnowFlake每秒能够产生26万ID左右。 */ public class SnowflakeIdWorker { // ==============================Fields=========================================== /** 开始时间截 (2015-01-01) */ private final long twepoch = 1420041600000L; /** 机器id所占的位数 */ private final long workerIdBits = 5L; /** 数据标识id所占的位数 */ private final long datacenterIdBits = 5L; /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */ private final long maxWorkerId = -1L ^ (-1L << workerIdBits); /** 支持的最大数据标识id,结果是31 */ private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); /** 序列在id中占的位数 */ private final long sequenceBits = 12L; /** 机器ID向左移12位 */ private final long workerIdShift = sequenceBits; /** 数据标识id向左移17位(12+5) */ private final long datacenterIdShift = sequenceBits + workerIdBits; /** 时间截向左移22位(5+5+12) */ private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */ private final long sequenceMask = -1L ^ (-1L << sequenceBits); /** 工作机器ID(0~31) */ private long workerId; /** 数据中心ID(0~31) */ private long datacenterId; /** 毫秒内序列(0~4095) */ private long sequence = 0L; /** 上次生成ID的时间截 */ private long lastTimestamp = -1L; // ==============================Constructors===================================== /** * 构造函数 * * @param workerId * 工作ID (0~31) * @param datacenterId * 数据中心ID (0~31) */ public SnowflakeIdWorker(long workerId, long datacenterId) { if (workerId > maxWorkerId || workerId < 0) { throw new IllegalArgumentException( String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); } if (datacenterId > maxDatacenterId || datacenterId < 0) { throw new IllegalArgumentException( String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId)); } this.workerId = workerId; this.datacenterId = datacenterId; } // ==============================Methods========================================== /** * 获得下一个ID (该方法是线程安全的) * * @return SnowflakeId */ public synchronized long nextId() { long timestamp = timeGen(); // 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常 if (timestamp < lastTimestamp) { throw new RuntimeException(String.format( "Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } // 如果是同一时间生成的,则进行毫秒内序列 if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; // 毫秒内序列溢出 if (sequence == 0) { // 阻塞到下一个毫秒,获得新的时间戳 timestamp = tilNextMillis(lastTimestamp); } } // 时间戳改变,毫秒内序列重置 else { sequence = 0L; } // 上次生成ID的时间截 lastTimestamp = timestamp; // 移位并通过或运算拼到一起组成64位的ID return ((timestamp - twepoch) << timestampLeftShift) // | (datacenterId << datacenterIdShift) // | (workerId << workerIdShift) // | sequence; } /** * 阻塞到下一个毫秒,直到获得新的时间戳 * * @param lastTimestamp * 上次生成ID的时间截 * @return 当前时间戳 */ protected long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } /** * 返回以毫秒为单位的当前时间 * * @return 当前时间(毫秒) */ protected long timeGen() { return System.currentTimeMillis(); } // ==============================Test============================================= /** 测试 */ public static void main(String[] args) { SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0); for (int i = 0; i < 1000; i++) { long id = idWorker.nextId(); System.out.println(Long.toBinaryString(id)); System.out.println(id); } } }
2. Springboot 中使用雪花算法
糊涂工具包整合了一些通用的工具类,包括雪花算法、加密、缓存等工具类。因此使用糊涂包来进行生成ID。
糊涂工具包 git: https://github.com/dromara/hutool
API 地址: https://hutool.cn/
1. 工具引入POM
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.6.6</version> </dependency>
2. 对hutu工具进行包装
package com.xm.ggn.utils; import cn.hutool.core.lang.Snowflake; import cn.hutool.core.net.NetUtil; import cn.hutool.core.util.IdUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; /** * @Author: qlq * @Description * @Date: 18:46 2021/5/30 */ @Slf4j @Component public class IdGenerator { private long workerId = 0; private long datacenterId = 1; private Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId); @PostConstruct public void init() { try { workerId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr()); log.info("当前机器的IP: {}, workerId: {}", NetUtil.getLocalhostStr(), workerId); } catch (Exception e) { log.error("获取当前机器workerId 异常", e); workerId = NetUtil.getLocalhostStr().hashCode(); } } /** * 使用默认的workId 和 datacenter * * @return */ public synchronized long snowflakeId() { return snowflake.nextId(); } /** * 使用自定义的workerId 和 datacenter * * @param workerId * @param datacenterId * @return */ public synchronized long snowflakeId(long workerId, long datacenterId) { return IdUtil.createSnowflake(workerId, datacenterId).nextId(); } public static void main(String[] args) { System.out.println(new IdGenerator().snowflakeId(1, 1)); } }
3. 其他类中引用
@Autowired private IdGenerator idGenerator; @GetMapping("snowflakeIdTest") public Long snowflakeIdTest() { return idGenerator.snowflakeId(); }
4. 总结:
优点:
毫秒数在高位,自增序列在地位,整合ID都是趋势递增的
不依赖于第三方中间件,稳定性高,生成的ID性能也高
可以根据业务自身需求分配bit 位,非常灵活
缺点:
依赖机器时钟,如果机器时钟回拨,会导致重复ID生成。可以使用百度开源的UidGenerator 或者 美团分布式ID生成系统 Leaf。
3. 原理
关于位移:
package qz.operation; /** * java 位运算 */ public class JavaWeiCompute { public static void main(String[] args) { /** * 1. int类型32 个字节。 最高位第31 位是符号位。 为0代表正,为1代表负数 * 范围是: -2^31 到 +2^31 -1。 因为0 划到非负区域,占用了正数的一个位置 * 2. << 左移操作,实际是将二进制位整体左移,右边的补零。可以理解位左移几位就是乘以几个2 * >> 右移操作,二进制位整体右移,左边补领,可以理解位移动几位就是除以几个2 * 3. 1 向左边移动几位就是二进制位对应位置只有1,其他位置都是0 * 4. 负数的计算法则是,将所有位置取反、然后加一,计算值 * 比如: 1......111.01, 计算如下。 * 1>所有位置取反,然后得到的是0......10 * 2>二进制加一就是0......11 * 2>计算二进制的值是3,那么值就是-3 * 5.位运算 * &:按位与运算,位置都是1才是1 * |:按位或运算,只要有1就是1 * ^:异或运算,相同为0,不同为1 * ~:取反运算 */ print(Integer.MIN_VALUE); print(-3); print(3); print(4); System.out.println("位运算"); print(~(-3)); print(3 & 4); print(3 | 4); print(3 ^ 4); } public static void print(int num) { System.out.println("num: " + num); for (int i = 31; i >= 0; i--) { System.out.print((num & (1 << i)) == 0 ? "0" : "1"); } System.out.println(); } }
结果:
num: -2147483648 10000000000000000000000000000000 num: -3 11111111111111111111111111111101 num: 3 00000000000000000000000000000011 num: 4 00000000000000000000000000000100 位运算 num: 2 00000000000000000000000000000010 num: 0 00000000000000000000000000000000 num: 7 00000000000000000000000000000111 num: 7 00000000000000000000000000000111
2、雪花算法生成
1》位数分析Long 型总共64位; 1位符号位(0正1负)、剩下63位可分配
TIMESTAMP_LEFT_SHIFT: 22, 意味着时间占用41位 DATA_CENTER_ID_SHIFT: 17, 意味着占5位 WORKER_ID_SHIFT: 12, 意味着占5位 那么剩下的最后12位是序列位。 总分布:
1位符号位|41位时间戳|5位数据中心|5位工作中心|12位序列位
2》合并原理
1、生成时间戳, 然后右移22位,右边的22位为0。 1位符号位|41位时间戳|(22个0) 2、数据中心5位 1位符号位|(41个0)|5位数据中心|(17个0) 3、工作ID5位 1位符号位|(46个0)|5位工作ID|(12个0) 4、12位序列号 1位符号位|(51个0)|(12个0) 上面生成的4个数进行按位异或运算(有1就是1), 这样形成的就是64位分布的完整的一个ID。