雪花算法

158 阅读7分钟

前言

雪花算法(SnowFlake)的背景

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

为什么需要雪花算法

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

  1. 在分布式的环境下必须全局且唯一。
  2. 一般都需要单调递增,因为一般唯一ID都会存到数据库,而innodb引擎的特性就是将内容存储在主键索引树上的叶子节点,而且是从左往右递增的,所以考虑到数据库性能,一般生成的ID也最好是单调递增。

一般通用方案

UUID(Universally Unique ldentifier)

UUID的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符,示例:550e8400-e29b-41d4-a716-446655440000。优点是生成性能非常高(本地生成,没有网络消耗),缺点是无序,入库性能差。

1678719238014.jpg

MySQL 主键自增/REPLACE INTO

优点:唯一、自增;缺点:MySQL性能并不是很高、集群情况下扩展难度大。如下:

  • 系统水平扩展比较困难,比如集群情况下,定义好步长和机器台数之后,如果要添加机器该怎么做?假设现在只有一 台机器,主键是1、2、3(此时步长为1),这个时候需要扩容机器一台。可以这样做:把第二台机器的初始主键值设置得比第一台超过很多,貌似还好,现在如果我们线上有100台机器,要扩容就非常难整,所以系统水平扩展方案复杂难以实现。
  • 数据库压力很大,每次获取ID都得读写一次数据库,非常影响性能,不符合分布式ID里面的延迟低和要高QPS的规则(在高并发下,如果都去数据库里面获取id,那是非常影响性能的)

REPLACE INTO 说明
REPLACE INTOINSERT 功能类似,区别在于REPLACE INTO首先会尝试插入数据,如果发现表中已经有此行数据(根据主键或唯一索引判断)则先删除,再插入,否则直接插入新数据。(即插入一条记录,如果表中唯一索引的值遇到冲突,则替换老数据)

REPLACE INTO SQL样例
 CREATE TABIE t_test(
    id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    stub CHAR(1) NOT NULL DEFAULT '',
    UNIQUE KEY stub (stub)
)
SELECT * FROM t_test;
REPLACE INTO t_test (stub) VALUES ('b');
SELECT LAST_INSERT_ID(); 

基于Redis生成全局ID策略

  • 单机版:因为Redis是单线的天生保证原子性,可以使用原子操作INCR和INCRBY来实现
  • 集群版:同样和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
缺点:不易扩展、维护不易(需保障其高可用性)、需要引入Redis组件,投入产出比不高。

全局ID的规则及要求

业务要求

1. 全局唯一
2. 趋势递增
在MySQL的InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用Btree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
3. 单调递增
尽量保证下一个IP一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。
4. 信息安全
如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞争对手可以直接知道我们一天的单量。所以在一些应用场景下,需要ID无规则不规则,让竞争对手不好猜。
5. 含时间戳
帮助开发者快速了解该ID的生成时间,有助于排查问题

可用性要求

  • 高可用:发一个获取分布式ID的请求,服务器就要保证 99.999% 的情况下给我创建一个唯一分布式ID
  • 低延迟:发一个获取分布式ID的请求,服务器就要快,极速
  • 高QPS:假如一口气并发10万个创建分布式ID的请求,服务器要顶的住且能快速成功创建10万个ID

雪花算法

GitHub 原Scala语言源码
GitHub 改写Java语言版本源码,存在时钟回拨问题,慎用

特性

  1. 经测试每秒能够产生26万个自增可排序的ID
  2. ID能够按照时间有序生成
  3. 生成的ID是一个64bit大小的整数,为一个Long型,转换成字符串后长度最多19位
  4. 分布式系统内不会产生ID碰撞(由datacenter和workerld作区分),并且效率较高

结构

image.png
上图号段解析如下:

  • 1bit\color{red}{1bit}:不用,因为二进制中最高位是符号位,1表示负数,0表示正数。生成的id一般都是用整数,所以最高位固定为0。
  • 41bit\color{red}{41 bit}:时间戳,用来记录时间戳,毫秒级。41位可以表示 2^41-1 个数字,如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 ~ 2^41-1,减1是因为可表示的数值范围是从0开始算的,而不是1,也就是说41位可以表示2^41-1个毫秒的值,转化成单位年,则是 (2^41 - 1) / (1000 * 60 * 60 * 24 * 365) ≈ 69年
  • 10bit\color{red}{10bit}:工作机器ID,用来记录工作机器ID。可以部署在 2^10 = 1024 个节点,包括 5位datacenterId 和 5位workerId。5位(bit)可以表示的最大正整数是2^5 - 1 = 31,即可以用0、1、2、3、……31这32个数字,来表示不同的datecenterId或workerId。
  • 12bit\color{red}{12bit}:序列号,用来记录同一毫秒内产生的不同id。12位(bit)可以表示的最大正整数是 2^12 - 1 = 4095,即可以用0、1、2、3、……4094这4095个数字来表示同一机器同一时间戳(毫秒)内产生的4095个ID序号。

以上,可保证SnowFlake:所有生成的ID按时间趋势递增;整个分布式系统内不会产生重复ID(有 datacenterId 和 workerId 来做区分)。

使用

hutool工具包

GitHub Hutool源码-雪花算法所在模块

依赖

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.15</version>
</dependency>
或
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-captcha</artifactId>
    <version>5.8.15</version>
</dependency>

以下为简易集成代码,可直接使用Hutool的IdUtil工具类

import cn.hutool.core.lang.Snowflake;
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
{
    // 范围:0 ~ 31
    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("当前机器的workerId:{}", workerId);
        } catch (Exception e) 
        {
            e.printStackTrace();
            log.warn("当前机器的workerId获取失败", e);
            workerId = NetUtil.getLocalhostStr().hashCode();
        }
    }
    
    public synchronized long snowflakeId()
    {
        return snowflake.nextId();
    }
    
    public synchronized long snowflakeId(long workerId, long datacenterId)
    {
        Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId);
        return snowflake.nextId();
    }
}

优缺点

优点

  • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
  • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
  • 可以根据自身业务特性分配bit位,非常灵活。

缺点

  • 依赖机器时钟,如果机器时钟回拨,会导致重复ID生成。
  • 在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,有时候会出现不是全局递增的情况(此缺点可以认为无所谓,一般分布式ID只要求趋势递增,并不会严格要求递增,90%的需求都只要求趋势递增)。

其他补充方案

以下方案中解决了时钟回拨问题,优化了雪花算法

  • 百度开源的分布式唯一ID生成器UidGenerator
  • Leaf—美团点评分布式ID生成系统

友情链接