雪花算法
原文链接: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 | CREATE TABLE t_test( |
集群分布式
那数据库自增 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 | /** |
工程落地经验
Hutool 的 Snowflake 文档:
添加依赖
1 | <dependency> |
示例程序:
1 | import cn.hutool.core.lang.Snowflake; |
优缺点
优点:
毫秒数在高位,自增序列在低位,整个 ID 都是趋势递增的。
不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
可以根据自身业务特性分配bit位,非常灵活。
缺点:
依赖机器时钟,如果机器时钟回拨,会导致重复 ID 生成。
在单机上是递增的,但是由于设计到分布式环境,每台机器上的时钟不可能完全同步,有时候会出现不是全局递增的情况。
(此缺点可以认为无所谓,一般分布式 ID 只要求趋势递增,并不会严格要求递增,90% 的需求都只要求趋势递增)