SpringCloud-Snowflake-Algorithm

雪花算法

原文链接:https://blog.csdn.net/u011863024/article/details/114298270

为什么需要分布式全局唯一 ID 以及分布式 ID 的业务需求?集群高并发情况下如何保证分布式唯一全局 ID 生成?

在复杂分布式系统中,往往需婴对大量的数据和消息进行唯一标识,如在美团点评的金融、支付、餐饮、酒店,猫眼电影等产品的系统中数据日渐增长,对数据分库分表后需要有一个唯一 ID 来标识一条数据或消息。特别一点的如订单、骑手、优惠券也都雷要有唯一 ID 做标识。此时一个能够生成全局唯一 ID 的系统是非常必要的。

ID 生成规则部分硬性要求

全局唯一:不能出现重复的 ID 号,既然是唯一-标识,这是最基本的要求

趋势递增:在 MySQL 的 InnoDB 引擎中使用的是聚集索引,由于多数 RDBMS 使用 Btree 的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。

单调递增:保证下一个 ID 一定大于上一个 ID,例如事务版本号、IM 增量消息、排序等特殊需求

信息安全:如果 ID 是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可。如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,需要ID无规则不规则,让竞争对手否好猜。

含时间戳:这样就能够在开发中快速了解这个分布式id的生成时间。

ID 号生成系统的可用性要求

高可用:发一个获取分布式 ID 的请求,服务器就要保证 99.999% 的情况下给我创建一个唯一分布式 ID。

低延迟:发一个获取分布式 ID 的请求,服务器就要快,极速。

高 QPS:假如并发一口气 10 万个创建分布式 ID 请求同时杀过来,服务器要顶的住且一下子成功创建 10 万个分布式 ID。

一般通用方案

UUID

UUID(Universally Unique ldentifer) 的标准型式包含 32 个 16 进制数字,以连了号分为五段,形式为 8-4-4-4-12 的 36 个字符, 示例:550e8400-e29b-41d4-a716-446655440000

性能非常高:本地生成,没有网络消耗

如果只是考虑唯一性,那就选用它吧

但是,入数据库性能差

为什么无序的 UUID 会导致入库性能变差呢?

无序,无法预测他的生成顺序,不能生成递增有序的数字。首先分布式 ID 一般都会作为主键, 但是安装 MySQL 官方推荐主键要尽量越短越好,UUID 每一个都很长,所以不是很推荐。

主键,ID 作为主键时在特定的环境会存在一些问题。比如做 DB 主键的场景下,UUID 就非常不适用 MySQL 官方有明确的建议主键要尽量越短越好 36 个字符长度的 UUID 不符合要求。

索引,既然分布式 ID 是主键,然后主键是包含索引的,然后MySQL的索引是通过B+树来实现的,每一次新的 UUID 数据的插入,为了查询的优化,都会对索引底层的B+树进行修改,因为UUID数据是无序的,所以每一次 UUID 数据的插入都会对主键地械的B+树进行很大的修改,这一点很不好。 插入完全无序,不但会导致一-些中间节点产生分裂,也会白白创造出很多不饱和的节点,这样大大降低了数据库插入的性能。

All indexes other than the clustered index are known as secondary indexes. In InnoDB, each record in a secondary index contains the primary key columns for the row, as well as the columns specified for the secondary index. InnoDB uses this primary key value to search for the row in the clustered index.

If the primary key is long, the secondary indexes use more space, so it is advantageous to have a short primary key.

Mysql 官网解释:https://dev.mysql.com/doc/refman/8.0/en/innodb-index-types.html

数据库自增主键

单机

在单机里面,数据库的自增 ID 机制的主要原理是:数据库自增 ID 和 MySQL 数据库的 replace into 实现的。

REPLACE INTO 的含义是插入一条记录,如果表中唯一索引的值遇到冲突,则替换老数据。

这里的 replace into 跟 insert 功能类似,不同点在于:replace into 首先尝试插入数据列表中,如果发现表中已经有此行数据(根据主键或唯一索引判断)则先删除,再插入。否则直接插入新数据。

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE t_test(
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
stub CHAR(1) NOT NULL DEFAULT '',
UNIQUE KEY stub(stub)
)

SELECT * FROMt_ test;

REPLACE INTO t_test (stub) VALUES('b');

SELECT LAST_INSERT_ID();

集群分布式

那数据库自增 ID 机制适合作分布式 ID 吗?答案是不太适合

1:系统水平扩展比较困难,比如定义好了步长和机器台数之后,如果要添加机器该怎么做?假设现在只有一台机器发号是 1,2,3,4,5(步长是1),这
个时候需要扩容机器一台。可以这样做:把第二台机器的初始值设置得比第一台超过很多,貌似还好,现在想象一下如果我们线上有 100 台机器,这
个时候要扩容该怎么做?简直是噩梦,所以系统水平扩展方案复杂难以实现。

2:数据库压力还是很大,每次获取 ID 都得读写一次数据库, 非常影响性能,不符合分布式 ID 里面的延迟低和要高 QPS 的规则(在高并发下,如果都去数据库里面获取 id,那是非常影响性能的)

基于 Redis 生成全局 ID 策略

因为 Redis 是单线的天生保证原子性,可以使用原子操作INCR和INCRBY来实现

注意:在 Redis 集群情况下,同样和 MySQL 一样需要设置不同的增长步长,同时 key 一定要设置有效期可以使用 Redis 集群来获取更高的吞吐量。

假如一个集群中有 5 台 Redis。可以初始化每台 Redis 的值分别是 1,2,3,4,5,然后步长都是 5。

各个 Redis 生成的 ID 为:

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

Twitter 的分布式自增 ID 算法 snowflake

概述

Twitter 的 snowflake 解决了这种需求,最初 Twitter 把存储系统从 MySQL 迁移到 Cassandra(由 Facebook 开发一套开源分布式 NoSQL 数据库系统)。因为 Cassandra 没有顺序 ID 生成机制,所以开发了这样一套全局唯一生成服务。

Twitter 的分布式雪花算法 SnowFlake ,经测试 snowflake 每秒能够产生 26 万个自增可排序的 ID

Twitter 的 SnowFlake 生成 ID 能够按照时间有序生成。

SnowFlake 算法生成 ID 的结果是一个 64bit 大小的整数, 为一个 Long 型(转换成字符串后长度最多 19)。

分布式系统内不会产生 ID 碰撞(由 datacenter 和 workerld 作区分)并且效率较高。

分布式系统中,有一些需要使用全局唯一 ID 的场景, 生成 ID 的基本要求:

在分布式的环境下必须全局且唯一 。

一般都需要单调递增,因为一般唯一 ID 都会存到数据库,而 Innodb 的特性就是将内容存储在主键索引树上的叶子节点而且是从左往右,递增的,所以考虑到数据库性能,一般生成的 ID 也最好是单调递增。 为了防止 ID 冲突可以使用 36 位的 UUID,但是 UUID 有一些缺点, 首先他相对比较长, 另外 UUID 一般是无序的。

可能还会需要无规则,因为如果使用唯一 ID 作为订单号这种,为了不然别人知道一天的订单量是多少,就需要这个规则。

结构

雪花算法的几个核心组成部分:

号段解析:

1bit:

不用,因为二进制中最高位是符号位,1 表示负数,0 表示正数。生成的 id 一般都是用整数,所以最高位固定为 0。

41bit - 时间戳,用来记录时间戳,毫秒级:

41 位可以表示 2^41 -1 个数字。

如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 至 2^41 -1, 减 1 是因为可表示的数值范围是从 0 开始算的,而不是 1。

也就是说 41 位可以表示 2^41−1 个毫秒的值,转化成单位年则是 ( 2^41 − 1 ) / ( 1000 x 60 x 60 x 24 x 365 ) = 69 年。

10bit - 工作机器 ID,用来记录工作机器 ID:

可以部署在 2^10=1024 个节点,包括 5 位 DataCenterId 和 5 位 Workerld。

5 位(bit) 可以表示的最大正整数是 2^5 −1=31,即可以用 0、1、2、3、…31 这 32 个数字,来表示不同的 DataCenterld 或 Workerld。

12bit - 序列号,用来记录同毫秒内产生的不同 id。

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

SnowFlake 可以保证:

所有生成的 ID 按时间趋势递增。
整个分布式系统内不会产生重复 id(因为有 DataCenterId 和 Workerld 来做区分)

源码

以下代码仅供学习:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/**
* Twitter_Snowflake
* SnowFlake的结构如下(每部分用-分开):
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
* 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0
* 41位时间戳(毫秒级),注意,41位时间戳不是存储当前时间的时间戳,而是存储时间戳的差值(当前时间戳 - 开始时间戳)
* 得到的值),这里的的开始时间戳,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下面程序SnowflakeIdWorker类的startTime属性)。41位的时间戳,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
* 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId
* 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间戳)产生4096个ID序号
* 加起来刚好64位,为一个Long型。
*/
public class SnowflakeIdWorker {
/** 开始时间戳 (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();
}
/** 测试 */
public static void main(String[] args) {
System.out.println("开始:"+System.currentTimeMillis());
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
for (int i = 0; i < 50; i++) {
long id = idWorker.nextId();
System.out.println(id);
// System.out.println(Long.toBinaryString(id));
}
System.out.println("结束:"+System.currentTimeMillis());
}
}

工程落地经验

Hutool 的 Snowflake 文档:

https://www.hutool.cn/docs/#/core/%E5%B7%A5%E5%85%B7%E7%B1%BB/%E5%94%AF%E4%B8%80ID%E5%B7%A5%E5%85%B7-IdUtil?id=snowflake

添加依赖

1
2
3
4
5
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-captcha</artifactId>
<version>4.6.8</version>
</dependency>

示例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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;
@Slf4j
@Component
public class IdGeneratorSnowflake{
private long workerId = 0;
private long datacenterId = 1;
private Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId);
public synchronized long snowflakeId(){
return snowflake.nextId();
}
public synchronized long snowflakeId(long workerId, long datacenterId){
Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId);
return snowflake.nextId();
}
public static void main(String[] args){
IdGeneratorSnowflake idGenerator = new IdGeneratorSnowflake();
System.out.println(idGenerator.snowflakeId());

ExecutorService threadPool = Executors.newFixedThreadPool(5);
for (int i = 1; i <= 20; i++){
threadPool.submit(() -> {
System.out.print1n(idGenerator.snowflakeId());
});
}

threadPool.shutdown();
}
}

优缺点

优点:

毫秒数在高位,自增序列在低位,整个 ID 都是趋势递增的。

不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。

可以根据自身业务特性分配bit位,非常灵活。

缺点:

依赖机器时钟,如果机器时钟回拨,会导致重复 ID 生成。

在单机上是递增的,但是由于设计到分布式环境,每台机器上的时钟不可能完全同步,有时候会出现不是全局递增的情况。

(此缺点可以认为无所谓,一般分布式 ID 只要求趋势递增,并不会严格要求递增,90% 的需求都只要求趋势递增)

Contents
  1. 1. 雪花算法
    1. 1.1. Twitter 的分布式自增 ID 算法 snowflake
|