当前位置: 首页 > news >正文

Java分布式全局唯一Id:id生成要求、为什么不用UUID、生成分布式雪花Id

文章目录

  • 为什么需要分布式全局唯一Id
  • Id生成规则部分硬性要求
  • Id生成系统的可用性要求
  • 为什么不用UUID
  • 生成分布式雪花Id
    • POM
    • 代码示例
    • API
    • 生成18位雪花Id
    • 生成13位雪花Id

为什么需要分布式全局唯一Id

在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。

如在美团点评的金融、支付、餐饮、酒店;猫眼电影等产品的系统中数据日渐增长,对数据分库分表后需要有一个唯一Id来标识一条数据或消息;特别一点的如订单、骑手、优惠券也都需要有唯一Id坐标时。

此时一个能够生成全局唯一Id的系统是非常必要的。

Id生成规则部分硬性要求

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

Id生成系统的可用性要求

  • 高可用:发一个获取分布式Id的请求,服务器就要保证99.999%的情况下给我创建一个唯一分布式Id;
  • 低延迟:发一个获取分布式Id的请求,服务器要快,极速;
  • 高QPS:例如并发一口气10万个创建分布式Id请求同时杀过来,服务器要顶得住且一下子成功创建10万个分布式Id。

为什么不用UUID

在《阿里巴巴 Java 开发手册》第五章 MySQL 规定第九条中,强制规定了单表的主键 id 必须为无符号的 bigint 类型,且是自增的。MySQL 中索引的数据结构是 B+Tree,这种数据结构的特点是索引树上的节点的数据是有序的,而如果使用 UUID 作为主键,那么每次插入数据时,因为无法保证每次产生的 UUID 有序,所以就会出现新的 UUID 需要插入到索引树的中间去,这样可能会频繁地导致页分裂,使性能下降。

太占用内存。每个 UUID 由 36 个字符组成,在字符串进行比较时,需要从前往后比较,字符串越长,性能越差。另外字符串越长,占用的内存越大,由于页的大小是固定的,这样一个页上能存放的关键字数量就会越少,这样最终就会导致索引树的高度越大,在索引搜索的时候,发生的磁盘 IO 次数越多,性能越差。

生成分布式雪花Id

POM

   		<dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.6</version>
        </dependency>

代码示例

   @Test
    public void test(){
        System.out.println(IdUtil.getSnowflakeNextId());
    }

API

package cn.hutool.core.util;

import cn.hutool.core.exceptions.UtilException;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.ObjectId;
import cn.hutool.core.lang.Singleton;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.lang.id.NanoId;
import cn.hutool.core.net.NetUtil;

public class IdUtil {
    public IdUtil() {
    }

    public static String randomUUID() {
        return UUID.randomUUID().toString();
    }

    public static String simpleUUID() {
        return UUID.randomUUID().toString(true);
    }

    public static String fastUUID() {
        return UUID.fastUUID().toString();
    }

    public static String fastSimpleUUID() {
        return UUID.fastUUID().toString(true);
    }

    public static String objectId() {
        return ObjectId.next();
    }

    /** @deprecated */
    @Deprecated
    public static Snowflake createSnowflake(long workerId, long datacenterId) {
        return new Snowflake(workerId, datacenterId);
    }

    public static Snowflake getSnowflake(long workerId, long datacenterId) {
        return (Snowflake)Singleton.get(Snowflake.class, new Object[]{workerId, datacenterId});
    }

    public static Snowflake getSnowflake(long workerId) {
        return (Snowflake)Singleton.get(Snowflake.class, new Object[]{workerId});
    }

    public static Snowflake getSnowflake() {
        return (Snowflake)Singleton.get(Snowflake.class, new Object[0]);
    }

    public static long getDataCenterId(long maxDatacenterId) {
        Assert.isTrue(maxDatacenterId > 0L, "maxDatacenterId must be > 0", new Object[0]);
        if (maxDatacenterId == Long.MAX_VALUE) {
            --maxDatacenterId;
        }

        long id = 1L;
        byte[] mac = null;

        try {
            mac = NetUtil.getLocalHardwareAddress();
        } catch (UtilException var6) {
        }

        if (null != mac) {
            id = (255L & (long)mac[mac.length - 2] | 65280L & (long)mac[mac.length - 1] << 8) >> 6;
            id %= maxDatacenterId + 1L;
        }

        return id;
    }

    public static long getWorkerId(long datacenterId, long maxWorkerId) {
        StringBuilder mpid = new StringBuilder();
        mpid.append(datacenterId);

        try {
            mpid.append(RuntimeUtil.getPid());
        } catch (UtilException var6) {
        }

        return (long)(mpid.toString().hashCode() & '\uffff') % (maxWorkerId + 1L);
    }

    public static String nanoId() {
        return NanoId.randomNanoId();
    }

    public static String nanoId(int size) {
        return NanoId.randomNanoId(size);
    }

    public static long getSnowflakeNextId() {
        return getSnowflake().nextId();
    }

    public static String getSnowflakeNextIdStr() {
        return getSnowflake().nextIdStr();
    }
}

生成18位雪花Id

public class SnowFlake {
    // 起始的时间戳,这个时间戳可以是你的系统初始时间,一般取当前时间戳
    private final static long START_TIMESTAMP = 1672502400000L; // 2023-01-01 00:00:00

    // 每一部分占用的位数,可以根据自己的需求进行调整,这里是按照默认的占位数进行分配
    private final static long SEQUENCE_BIT = 12; // 序列号占用的位数
    private final static long MACHINE_BIT = 5; // 机器标识占用的位数
    private final static long DATA_CENTER_BIT = 5; // 数据中心占用的位数

    // 每一部分的最大值,可以根据占用的位数进行计算得到
    private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
    private final static long MAX_MACHINE_NUM = ~(-1L << MACHINE_BIT);
    private final static long MAX_DATA_CENTER_NUM = ~(-1L << DATA_CENTER_BIT);

    // 每一部分向左的位移,计算出来的值是为了后面生成 ID 做准备
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;

    private long dataCenterId; // 数据中心 ID
    private long machineId; // 机器 ID
    private long sequence = 0L; // 序列号
    private long lastTimeStamp = -1L; // 上一次时间戳

    /**
     * <h2>构造方法</h2>
     *
     * @param dataCenterId 数据中心 ID
     * @param machineId    机器 ID
     */
    public SnowFlake(long dataCenterId, long machineId) {
        if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
            throw new IllegalArgumentException("数据中心标识不能大于等于 " + MAX_DATA_CENTER_NUM + " 或小于 0");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("机器标识不能大于等于 " + MAX_MACHINE_NUM + " 或小于 0");
        }
        this.dataCenterId = dataCenterId;
        this.machineId = machineId;
    }

    /**
     * <h2>雪花算法核心方法</h2>
     * 通过调用 nextId() 方法,让当前这台机器上的 snowflake 算法程序生成一个全局唯一的 id
     */
    public synchronized long nextId() {
        // 获取系统当前时间戳
        long currentTimeStamp = getSystemCurrentTimeMillis();
        if (currentTimeStamp < lastTimeStamp) {
            throw new RuntimeException("时钟向后移动,拒绝生成雪花算法ID");
        }

        if (currentTimeStamp == lastTimeStamp) {
            // 当前毫秒内,序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            // 序列号超出范围,需要等待下一毫秒
            if (sequence == 0L) {
                // 获取下一毫秒
                currentTimeStamp = getNextMill(lastTimeStamp);
            }
        } else {
            // 不同毫秒内,序列号置为 0
            sequence = 0L;
        }

        lastTimeStamp = currentTimeStamp;

        // 使用位运算生成最终的 ID
        return (currentTimeStamp - START_TIMESTAMP) << TIMESTAMP_LEFT
                | dataCenterId << DATA_CENTER_LEFT
                | machineId << MACHINE_LEFT
                | sequence;
    }

    /**
     * <h2>获取系统当前时间戳</h2>
     *
     * @return 当前时间(毫秒)
     */
    private long getSystemCurrentTimeMillis() {
        return System.currentTimeMillis();
    }

    /**
     * <h2>获取下一毫秒</h2>
     * 当某一毫秒的时间,产生的 id 数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生 ID
     *
     * @param lastTimestamp 上次生成 ID 的时间截
     * @return 当前时间戳
     */
    private long getNextMill(long lastTimestamp) {
        long timeMillis = getSystemCurrentTimeMillis();
        while (timeMillis <= lastTimestamp) {
            timeMillis = getSystemCurrentTimeMillis();
        }
        return timeMillis;
    }

    public static void main(String[] args) {
        SnowFlake worker1 = new SnowFlake(1, 1);
        System.out.println(worker1.nextId());
    }
}

生成13位雪花Id

package com.ais.common.web.utils;

public class IdUtil {
    /**
     * 开始时间截 (本次时间戳为:Thu Nov 04 2010 09:42:54 GMT+0800 (中国标准时间)----1288834974657L---1656543015264587776--19 )
     */
    private final long startTime = 1683803335498L;

    /**
     * 机器id所占的位数
     */
    private final long workerIdBits = 3L;

    /**
     * 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
     */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

    /**
     * 序列在id中占的位数
     */
    private final long sequenceBits = 5L;

    /**
     * 机器ID向左移12位
     */
    private final long workerIdShift = sequenceBits;

    /**
     * 时间截向左移22位(10+12)
     */
    private final long timestampLeftShift = sequenceBits + workerIdBits;

    /**
     * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
     */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

    /**
     * 工作机器ID(0~1024)
     */
    private long workerId;

    /**
     * 毫秒内序列(0~4095)
     */
    private long sequence = 0L;

    /**
     * 上次生成ID的时间截
     */
    private long lastTimestamp = -1L;
    private final static IdUtil idWorker = new IdUtil(1);
    //==============================Constructors=====================================

    /**
     * 构造函数
     *
     * @param workerId 工作ID (0~1024)
     */
    public IdUtil(long workerId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", maxWorkerId));
        }
        this.workerId = workerId;
    }

    // ==============================Methods==========================================

    /**
     * 获得下一个ID (该方法是线程安全的)
     *
     * @return SnowflakeId
     */
    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 {
            sequence = 0L;
        }

        //上次生成ID的时间截
        lastTimestamp = timestamp;

        //移位并通过或运算拼到一起组成64位的ID
        return ((timestamp - startTime) << timestampLeftShift) | (workerId << workerIdShift) | sequence;
    }

    /**
     * 阻塞到下一个毫秒,直到获得新的时间戳
     *
     * @param lastTimestamp 上次生成ID的时间截
     * @return 当前时间戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

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

    public static long getSnowflakeNextId() {
        return idWorker.nextId();
    }

    /**
     * 测试
     */
    public static void main(String[] args) {
        System.out.println(IdUtil.getSnowflakeNextId());
    }
}

相关文章:

  • Flask 快速入门
  • 鸿蒙(HarmonyOS)应用开发——构建页面(题目答案)
  • 主要分布式文件系统架构对比分析:GFS vs. Tectonic vs. JuiceFS
  • 从零开始训练一个ChatGPT大模型(低资源,1B3)
  • 【C++】期末复习题(一)
  • 25、pytest的测试报告插件allure
  • leetcode每日一题36
  • 聊聊 Jetpack Compose 的 “状态订阅自动刷新” -- mutableStateListOf
  • OpenCV Mat和Bitmap的转换
  • Linux篇之基于Centos的everything镜像搭建yum镜像源
  • SSM项目实战-mapper实现
  • 【halcon】halcon 函数文件 以及 脚本引擎如何调用外部函数文件 下篇
  • 基于SSM的图书馆管理系统的设计与实现
  • 目标检测YOLO实战应用案例100讲-基于深度学习的输电线路 鸟巢检测
  • 阿里云虚拟机安装nginx容器步骤
  • 互联网Java工程师面试题·Spring Boot篇·第一弹
  • uniapp开发小程序经验记录
  • python监控cpu程序
  • dockerfile与docker-compose解释及对比
  • C++12.4
  • 美国4月CPI同比上涨2.3%低于预期,为2021年2月来最小涨幅
  • 第十届影像上海博览会落幕后,留给中国摄影收藏的三个问题
  • 成都警方通报:8岁男孩落水父母下水施救,父亲遇难
  • 新剧|《藏海传》定档,《折腰》《人生若如初见》今日开播
  • 学者纠错遭网暴,人民锐评:“饭圈”该走出畸形的怪圈了
  • 为证明我爸是我爸,我将奶奶告上法庭