SnowFlake 雪花算法 生成分布式唯一ID

背景

公司近期需要重构已有项目,打算重新从架构开始底层搭建,所采用的是市面上最流行的分布式架构搭建,但是业务中有非常之多的数据需要在整个系统中都保持唯一标识,那么问题就来了,分布式项目中id的生成是采用数据库Mysql的唯一自增主键呢?又或者是采用UUID的方式呢?


这两种方法弊端都很明显:

  • 【Mysql自增主键生成唯一id】生成的方式是调用了数据库,网络I/O开销和数据库开销非常大。
  • 【UUID生成唯一id】性能倒是非常之高,但是所有的数据都得入库呀,但UUID的生成方式又是无序的,就拿Mysql来说,新增性能非常差。

有没有更好的可以生成分布式唯一ID的方式呢?


对的,相信很多人都知道,twitter家的雪花算法生成唯一id(现在百度的UidGenerator…都是基于雪花算法的),虽然听过,但是很多时候很多业务,真的就是不接触就不了解,又或者是很多时候你在用的都快用烂的一些玩意儿,就是没有深入的看过原理。那就好好的了解一下这个到底是怎么样生成的吧。

JAVA 源码

/**
 * 雪花算法生成分布式id
 */
public class SnowUtil {

    private long sequence = 0L;// 序列号
    private long machineId = 1L;// 自定义 节点
    private static long lastTimestamp = -1L;// 上次时间
    private final static long START_TIME_STAMP = 1698372440174L;// 开始时间戳 2023-10-27

    /**
     * 每一部分占用的位数
     */
    private final static long SEQUENCE_BIT = 11L;
    private final static long MACHINE_BIT = 10L;

    /**
     * 每一部分的最大值
     */
    // 1024/节点
    private static long MACHINE_MAX = -1L ^ (-1L << MACHINE_BIT);
    // 2048/序号
    private final static long SEQUENCE_MAX = -1L ^ (-1L << SEQUENCE_BIT);

    /**
     * 每一部分向左的位移
     */
    // 11位
    private final static long MACHINE_ID_LEFT = SEQUENCE_BIT;
    // 22位
    private final static long TIME_STAMP_LEFT = SEQUENCE_BIT + MACHINE_BIT;

    private static SnowUtil snowUtil = null;

    static {
        snowUtil = new SnowUtil();
    }

    /**
     * 生成唯一id
     * @return
     */
    public static synchronized long nextId() {
        return snowUtil.getNextId();
    }


    private SnowUtil() {
        this.machineId = machineId & MACHINE_MAX;
    }

    public synchronized long getNextId() {
        long timestamp = getCurrentTimeStamp();
        if (timestamp < lastTimestamp) {
            try {
                throw new Exception("");
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & SEQUENCE_MAX;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }
        lastTimestamp = timestamp;
        long nextId = ((timestamp - START_TIME_STAMP) << TIME_STAMP_LEFT)  | (machineId << MACHINE_ID_LEFT) | sequence;
        return nextId;
    }

    /**
     * 再次获取时间戳直到获取的时间戳与现有的不同
     *
     * @param lastTimestamp
     * @return 下一个时间戳
     */
    private long tilNextMillis(final long lastTimestamp) {
        long timestamp = this.getCurrentTimeStamp();
        while (timestamp <= lastTimestamp) {
            timestamp = this.getCurrentTimeStamp();
        }
        return timestamp;
    }

    /**
     * 返回以毫秒为单位的当前时间
     */
    protected long getCurrentTimeStamp() {
        return System.currentTimeMillis();
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for(long i = 0 ; i < 500000L ;  i ++){
            long nextId = SnowUtil.nextId();
        }
        long end = System.currentTimeMillis();
        Long cost = end-start;
        System.out.println("生成50wid总共花费了" + cost.toString() + "毫秒");
    }

}

性能测试

可能是因为在本地的缘故,同事用C#同一套思想(同步方法不同,也不是很清楚是不是这个问题),但是C#是运行在Windows平台直接用PowerShell运行速度非常快。

C#的性能测试

image.png

java本地环境IDEA运行的测试数据

image.png

虽然如此,但是生成id的速度还是让人瞠目结舌。

代码中注意的几个点

1、建议采用单例模式

  • 防止重复创建对象,影响性能
private static SnowUtil snowUtil = null;

static {
    snowUtil = new SnowUtil();
}

2、同步代码块

  • 防止序列号自增产生的并发问题
public synchronized long getNextId() {}

3、需要修改开始的时间戳

因为时间的位数只有1-42 (41位),总共能生成的id只有 (0,2^42-1) (139年)。

  • 为了这个生成id的方法可以多用几年,一定要修改开始的时间戳。
private final static long START_TIME_STAMP = 1698372440174L;// 开始时间戳 2023-10-27

原理

Id的数据类型采用Long数据类型,8byte=64bit。

地址(高到低)位数用途存储数值范围
0-11符号位0
1-4242时间差(0,2^42-1) (139年)
42-5210节点编号(0,2^10-1) (1024节点)
52-6411序列数(0,2^11-1)(2048/ms)

也可以根据自己的实际需求自定义适合自己实际状况的id生成器。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
雪花算法是种生成分布式ID算法,它可以生成一个64位的ID,其中包含了时间戳、数据中心ID和机器ID等信息。下面是雪花算法生成分布式ID的软件设计模型: 1. 定义一个Snowflake类,该类包含以下属性: - datacenter_id: 数据中心ID,占5位,取值围为0~31。 - worker_id: 机器ID,占5位,取值范围为0~31。 - sequence: 序列号,占12位,取值范围为0~4095。 - last_timestamp: 上一次生成ID的时间戳。 2. 实现Snowflake类的构造函数,初始化datacenter_id和worker_id属性。 3. 实现一个next_id方法,该方法用于生成下一个ID。具体实现如下: - 获取当前时间戳,单位为毫秒。 - 如果当前时间戳小于上一次生成ID的时间戳,则说明系统时钟回退过,抛出异常。 - 如果当前时间戳等于上一次生成ID的时间戳,则将序列号加1。 - 如果当前时间戳大于上一次生成ID的时间戳,则将序列号重置为0,并将last_timestamp属性更新为当前时间戳。 - 将datacenter_id、worker_id、时间戳和序列号按照一定的位数组合成一个64位的ID。 - 返回生成ID。 4. 在分布式系统中,每个节点都需要创建一个Snowflake实例,并指定不同的datacenter_id和worker_id。每个节点生成ID都是唯一的,且具有时间顺序。 下面是一个Python实现的雪花算法生成分布式ID的代码示例: ```python import time class Snowflake: def __init__(self, datacenter_id, worker_id): self.datacenter_id = datacenter_id self.worker_id = worker_id self.sequence = 0 self.last_timestamp = -1 def next_id(self): timestamp = int(time.time() * 1000) if timestamp < self.last_timestamp: raise Exception("Clock moved backwards. Refusing to generate id") if timestamp == self.last_timestamp: self.sequence = (self.sequence + 1) & 4095 if self.sequence == 0: timestamp = self.wait_next_millis(self.last_timestamp) else: self.sequence = 0 self.last_timestamp = timestamp return ((timestamp - 1288834974657) << 22) | (self.datacenter_id << 17) | (self.worker_id << 12) | self.sequence def wait_next_millis(self, last_timestamp): timestamp = int(time.time() * 1000) while timestamp <= last_timestamp: timestamp = int(time.time() * 1000) return timestamp ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值