分布式ID之雪花算法

分布式ID

分布式ID:distributed id,在分布式系统中生成的全局唯一标识符。

使用场景:订单号、分库分表环境下的数据库主键等

分布式ID常见的实现方式:

  • UUID:例如,UUID.randomUUID().toString(),优点是本地生成,无网络开销,缺点是生成的id太大,导致存储空间大,查询效率低,不适合作为数据库主键
  • 号段模式:每次获取一个号段,然后在内存中分配这个号段,分配完成后再次从数据库中获取。 优点是可以减少数据库的访问
  • 雪花算法:snow flake algorithm,由推特开源的分布式ID生成算法,生成一个64位的ID,它的结构是 1位符号位 + 41位时间戳 + 10位机器ID + 12位序列号。它的优点是本地生成、性能高、趋势递增,缺点是依赖机器时钟,如果时钟回拨,可能会生成重复的ID
  • 基于redis的原子操作:基于redis提供的操作,incr、incrby,来生成分布式id,优点是性能较好,id可以保证全局递增,缺点是每次生成都依赖redis

在实际生产中,雪花算法估计是使用最多的,虽然它依赖机器时钟,但生产环境下通常不会出现时钟不准、时钟回拨等情况,推荐使用ntp服务来维护机器时间

雪花算法

雪花算法的原理:

1、雪花算法使用41比特位来存储时间戳,使用如下案例来计算,41比特位可以存储的时间戳,在约80年左右后才会满

java 复制代码
    @Test
    public void test1() {
        long limit = (1L << 42) - 1;
        long sub = limit - System.currentTimeMillis(); // 当前2025年
        System.out.println("支持" + (sub / 1000 / 60 / 60 / 24 / 365) + "年"); // 83
    }

2、雪花算法使用10比特位来存储机器id,这支持1024个机器id,通常,单个服务最多不会部署超过100台机器,尤其是在分布式、微服务的背景下,所以1024个机器预计完全够用。

3、雪花算法使用12比特位序列号,也就是说,1毫秒内,支持生成4096个序列号,在单台机器上,预计完全够用。

雪花算法的实现

java 复制代码
/**
 * 使用雪花算法生成分布式id,线程安全
 */
public class SnowFlakeIdGenerator implements IdGenerator {
    /**
     * 时间戳的位数,二进制的
     */
    private static final int TS_BIT_SIZE = 41;
    /**
     * 机器id的位数
     */
    private static final int WORKER_BIT_SIZE = 10;
    /**
     * 序列号的位数
     */
    private static final int SEQUENCE_BIT_SIZE = 12;
    /**
     * workId的最大值
     */
    private static final long MAX_WORK_ID = (1L << WORKER_BIT_SIZE) - 1;
    /**
     * 序列号的最大值
     */
    private static final int MAX_SEQUENCE_ID = (1 << SEQUENCE_BIT_SIZE) - 1;

    /**
     * 基础时间,用于减少分布式ID的大小
     */
    private static final long BASE_TIME = getBaseTime();

    /**
     * 机器id
     */
    private final long workId;
    /**
     * 序列号,在同一毫秒内递增
     */
    private int sequence;

    /**
     * 上一次生成分布式ID时的时间戳
     */
    private long lastTime;

    private final ReentrantLock LOCK = new ReentrantLock();

    /* 提供两个构造方法,1个使用默认方式获取workId,一个由外部传入workId */

    public SnowFlakeIdGenerator() {
        workId = defaultWorkId();
    }

    public SnowFlakeIdGenerator(Long workId) {
        if (workId == null || workId < 0 || workId > MAX_WORK_ID) {
            throw new RuntimeException("workId不符合规范");
        }
        this.workId = workId;
    }

    @Override
    public long generateId() {
        LOCK.lock();
        try {
            long time = System.currentTimeMillis();
            if (time < lastTime) {
                throw new RuntimeException("发生时钟回拨");
            }

            if (lastTime == time) {
                if (sequence == MAX_SEQUENCE_ID) {
                    time = waitNextTime(time);
                    lastTime = time;
                    sequence = 0;
                } else {
                    sequence++;
                }
            } else {
                lastTime = time;
                sequence = 0;
            }

            return ((time - BASE_TIME) << (SEQUENCE_BIT_SIZE + WORKER_BIT_SIZE))
                    | (workId << SEQUENCE_BIT_SIZE)
                    | sequence;
        } finally {
            LOCK.unlock();
        }
    }

    private long waitNextTime(final long time) {
        long t = System.currentTimeMillis();
        while (t <= time) {
            t = System.currentTimeMillis();
        }
        return t;
    }

    /**
     * 根据IP地址,获取默认的workId
     */
    private Long defaultWorkId() {
        // 根据ip地址生成一个workId
        try {
            String address = getLocalIp();
            long currentPid = getCurrentPid();
            return (address.hashCode() + currentPid) * 31 % (MAX_WORK_ID + 1);
        } catch (UnknownHostException e) {
            e.printStackTrace();
            throw new RuntimeException("机器ID生成失败");
        }
    }

    /**
     * 获取本机的IP地址
     */
    private String getLocalIp() throws UnknownHostException {
        return InetAddress.getLocalHost().getHostAddress();
    }

    /**
     * 获取当前进程ID
     * @return 当前进程ID,获取失败返回-1
     */
    private long getCurrentPid() {
        try {
            String runtimeName = ManagementFactory.getRuntimeMXBean().getName();
            if (runtimeName == null || !runtimeName.contains("@")) {
                return -1;
            }
            return Long.parseLong(runtimeName.split("@")[0]);
        } catch (Exception e) {
            // 捕获解析失败/空指针等异常
            System.err.println("获取PID失败:" + e.getMessage());
            return -1;
        }
    }

    private static long getBaseTime() {
        final String S = "2016-01-01 00:00:00";
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date;
        try {
            date = formatter.parse(S);
        } catch (ParseException e) {
            e.printStackTrace();
            throw new RuntimeException("解析基础时间失败");
        }
        return date.getTime();
    }

    /**
     * 解析ID获取详细信息(调试用)
     */
    public Data parseId(long id) {
        long timestamp = (id >> (SEQUENCE_BIT_SIZE + WORKER_BIT_SIZE)) + BASE_TIME;
        long workerId = (id >> SEQUENCE_BIT_SIZE) & MAX_WORK_ID;
        long sequence = id & MAX_SEQUENCE_ID;

        return new Data(timestamp, workerId, sequence);
    }

    public static class Data {
        public long timestamp;
        public long workerId;
        public long sequence;

        public Data(long timestamp, long workerId, long sequence) {
            this.timestamp = timestamp;
            this.workerId = workerId;
            this.sequence = sequence;
        }
    }
}

测试:

java 复制代码
public class SnowFlakeIdGeneratorTest {
    /**
     * 测试,验证生成的序列化id,后一个一定大于前一个。单线程
     */
    @Test
    public void test1() {
        IdGenerator idGenerator = new SnowFlakeIdGenerator();
        long lastId = 0;
        for (int i = 0; i < 300000; i++) {
            long id = idGenerator.generateId();
            if (id <= lastId) {
                System.out.println(lastId + " " + id);
                throw new RuntimeException("id重复");
            }
            lastId = id;
        }
    }

    /**
     * 测试,验证生成的序列化id,后一个一定大于前一个。多线程
     */
    @Test
    public void test2() {
        IdGenerator idGenerator = new SnowFlakeIdGenerator(10L);

        List<List<Long>> lists = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            lists.add(new ArrayList<>());
        }

        List<Thread> threadList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            threadList.add(new Thread(() -> {
                List<Long> list = lists.get(finalI);

                long lastId = 0;
                for (int j = 0; j < 30000; j++) {
                    long id = idGenerator.generateId();
                    list.add(id);
                    if (id <= lastId) {
                        System.out.println(lastId + " " + id);
                        throw new RuntimeException("id重复");
                    }
                    lastId = id;
                }
            }));
        }

        for (Thread thread : threadList) {
            thread.start();
        }
        for (Thread thread : threadList) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        Set<Long> set = new HashSet<>();
        for (List<Long> list : lists) {
            for (Long id : list) {
                if (set.contains(id)) {
                    throw new RuntimeException("重复id");
                } else {
                    set.add(id);
                }
            }
        }

        ArrayList<Long> list = new ArrayList<>(set);
        list.sort(Long::compareTo);


        int i = 0;
        for (Long l : list) {
            SnowFlakeIdGenerator snowFlakeIdGenerator = new SnowFlakeIdGenerator();
            SnowFlakeIdGenerator.Data data = snowFlakeIdGenerator.parseId(l);

            System.out.println(l + " " +
                    "data.timestamp = " + data.timestamp + " " + formatter.format(new Date(data.timestamp)) + " " +
                    "data.workerId = " + data.workerId + " " +
                    "data.sequence = " + data.sequence);
            i++;
            if (i == 20000) {
                break;
            }
        }
    }

    @Test
    public void test3() {
        System.out.println(Long.MAX_VALUE);
    }
}

性能测试:

java 复制代码
@State(Scope.Benchmark)
@BenchmarkMode(Mode.Throughput)  // 测试吞吐量
@OutputTimeUnit(TimeUnit.SECONDS)
@Warmup(iterations = 3, time = 1)  // 预热3轮,每轮1秒
@Measurement(iterations = 5, time = 2)  // 测试5轮,每轮2秒
@Threads(100)  // 100个线程并发
@Fork(1)
public class SnowflakeJMHTest {
    
    private IdGenerator idGenerator;
    private ConcurrentHashMap<Long, Boolean> idSet;
    private AtomicLong duplicateCount;
    
    @Setup
    public void setup() {
        idGenerator = new SnowFlakeIdGenerator(10L);
        idSet = new ConcurrentHashMap<>();
        duplicateCount = new AtomicLong(0);
    }
    
    @Benchmark
    public void testConcurrentGeneration() {
        long id = idGenerator.generateId();
        
        // 检查重复
        Boolean existed = idSet.putIfAbsent(id, Boolean.TRUE);
        if (existed != null) {
            duplicateCount.incrementAndGet();
        }
    }
    
    @TearDown
    public void tearDown() {
        long duplicates = duplicateCount.get();
        if (duplicates > 0) {
            System.err.println("警告:发现 " + duplicates + " 个重复ID!");
        }
    }
    
    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
            .include(SnowflakeJMHTest.class.getSimpleName())
            .build();
        
        new Runner(opt).run();
    }
}

测试结果:

text 复制代码
Benchmark                                            Mode       Cnt         Score    Error  Units
SnowflakeJMHTest.testConcurrentGeneration          sample  12648265      3859.767 ± 69.740  ns/op
SnowflakeJMHTest.testConcurrentGeneration:p0.00    sample                     ≈ 0           ns/op
SnowflakeJMHTest.testConcurrentGeneration:p0.50    sample                     ≈ 0           ns/op
SnowflakeJMHTest.testConcurrentGeneration:p0.90    sample                 100.000           ns/op
SnowflakeJMHTest.testConcurrentGeneration:p0.95    sample                 100.000           ns/op
SnowflakeJMHTest.testConcurrentGeneration:p0.99    sample                1200.000           ns/op
SnowflakeJMHTest.testConcurrentGeneration:p0.999   sample              971776.000           ns/op
SnowflakeJMHTest.testConcurrentGeneration:p0.9999  sample             2025472.000           ns/op
SnowflakeJMHTest.testConcurrentGeneration:p1.00    sample            30212096.000           ns/op

性能统计分析,最快的,0秒就可以返回,最慢的,要30毫秒,TP99在9微妙左右。

雪花算法相关问题

时钟回拨问题

时钟回拨:系统时间向后跳变,例如NTP服务同步时间,或者手动设置时间。据统计,在使用ntp服务的情况下,物理服务器大约每月会发生0到3次轻微时钟回拨,回拨范围在100ms以内。(来自deepseek的数据)

如何处理时钟回拨:这里的方案是抛异常,业界成熟的方案:轻微时钟回拨,通常是200毫秒以内,等待,其它程度的时间回拨,抛异常。

workId重复问题

当前代码中,使用IP地址的哈希值 + 进程号,然后和1024取模,来生成workId,有小概率情况下,会发生哈希冲突,想要避免这种情况,可以使用redis,来为当前进程分配一个唯一id,这个id记录在redis中,不过这种方式存在回收id比较困难的情况,可以写一个扫描程序,检测指定服务器上是否有该进程id,如果没有,回收workId。

相关推荐
大叔_爱编程2 小时前
基于人脸识别的互联网课堂考勤系统-springboot
java·spring boot·毕业设计·人脸识别·源码·课程设计·课堂考勤系统
黛琳ghz2 小时前
BoostKit 性能优化原理与分布式存储 Global Cache 深度解析
分布式·性能优化·鲲鹏·服务·boostkit
invicinble2 小时前
关于认识cpu对线程处理能力的相关知识概念
java
凌乱风雨12112 小时前
Java单元测试、集成测试,区别
java·单元测试·集成测试
红队it2 小时前
【数据分析】基于Spark链家网租房数据分析可视化大屏(完整系统源码+数据库+开发笔记+详细部署教程+虚拟机分布式启动教程)✅
java·数据库·hadoop·分布式·python·数据分析·spark
奥特曼_ it2 小时前
【数据分析】基于Spark链家网租房数据分析可视化大屏(完整系统源码+数据库+开发笔记+详细部署教程+虚拟机分布式启动教程)✔
大数据·笔记·分布式·数据挖掘·数据分析·spark·毕设
夏幻灵2 小时前
配置环境变量的核心目的
java
廋到被风吹走2 小时前
【Spring】Spring Core解析
java·spring·rpc
悟能不能悟2 小时前
Spring HATEOAS 详细介绍
java·后端·spring