分布式系统下雪花算法生成全局唯一ID

雪花算法

在分布式系统中,为了避免多个节点生成相同的唯一标识符id,我们通常使用一种全局唯一ID生成策略,而Snowflake Algorithm就是广泛使用的解决方案之一。

雪花算法简介

雪花算法生成的 ID 是一个 64 位的二进制整数,具有以下组成部分:

部分 字节长度 描述
符号位 1bit 固定为0,因为生成的ID是整数
时间戳 41bits 表示时间戳,单位是毫秒,可以存储大约68年的时间
机器ID 10bits 用于标识不同的机器(节点)。10位可以将其分数据中心ID(5位)和机器ID(5位),共支持1024台机器同时生成ID
序列号 12bits 同一毫秒内的序列号,用来区分在同一台机器、同一时间戳下生成的多个ID,最大支持同一毫秒生成4096个唯一ID

代码实现

package com.can.springbootmessage;

public class SnowflakeIdGenerator {
    // 起始时间戳,Long是64位,所以这里的Statt_Timestamp实际上有64位
    private static final long START_TIMESTAMP = 157286400000L;

    // 每部分占用的位数,符号位默认1位且位0,时间戳
    private static final long WORKER_ID_BITS = 5L;    // 机器ID所占位数
    private static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);    // 最大支持机器ID数量:31
    // 序列号占用的位数为 12 位,同一毫秒内可以生成 2^12 = 4096 个序列号。
    private static final long SEQUENCE_BITS = 12L;

    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
    private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;

    // 当前机器的唯一标识(0-31)。
    private final long workerId;
    private long sequence = 0L;
    // 上一次生成 ID 时的时间戳,用于判断是否跨毫秒。
    private long lastTimestamp = -1L;

    public SnowflakeIdGenerator(long workerId) {
        if(workerId > MAX_WORKER_ID || workerId < 0) {
            throw new IllegalArgumentException(String.format("Worker ID cantnot be greater than %d or less than 0", MAX_WORKER_ID));
        }
        this.workerId = workerId;
    }

    // 获取当前的系统时间戳(以毫秒为单位)。
    private long timeGen() {
        return System.currentTimeMillis();
    }

    // 生成下一个唯一的 ID,synchronized 关键字确保多线程环境下的线程安全。
    public synchronized long nextId() {
        long timestamp = timeGen();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("Clock moved backwards. Refusing to generate id");
        }
        // 如果当前时间戳等于上一次生成 ID 的时间戳,说明是在同一毫秒内生成 ID。
        if (timestamp == lastTimestamp) {
            // 序列号递增。通过 & 运算保证序列号不超过 4095 (2^12 - 1)。如果sequence递增到4096,通过这个 & 操作,sequence就变成0了
            sequence = (sequence + 1) & (1 << SEQUENCE_BITS - 1);
            if (sequence == 0) {
                // 如果该毫秒内,sequence增加到4096,循环等到下一毫秒生成新的 ID
                timestamp = tilNextMillis(lastTimestamp);

            } else {
                // 如果当前时间戳大于上一次生成 ID 的时间戳,说明是新的一毫秒,序列号重置为 0。
                sequence = 0L;
            }
        }
        // 更新 lastTimestamp 为当前时间戳,以便后续生成 ID 时使用。
        lastTimestamp = timestamp;
        // 生成 ID 并返回。通过位移和或操作将时间戳、机器 ID 和序列号合并成一个 64 位的 long 值。
        return ((timestamp - START_TIMESTAMP) << TIMESTAMP_LEFT_SHIFT)  // 时间戳部分
                | (workerId << WORKER_ID_SHIFT)  // 机器 ID 部分
                | sequence;  // 序列号部分
    }

    // 如果当前时间戳与上一次生成 ID 的时间戳相同,并且序列号已用尽,等待到下一毫秒。
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        // 循环,直到获取到比 lastTimestamp 大的时间戳。
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }
    
    public static void main(String[] args) {
        // example usage
        SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1);
        for (int i = 0; i < 10; i++) {
            long id = idGenerator.nextId();
            System.out.println("Generated ID:" + id);
        }
    }
}

  1. 参数初始化:
    • START_TIMESTAMP 是一个时间戳的起点,通常定义为系统的某个固定时间,所有生成的 ID 的时间戳都基于这个时间差来计算。
    • SEQUENCE_BITS + WORKER_ID_BITS 分别表示序列号 和 机器 ID 占用的位数,设计成 12 位和 5 位,这些位数的设计可以根据系统规模调整。
    • 您可以根据自己的需求和分布式环境的规模来调整 WORKER_ID_BITS 的长度,以适应不同的场景,这里只设置了5位的机器ID
  2. ID 生成逻辑:
    • 生成使用了 41 位的时间戳、5 位的机器中心 ID,以及 12 位的序列号。
    • 生成新的 ID 时,如果时间戳不变,序列号递增;如果时间戳变了,序列号清零
    • 一毫秒内的序列号已经用完(4096 个 ID),会等待到下一毫秒再继续生成
  3. 时钟回拨检测:
    • 当系统时间出现回退(即当前时间小于上次生成 ID 时的时间),为了避免生成重复 ID,直接抛出异常。

总结

  1. 雪花算法算法是用来生成全局唯一ID
  2. 核心是64位二进制数,基于时间戳的算法,
    1. 1位默认位0,41位时间用于时间戳,10位用于机器ID,12位用于序列号。
    2. 机器ID,用于标识不同的机器(节点)。10位可以将其分数据中心ID(5位)和机器ID(5位),共支持1024台机器同时生成ID
    3. 序列号用来区分在同一台机器、同一时间戳下生成的多个ID,最大支持同一毫秒生成4096个唯一ID。
  3. 生成新的ID时:
    1. 如果时间戳不变,序列号增加;
    2. 如果时间戳改变,序列号清零;
    3. 一毫秒内的序列号如果用完(4096个ID),会等待到下一毫秒再继续生成(进入循环,直到时间戳更新)

Read more

如何设计秒杀系统

什么是秒杀设计 秒杀设计 是一种针对高并发、大流量场景的系统设计,通常应用于电商活动中(如限时抢购、促销等),用户在非常短的时间内大量涌入系统,抢购有限的商品或优惠。这种场景下,系统需要能够承受巨大的瞬时并发请求,同时保证数据的一致性和业务的正确性。 秒杀技术分析 秒杀系统贯穿活动、商品和下单三个领域,涵盖了页面静态化、接口限流、Redis预减库存、异步下单和接口动态化等技术。 需要迎接的挑战有: 1. 高并发和压力测试:秒杀活动会带来巨大的流量,服务器和数据库的并发处理能力是关键。 2. 保证数据一致性:抢购涉及到商品库存的实时减少,需要保证在高并发场景下库存数据的准确性和一致性 3. 防止超卖和重复购买:确保同一商品不会被重复购买,同时避免超卖,即使是在极端的高并发情况下 4. 分布式锁和限流:使用分布式锁来保护关键资源,限制用户访问频率以免系统崩溃 5. 性能优化:包括代码层面的优化、数据库的优化、缓存的使用等,以提高系统性能和响应速度。 页面静态化 秒杀页面静态化是将动态生成的秒杀页面转换为静态HTML页面,从而提高页面响应速度和系统性能

By Yucan Huang

责任链设计模式

什么是责任链模式 项目有个请求,需要有对应的服务来处理,然后这个请求可能需要被很多个层级权限的服务来处理。我们将这些处理该请求的服务放在一条链上,链从前往后,是层级更高的服务,第一个服务处理不了,传递到链上的下一个服务,直到这个请求被处理成功。 责任链模式(Chain of Responsibility)是一种处理请求的模式,它让多个处理器都有机会处理该请求,直到其中某个处理器成功处理该请求,责任链模式把多个处理器串成链,然后让请求在链上传递。 1. 如何把多个处理器串成链,然后让请求在链上传递 客户端发送请求,处理类去处理它的请求,所以有个处理方法(handleRequest),处理类要连接在一起,所以**处理类要有一个方法(成员变量nextHandler)**指向下一个处理类。 我们抽象出一个公共的父类,然后去定义不同的处理类,这些处理类通过nextHandler连接起来。 2. 代码演示 package interview.pattern; public class ChainRespPattern { Handl

By Yucan Huang
外卖点餐系统07

外卖点餐系统07

缓存菜品 问题说明 用户通过微信小程序查询菜品,小程序会将请求发送给后端服务,后端就会查询MySQL数据库。 用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大,就会造成系统响应慢,用户体验差。 解决思路 * 通过Redis来缓存菜品数据,减少数据库查询操作。 微信小程序查询数据后,会向后端服务发送请求,判断请求的数据在缓存中是否存在,如果存在直接读取缓存,不存在再查询MySQL,并将该数据载入缓存。 * 缓存逻辑分析 微信小程序展示菜品粒度是根据分类展示,所以缓存数据应该根据分类缓存菜品。 1. 每个分类下的菜品保存一份缓存数据 * 使用分类id作为key,分类下的菜品数据使用String字符串保存,分类下的菜品应该是List集合,在Java中可以通过序列化将这个集合转换成Redis的字符串类型 2. 数据库中菜品数据有变更时及时清理缓存数据 代码开发 1. 修改用户端接口 DishContr

By Yucan Huang