java中使用雪花算法(Snowflake)为分布式系统生成全局唯一ID

(全局唯一ID的解决方案有很多种,这里主要是介绍和学习Snowflake算法)

什么是雪花算法(Snowflake)

雪花算法 (Snowflake Algorithm)是由Twitter公司在2010年左右提出的一种分布式ID生成算法,主要用于生成全局唯一且趋势递增的ID。这种算法生成的ID是一个64位的长整型数字,具有很高的性能与扩展性,特别适合于分布式环境下的主键生成场景,比如数据库表主键、消息队列的Message ID等。

实现原理

Snowflake算法的原理主要体现在它生成64位ID的结构上,主要划分为如下几个部分:

0 | 00000000000000000000000000000000000000000 | 00000 | 00000 | 000000000000

  • 1bit-符号位:

第1位通常固定为0,表示生成的ID都是正数。

  • 41bit-时间戳部分:

从第2位到第42位(共41位)存储时间戳信息,精确到毫秒级别。时间戳可以是自定义的一个起始时间点(如Twitter使用的是2010-11-04的某一时刻),这样可以通过比较ID中的时间戳部分来判断事件发生的先后顺序。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69。

  • 10bit-工作机器ID(5bit数据中心ID+5bit机器ID):

从第43位到第52位(共10位)存储工作机器ID或者数据中心ID。这部分可以进一步细分为两部分,例如前5位标识数据中心ID,后5位标识工作节点ID。这样可以支持32(0~31)个数据中心以及每个数据中心内部的32(0~31)个工作节点,足够覆盖大规模分布式系统的节点标识。

  • 12bit-序列号部分:

从第53位到第64位(共12位)存储同一节点同一毫秒内生成的序列号,这意味着同一个节点在同毫秒内可以生成最多4096个不同的ID(2^12)。

当生成ID时,首先获取当前时间戳,然后加上工作节点ID以及序列号。如果在同一毫秒内有新的请求,则序列号加1。若序列号达到最大值,则等待下一毫秒再进行分配,从而确保在同一节点内生成的ID是唯一的

雪花算法的优缺点

优点:

  1. 全局唯一性:雪花算法生成的ID是全局唯一的,这在分布式系统中非常重要,可以避免因ID冲突而导致的数据不一致问题。

  2. 递增有序:由于ID中包含时间戳部分,所以生成的ID是递增有序的。这有助于数据库插入性能的优化,因为有序的ID可以减少数据库的页分裂,提高写入效率。

  3. 灵活性:雪花算法允许自定义配置工作机器ID和数据中心ID的位数,可以根据实际部署环境调整这些配置,以支持不同规模的分布式系统。

  4. 高效性:算法本身实现简单,生成ID的速度快,能够满足高并发场景下的需求。

缺点:

  1. 时钟依赖:雪花算法依赖于系统时钟来生成时间戳部分。如果系统时钟出现回拨或漂移,可能会导致生成的ID不唯一或有序性受到破坏。虽然可以通过一些机制来处理时钟回拨问题,但时钟漂移仍然是一个潜在的风险。

  2. 机器ID冲突:如果部署的工作节点数量超过了算法中定义的机器ID位数所能表示的范围,就会发生机器ID冲突。这需要在设计系统时预先规划好机器ID的分配和管理。

  3. 缺乏安全性:雪花算法生成的ID本身并不包含加密或签名信息,因此容易受到恶意篡改。如果ID的安全性要求较高,需要在生成ID后添加额外的加密或签名措施。

  4. 扩展性限制:由于雪花算法的ID结构是固定的,因此在某些情况下可能会受到扩展性的限制。例如,如果未来需要添加更多的元数据到ID中,或者需要支持更大的分布式系统规模,可能需要重新设计ID生成算法。

因此,为了更全面地解决雪花算法的缺陷问题,可能需要采取额外的措施,例如:

  • 增强时钟同步:使用NTP(Network Time Protocol)或其他时钟同步机制来确保各个节点之间的时钟尽可能准确同步。

  • 增加机器ID的灵活性:设计一种更灵活的方式来分配和管理机器ID,以便支持更多的工作节点和数据中心。

  • 安全性考虑:对生成的ID进行加密或签名,以防止恶意篡改。

综上所述,雪花算法在分布式系统中具有广泛的应用价值,其全局唯一性和递增有序性使得它成为生成唯一ID的优选方案之一。然而,在使用雪花算法时也需要注意其潜在的缺点,并根据实际需求进行配置和优化。

Snowflake算法生成ID的Java代码示例

以下是Snowflake算法的一个java简化版实现:

java 复制代码
public class SnowflakeIdWorker {  
    // 起始的时间戳(自定义,例如系统上线时间)  
    private final long twepoch = 1288834974657L;  
  
    // 机器id所占的位数  
    private final long workerIdBits = 5L;  
  
    // 数据标识id所占的位数  
    private final long datacenterIdBits = 5L;  
  
    // 最大机器ID  
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);  
  
    // 最大数据标识ID  
    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=4095)  
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);  
  
    // 上次生成ID的时间截  
    private long lastTimestamp = -1L;  
  
    // 序列号  
    private long sequence = 0L;  
  
    // 工作机器ID  
    private final long workerId;  
  
    // 数据中心ID  
    private final long datacenterId;  
  
    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;  
    }    
  
    // 生成ID  
    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 {  
            // 时间戳改变,序列号重置为0  
            sequence = 0L;  
        }  
  
        // 更新最后的时间戳  
        lastTimestamp = timestamp;  
  
        // 移位并通过或运算拼到一起组成64位的ID  
        return ((timestamp - twepoch) << timestampLeftShift) |  
               (datacenterId << datacenterIdShift) |  
               (workerId << workerIdShift) |  
               sequence;  
    }  
  
    // 获取当前时间戳  
    protected long timeGen() {  
        return System.currentTimeMillis();  
    }  
  
    // 等待下一个毫秒  
    protected long tilNextMillis(long lastTimestamp) {  
        long timestamp = timeGen();  
        while (timestamp <= lastTimestamp) {  
            timestamp = timeGen();  
        }  
        return timestamp;  
    }

    public static void main(String[] args) {  
        SnowflakeIdWorker idWorker = new SnowflakeIdWorker(1, 1);  
        for (int i = 0; i < 5; i++) {  
            long id = idWorker.nextId();  
            System.out.println(Long.toBinaryString(id));  
            System.out.println(id);  
        }  
    }  
}

代码输出:

这段代码实现了雪花算法的核心逻辑。在nextId()方法中,它首先获取当前时间戳,然后检查时间戳是否小于上一次生成ID时的时间戳,如果是,则抛出异常,因为这意味着系统时钟回退,可能会导致ID生成出现混乱。如果时间戳相同,则序列号自增,并检查是否溢出,如果溢出则等待下一个毫秒。如果时间戳不同,则重置序列号。最后,将时间戳、数据中心ID、机器ID和序列号按照各自的偏移量左移,然后进行位或运算,组合成一个64位的ID。

(注:关于数据中心ID、机器ID,根据实际情况来进行配置。)

相关推荐
2401_857439691 小时前
SSM 架构下 Vue 电脑测评系统:为电脑性能评估赋能
开发语言·php
SoraLuna1 小时前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
xlsw_2 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹2 小时前
基于java的改良版超级玛丽小游戏
java
Dream_Snowar3 小时前
速通Python 第三节
开发语言·python
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭3 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫3 小时前
泛型(2)
java
超爱吃士力架3 小时前
邀请逻辑
java·linux·后端
南宫生3 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石3 小时前
12/21java基础
java