Java学习篇(一)| 如何生成分布式全局唯一ID

news2024/9/23 11:23:17

Java学习篇(一)| 如何生成分布式全局唯一ID

  • 一、使用场景
  • 二、常用方法
    • 1、UUID (尽量不要用)
    • 2、数据库自增 (用的最多-但不适合做分布式ID)
    • 3、Redis 生成ID (可用)
      • 1、原因
      • 2、通过代码实现分布式全局唯一ID工具 (正式使用)
      • 3、编写获取工具
      • 4、测试获取工具
      • 5、总结
    • 4、雪花算法(SnowFlake)(可用)
      • 1.雪花算法概念
      • 2.雪花算法的缺点
      • 方式一:自定义的雪花算法 (该工具类已做了回拨处理)
      • 方式二:基于hutool工具类
        • 第一步:引入hutool工具依赖包
        • 第二步:Springboot整合具体代码实现
      • 方式三:使用 百度 Uidgenerator(基于SnowFlake算法改造的)
      • 方式四:使用 美团Leaf(基于SnowFlake算法改造的)
  • 三、前端直接使用发生精度丢失
  • 参考文章

一、使用场景

全局唯一ID在电商购物、社交网络、金融系统、分布式数据库和缓存、物联网等场景下都尤为重要。
它确保了数据的唯一性、一致性、安全性和高效性,是现代软件开发和系统设计中不可或缺的一部分。

二、常用方法

解决方案根据自己项目需求进行设计调整

1、UUID (尽量不要用)

UUID (Universally Unique Identifier),通用唯一识别码的缩写。UUID是由一组32位数的16进制数字所构成,所以UUID理论上的总数为 16^32=2^128,约等于 3.4 x 10^38。也就是说若每纳秒产生1兆个UUID,要花100亿年才会将所有UUID用完。 生成的UUID是由 8-4-4-4-12格式的数据组成,其中32个字符和4个连字符’ - ',一般我们使用的时候会将连字符删除 uuid.toString().replaceAll("-","")

目前UUID的产生方式有5种版本,每个版本的算法不同,应用范围也不同。

  • 版本1:基于时间的UUID 这个一般是通过当前时间,随机数,和本地Mac地址来计算出来,可以通过 org.apache.logging.log4j.core.util包中的 UuidUtil.getTimeBasedUuid()来使用或者其他包中工具。由于使用了MAC地址,因此能够确保唯一性,但是同时也暴露了MAC地址,私密性不够好。
  • 版本2 :DCE(Distributed Computing Environment) DCE安全的UUID 安全的UUID和基于时间的UUID算法相同,但会把时间戳的前4位置换为POSIX的UID或GID。这个版本的UUID在实际中较少用到。
  • 版本3:基于名字的UUID(MD5)- 版本3 基于名字的UUID通过计算名字和名字空间的MD5散列值得到。这个版本的UUID保证了:相同名字空间中不同名字生成的UUID的唯一性;不同名字空间中的UUID的唯一性;相同名字空间中相同名字的UUID重复生成是相同的。
  • 版本4:随机UUID -根据随机数,或者伪随机数生成UUID。这种UUID产生重复的概率是可以计算出来的,但是重复的可能性可以忽略不计,因此该版本也是被经常使用的版本。JDK中使用的就是这个版本。
  • 版本5:基于名字的UUID(SHA1) - 版本5 和基于名字的UUID算法类似,只是散列值计算使用SHA1(Secure Hash Algorithm 1)算法。

Java中 JDK自带的 UUID产生方式就是版本4根据随机数生成的 UUID 和版本3基于名字的 UUID,有兴趣的可以去看看它的源码。

 public static void main(String[] args) {
 
     //获取一个版本4根据随机字节数组的UUID。
     UUID uuid = UUID.randomUUID();
     System.out.println(uuid.toString().replaceAll("-",""));
 
     //获取一个版本3(基于名称)根据指定的字节数组的UUID。
     byte[] nbyte = {1, 2, 3};
     UUID uuidFromBytes = UUID.nameUUIDFromBytes(nbyte);
     System.out.println(uuidFromBytes.toString().replaceAll("-",""));
 }

优点:属于本地解决方案,无网络消耗

缺点:

  • 不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用
  • MAC 地址提供了唯一性的保证,但也带来安全风险,最糟的是它是字符串形式,占用空间大,查询性能低,无法保证趋势递增
  • ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用:
    • MySQL官方有明确的建议主键要尽量越短越好,36个字符长度的UUID不符合要求
    • 对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能

2、数据库自增 (用的最多-但不适合做分布式ID)

这种方式也是我们用的最多的方式,通常使用数据库自增,不同数据库自增命令可能不同,
以MySQL为例,直接设置AUTO_INCREMENT就可以使主键自增。

优点:

  • 单体项目实现简单,命令即可设置,成本小,有DBA专业维护
  • 生成的ID有序,可以实现一些对ID有特殊要求的业务。

缺点:

  • 不同数据库语法或实现不同,数据库迁移的时候需要处理
  • 在单个数据库或读写分离或一主多从多情况下,只有一个主库可以生成ID,有单点故障的风险
  • 在性能达不到要求的情况下比较难以扩展
  • 数据迁移或者系统数据合并比较麻烦
  • 分库分表时会比较麻烦
  • ID发号性能瓶颈限制在单台MySQL的读写性能

3、Redis 生成ID (可用)

1、原因

1、数据库自增ID是有序增长的很容易就被人猜到,比如我现在下一单看到的订单ID为999那么就知道你的系统里最多只有999单,
2、这种自增ID没有意义,而且不同业务的自增ID是重合的,对于信息区分度很低,而且考虑到多业务交互和用户端展示也都是不合适的,想想看要是你在某宝下单,订单ID是999,或者在对接别人订单系统时,给你的订单ID是999是不是很奇怪。
3、当存在分库分表设计时,自增ID的操作就会导致无法实现唯一性

那应该如何通过Redis来设计一个分布式全局唯一ID生成工具?

用户下单调用下单逻辑,先进行业务逻辑处理,然后携带订单ID标识通过分布式全局唯一ID工具获取一个唯一的订单ID,这个订单ID标识就是用于区分业务的,获取到订单ID后将数据组装入库,分布式全局唯一ID工具可以做成一个内嵌的utils,也可以封装成一个独立的jar,还可以做成一个分布式全局唯一ID生成服务供其它业务服务调用。

在这里插入图片描述

使用 Redis 计数器实现

RedisString结构提供了计数器自增功能,类似Java中的原子类,还要优于Java的原子类,
因为Redis是单线程执行的缓存读写本身就是线程安全的,也不用进行原子类的乐观锁操作,
每一次获取分布式全局唯一ID时就将自增序列加1

# 给key为GENERATEID:NO的value自增1,如果这key不存在则会添加到Redis中并且设置value为1
## GENERATEID:key前缀
## NO:订单ID标识
127.0.0.1:6379> incr GENERATEID:NO
(integer) 1

使用 Redis Hash结构实现

Redis Hash结构中的每一个field也可以进行自增操作,可以用一个Hash结构存储所有的标识信息和自增序列,方便管理,比较适合并发不高的小项目所有服务都是用的一个Redis,如果并发较高就不合适了,毕竟Redis操作普通String结构肯定比操作Hash结构快。

# 给key为GENERATEID,field为no的value自增1,如果这key不存在则会添加到Redis中并且设置value为1
## GENERATEID:分布式全局唯一ID Hash key
## NOHash结构中的field
127.0.0.1:6379> hincrby GENERATEID NO 1
(integer) 1

2、通过代码实现分布式全局唯一ID工具 (正式使用)

这里使用Redis 计数器实现,自增序列以天为单位存储,
在实际业务中,比如生成订单编号组成规则都类似NO1699631999000-1(业务标识key+当前时间戳+自增序列),这个规则可以自己定义,保证最终生成的订单编号不重复即可,不建议直接一个自增序列干到底。
订单编号这类型的数据都是有长度限制的,或者是要求生成20字符的订单编号,如果增长的过长反而不好处理。

3、编写获取工具

@Component
public class RedisGenerateIDUtils {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    // key前缀
    private String PREFIX = "GENERATEID:";

    /**
     * 获取全局唯一ID
     * @param key 业务标识key
     */
    public String generateId(String key) {
        // 获取对应业务自增序列
        Long incr = getIncr(key);
        // 组装最后的结果,这里可以根据需要自己定义,这里是按照业务标识key+当前时间戳+自增序列进行组装
        String resultID = key + System.currentTimeMillis() + "-" + incr;
        return resultID;
    }

    /**
     * 获取对应业务自增序列
     */
    private Long getIncr(String key) {
        String cacheKey = getCacheKey(key);
        Long increment = 0L;
        // 判断Redis中是否存在这个自增序列,如果不存在添加一个序列并且设置一个过期时间
        if (!redisTemplate.hasKey(cacheKey)) {
            // 这里存在线程安全问题,需要加分布式锁,这里做简单实现
            String lockKey = cacheKey + "_LOCK";
            // 设置分布式锁
            boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 30, TimeUnit.SECONDS);
            if (!lock) {
                // 如果没有拿到锁进行自旋
                return getIncr(key);
            }
            increment = redisTemplate.opsForValue().increment(cacheKey);
            // 我这里设置24小时,可以根据实际情况设置当前时间到当天结束时间的插值
            redisTemplate.expire(cacheKey, 24, TimeUnit.HOURS);

            // 释放锁
            redisTemplate.delete(lockKey);
        } else {
            increment = redisTemplate.opsForValue().increment(cacheKey);
        }

        return increment;
    }

    /**
     * 组装缓存key
     */
    private String getCacheKey(String key) {
        return PREFIX + key + ":" + getYYYYMMDD();
    }

    /**
     * 获取当前YYYYMMDD格式年月日
     */
    private String getYYYYMMDD() {
        LocalDate currentDate = LocalDate.now();
        int year = currentDate.getYear();
        int month = currentDate.getMonthValue();
        int day = currentDate.getDayOfMonth();
        return "" + year + month + day;
    }
}

4、测试获取工具

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RedisUniqueIdDemoApplication.class)
class RedisUniqueIdDemoApplicationTests {

    @Resource
    private RedisGenerateIDUtils redisGenerateIDUtils;
    

    @Test
    public void test() throws InterruptedException {
        // 定义一个线程池 设置核心线程数和最大线程数都为100,队列根据需要设置
        ThreadPoolExecutor executor = new ThreadPoolExecutor(100, 100, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10000));
        CountDownLatch countDownLatch = new CountDownLatch(10000);

        long beginTime = System.currentTimeMillis();
        // 获取10000个全局唯一ID 看看是否有重复
        CopyOnWriteArraySet<String> ids = new CopyOnWriteArraySet<>();
        for (int i = 0; i < 10000; i++) {
            executor.execute(() -> {
                // 获取全局唯一ID
                long beginTime02 = System.currentTimeMillis();
                String orderNo = redisGenerateIDUtils.generateId("NO");
                System.out.println(orderNo);
                System.out.println("获取单个ID耗时 time=" + (System.currentTimeMillis() - beginTime02));
                if (ids.contains(orderNo)) {
                    System.out.println("重复ID=" + orderNo);
                } else {
                    ids.add(orderNo);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        // 打印获取到的全局唯一ID集合数量
        System.out.println("获取到全局唯一ID count=" + ids.size());
        System.out.println("耗时毫秒 time=" + (System.currentTimeMillis() - beginTime));
    }
}

知识小贴士:关于countdownlatch
(这是在模拟100个请求并发获取1w个后再结束的操作所涉及的 多线程相关 知识点,和 获取分布式唯一id 没有直接关系哦)

countdownlatch名为信号枪:主要的作用是同步协调在多线程的等待于唤醒问题

我们如果没有CountDownLatch ,那么由于程序是异步的,当异步程序没有执行完时,主线程就已经执行完了,然后我们期望的是分线程全部走完之后,主线程再走,所以我们此时需要使用到CountDownLatch

CountDownLatch 中有两个最重要的方法

  • countDown
  • await

await 方法 是阻塞方法,我们担心分线程没有执行完时,main线程就先执行,所以使用await可以让main线程阻塞,那么什么时候main线程不再阻塞呢?当CountDownLatch 内部维护的 变量变为0时,就不再阻塞,直接放行,那么什么时候CountDownLatch 维护的变量变为0 呢,我们只需要调用一次countDown ,内部变量就减少1,我们让分线程和变量绑定, 执行完一个分线程就减少一个变量,当分线程全部走完,CountDownLatch 维护的变量就是0,此时await就不再阻塞,统计出来的时间也就是所有分线程执行完后的时间。

5、总结

通过线程池 我们模拟了100个请求同时去获取全局唯一ID是没问题的,
而且获取单个ID耗时在10-20毫秒左右,一般的业务已经完全够用,这个耗时也要看Redis性能和项目配置决定的。

4、雪花算法(SnowFlake)(可用)

1.雪花算法概念

雪花算法(Snowflake)是一种生成唯一ID的算法,主要应用于分布式系统中。它可以在不依赖于数据库等其他存储设施的情况下,生成全局唯一的ID。

雪花算法生成的ID为64位整数(二进制,全是0和1组成),具体的格式如下:

在这里插入图片描述

  • 1位符号位:
    二进制数据中首位表示正负,这里是0不可变
  • 41位的时间戳:
    41位用来标识时间戳,最大值可容纳是2的41次方( 2199023255552 ),换算转成时间的话就是可以用69年(从1970年开始算),所以建议在设置起始时间戳时,请用上线的时间,这样子就可以保证该系统69年的唯一id获取。
  • 10位的机器位:
    机器位最大为10位,一般做法是5位用于机房id的标识,5位用于机器id的标识。这样无论是机房和机器都可以最大容纳2的五次方减1(31)的数量。不过实际使用时可以根据实际情况进行调整,因为机房数量一般也到不了31,就是机器数量到达31的也不多。所以可以根据实际情况来进行调整机器位10个bit的分配。
  • 12位的随机数:
    12位的随机数最大可以表示2的12次方减1的数据(4095)所以也就是说最大我们可以在1ms内产生4095个id(时间戳位是ms),那么1s内就是4095000≈400W。而且这是单台机器上的每秒可产生的不重复id,如果横向扩展机器的话,这个值还会更大。所以12位的随机数位是肯定够用的了,当然真正使用时是不能使用随机数的,而是应该进行整数的自增,这样才能保证不重复。

实际情况下,我们是需要把这个二进制转成十进制来作为id使用的哦

2.雪花算法的缺点

由于Snowflake算法生成的ID包含时间戳等信息,因此在使用时需要保证系统时间的准确性。
如果系统时间发生回拨或者误差较大,可能会导致生成的ID出现重复或者乱序的问题。

方式一:自定义的雪花算法 (该工具类已做了回拨处理)

注意!注意!注意!以下代码亲测过,并有详细的注释解说,直接拿走,不谢!!!

public class SnowFlakeUtil {
 
    /**
     * 初始时间戳,可以根据业务需求更改时间戳
     */
    private final long twepoch = 11681452025134L;
 
    /**
     * 机器ID所占位数,长度为5位
     */
    private final long workerIdBits = 5L;
 
    /**
     * 数据标识ID所占位数,长度位5位
     */
    private final long datacenterIdBits = 5L;
 
    /**
     * 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
     */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
 
    /**
     * 支持的最大数据标识id,结果是31
     */
    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=0xfff=4095)
     */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);
 
    /**
     * 工作机器ID(0~31),2进制5位  32位减掉1位 31个
     */
    private volatile long workerId;
 
    /**
     * 数据中心ID(0~31),2进制5位  32位减掉1位 31个
     */
    private volatile long datacenterId;
 
    /**
     * 毫秒内序列(0~4095),2进制12位 4096 - 1 = 4095个
     */
    private volatile long sequence = 0L;
 
    /**
     * 上次时间戳,初始值为负数
     */
    private volatile long lastTimestamp = -1L;
 
 
    // ==============================Constructors=====================================
 
    /**
     * 有参构造
     * @param workerId 工作机器ID(0~31)
     * @param datacenterId 数据中心ID(0~31)
     * @param sequence 毫秒内序列(0~4095)
     */
    public SnowFlakeUtil(long workerId, long datacenterId, long sequence){
        // sanity check for workerId
        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));
        }
        System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
                timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
 
        this.workerId = workerId;
        this.datacenterId = datacenterId;
        this.sequence = sequence;
    }
 
    // ==============================Methods==========================================
 
    /**
     * 获得下一个ID (该方法是线程安全的)
     *  如果一个线程反复获取Synchronized锁,那么synchronized锁将变成偏向锁。
     * @return 生成的ID
     */
    public synchronized long nextId() {
        // 获取当前时间的时间戳,单位(毫秒)
        long timestamp = timeGen();
 
        // 获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常
        if (timestamp < lastTimestamp) {
            System.err.printf("当前时间戳不能小于上次时间戳,上次时间戳为: %d.", lastTimestamp);
            throw new RuntimeException(String.format("当前时间戳不能小于上次时间戳,生成ID失败. 时间戳差值: %d milliseconds",
                    lastTimestamp - timestamp));
        }
 
        // 获取当前时间戳如果等于上次时间戳(同一毫秒内),则在序列号加一;否则序列号赋值为0,从0开始。
        if (lastTimestamp == timestamp) {
            /* 逻辑:意思是说一个毫秒内最多只能有4096个数字,无论你传递多少进来,
                    这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围 */
            // sequence:毫秒内序列(0~4095);  sequenceMask: 序列号最大值;
            sequence = (sequence + 1) & sequenceMask;
            /* 逻辑:当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID */
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }
 
        // 将上次时间戳值刷新(逻辑:记录一下最近一次生成id的时间戳,单位是毫秒)
        lastTimestamp = timestamp;
 
        /* 核心逻辑:生成一个64bit的id;
                  先将当前时间戳左移,放到41 bit那儿;
                  将机房id左移放到5 bit那儿;
                  将机器id左移放到5 bit那儿;
                  将序号放最后12 bit
                  最后拼接起来成一个64 bit的二进制数字,转换成10进制就是个long型 */
        /*
         * 返回结果:
         * (timestamp - twepoch) << timestampLeftShift) 表示将时间戳减去初始时间戳,再左移相应位数
         * (datacenterId << datacenterIdShift) 表示将数据id左移相应位数
         * (workerId << workerIdShift) 表示将工作id左移相应位数
         * | 是按位或运算符,例如:x | y,只有当x,y不为0的时候结果才为0,其它情况结果都为1。
         * 因为个部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id
         */
        return ((timestamp - twepoch) << timestampLeftShift) |
                (datacenterId << dataCenterIdShift) |
                (workerId << workerIdShift) |
                sequence;
    }
 
    /**
     * 上次时间戳与当前时间戳进行比较
     * 逻辑:当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID
     * @param lastTimestamp 上次时间戳
     * @return 若当前时间戳小于等于上次时间戳(时间回拨了),则返回最新当前时间戳; 否则,返回当前时间戳
     */
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }
 
    /**
     * 获取系统时间戳
     * @return 当前时间的时间戳 14位
     */
    private long timeGen(){
        return System.currentTimeMillis();
    }
 
    public static void main(String[] args) {
        SnowFlakeUtil snowFlakeUtil = new SnowFlakeUtil(1,1,0);
        System.out.println(snowFlakeUtil.timeGen());
        for (int i = 0; i < 100; i++) {
            System.out.println("雪花算法生成第【"+(i+1)+"】个ID:"+ snowFlakeUtil.nextId());
        }
 
    }
 
}

以上代码执行结果:

雪花算法生成第【1】个ID:-5049534853385416704
雪花算法生成第【2】个ID:-5049534853385416703
雪花算法生成第【3】个ID:-5049534853385416702
雪花算法生成第【4】个ID:-5049534853385416701
雪花算法生成第【5】个ID:-5049534853385416700
雪花算法生成第【6】个ID:-5049534853385416699
雪花算法生成第【7】个ID:-5049534853385416698
雪花算法生成第【8】个ID:-5049534853385416697
雪花算法生成第【9】个ID:-5049534853385416696
雪花算法生成第【10】个ID:-5049534853385416695
……
……
…… (之后的结果就不一一展示了)

以上代码在遇到时间回拨的情况下,仍然能够获取到唯一的ID。
在常规的时间回拨场景下(如NTP同步导致的轻微时间调整),这段代码是足够健壮的。

方式二:基于hutool工具类

第一步:引入hutool工具依赖包
<!-- hutool工具类 -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.3.1</version>
</dependency>
第二步:Springboot整合具体代码实现
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.IdUtil;
 
import javax.annotation.PostConstruct;
 
/**
 * SpringtBoot整合雪花算法 (基于hutool工具类)
 */
public class SnowFlakeHutoolTestController {
    /**
     * 工作机器ID(0~31),2进制5位  32位减掉1位 31个
     */
    private long workerId = 0;
    /**
     * 数据中心ID(0~31),2进制5位  32位减掉1位 31个
     */
    private long datacenterId = 1;
 
    /**
     * 雪花算法对象
     */
    private Snowflake snowFlake = IdUtil.createSnowflake(workerId, datacenterId);
 
    @PostConstruct
    public void init() {
        try {
            // 将网络ip转换成long
            workerId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    /**
     * 获取雪花ID,默认使用网络IP作为工作机器ID
     * @return ID
     */
    public synchronized long snowflakeId() {
        return this.snowFlake.nextId();
    }
 
    /**
     * 获取雪花ID
     * @param workerId 工作机器ID
     * @param datacenterId 数据中心ID
     * @return ID
     */
    public synchronized long snowflakeId(long workerId, long datacenterId) {
        Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId);
        return snowflake.nextId();
    }
 
    public static void main(String[] args) {
        SnowFlakeHutoolTestController snowFlakeDemo = new SnowFlakeHutoolTestController();
        for (int i = 0; i < 20; i++) {
            int finalI = i;
            new Thread(() -> {
                System.out.println("雪花算法生成第【"+(finalI +1)+"】个ID:"+ snowFlakeDemo.snowflakeId());
            }, String.valueOf(i)).start();
        }
    }
 
}

以上代码执行结果:

雪花算法生成第【2】个ID:1646777064113700865
雪花算法生成第【5】个ID:1646777064113700868
雪花算法生成第【3】个ID:1646777064113700866
雪花算法生成第【4】个ID:1646777064113700867
雪花算法生成第【1】个ID:1646777064113700864
雪花算法生成第【11】个ID:1646777064113700873
雪花算法生成第【10】个ID:1646777064113700872
雪花算法生成第【8】个ID:1646777064113700871
雪花算法生成第【7】个ID:1646777064113700870
雪花算法生成第【6】个ID:1646777064113700869
雪花算法生成第【16】个ID:1646777064113700877
雪花算法生成第【13】个ID:1646777064113700876
雪花算法生成第【12】个ID:1646777064113700875
雪花算法生成第【9】个ID:1646777064113700874
雪花算法生成第【17】个ID:1646777064113700881
雪花算法生成第【19】个ID:1646777064113700880
雪花算法生成第【15】个ID:1646777064113700879
雪花算法生成第【14】个ID:1646777064113700878
雪花算法生成第【20】个ID:1646777064113700883
雪花算法生成第【18】个ID:1646777064113700882

方式三:使用 百度 Uidgenerator(基于SnowFlake算法改造的)

待记录

方式四:使用 美团Leaf(基于SnowFlake算法改造的)

待记录

三、前端直接使用发生精度丢失

如果前端直接使用服务端生成的long 类型 id,会发生精度丢失的问题,因为 JS 中Number是16位的(指的是十进制的数字)
而雪花算法计算出来最长的数字是19位的,这个时候需要用 String 作为中间转换,输出到前端即可。

参考文章

【1】分布式全局唯一ID生成方案(附源码)
【2】redis实现分布式全局唯一id
【3】使用 Redis 实现生成分布式全局唯一ID(使用SpringBoot环境实现)
【4】分布式Id生成之雪花算法(SnowFlake)
【5】SpringBoot实战:设备唯一ID生成【雪花算法、分布式应用】

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1996746.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AI革新3D建模:Stable Fast 3D工具的高效应用——图片快速生成3D模型

在3D建模领域,AI技术的介入正引发一场革命。Stable Diffusion(SD)的最新应用——Stable Fast 3D,为快速生成3D模型提供了一个强大的解决方案。以下是对这项技术及其应用的详细介绍和优化建议。 一、工具概览 Stable Fast 3D模型:这是一个基于AI的3D模型生成工具,可通过H…

Linux基础-总结篇

作者介绍&#xff1a;简历上没有一个精通的运维工程师。希望大家多多关注作者&#xff0c;下面的思维导图也是预计更新的内容和当前进度(不定时更新)。 经过前面30多小节的内容介绍&#xff0c;我们从虚拟机的安装&#xff0c;到Linux文件的基本操作(增删改查)&#xff0c;打包…

贪心算法总结(3)

一、最长回文串 409. 最长回文串 - 力扣&#xff08;LeetCode&#xff09; class Solution { public:int longestPalindrome(string s) {int hash[127]{0};for(char&ch:s) hash[ch];int ret0;for(int&x:hash) retx/2*2; //技巧1 利用向下取整return ret<s.size()?…

linux文件——深度学习文件fd、文件系统调用

前言&#xff1a;从本片开始正式进入linux文件的学习&#xff0c;本片内容主要是文件的fd。 本篇内容博主将要先带友友回忆C语言中的文件操作接口&#xff0c;然后再过渡到操作系统中的系统调用的学习&#xff0c;最后理解操作系统中的文件操作。 ps&#xff1a;本节内容设计一…

Android 10.0 DocumentsUI文件管理器首次进入默认显示内部存储文件功能实现

1.前言 在10.0的系统rom定制化开发中,在关于文件管理器的某些功能中,在首次进入文件管理器的时候默认进入下载 文件夹,点击菜单选择内部存储的时候,会显示内部存储的内容,客户开发需要要求默认显示内部存储的文件 接下来分析下功能的实现 如图: 2.DocumentsUI文件管理器首…

入营测评题解

第一题&#xff1a;first 第二题&#xff1a;chengji 打擂台&#xff0c;每个数跟当前最大、最小值比较&#xff0c;维护当前最值即可。 #include<bits/stdc.h> using lllong long; using namespace std;const int N1e610;int n; int x;//1e9, ll最大9e18 ll maxn0,minn…

C++ 新特性 | C++17 常用新特性介绍

目录 1、结构化绑定 2、constexpr扩展 2.1、constexpr lambda 2.2、constexpr if 2.3、constexpr string 4、if with initializer 5、std::optional 6、使用inline定义内联变量 7、std::filesystem库 8、折叠表达式 9、模板的模板参数推导 9.1、从构造函数参数推导…

景联文科技:破解数据标注行业痛点,引领高质量AI数据服务

数据标注行业是人工智能和机器学习领域中一个非常重要的组成部分。随着AI技术的发展&#xff0c;对高质量标注数据的需求也在不断增长。 数据标注市场的痛点 1. 团队管理 在众包和转包模式下&#xff0c;管理大量的标注人员是一项挑战。 需要确保标注人员的专业性、稳定性和…

【ConcurrentHashMap】JDK1.7版本源码解读与分析

如果对文章中提到的与 HashMap 相关的部分有任何疑问, 请移步HashMap源码详解 简介 底层是一个 Segment[] 数组, 每个 Segment对象 内部又有一个 Entry[ ] 数组, 一个 Entry[] 数组就相当于一个HashMap Entry[ ]采用拉链法解决冲突, 但是没有红黑树, 红黑树是1.8才引入的; 一…

音频进阶学习一——模拟信号和数字信号

文章目录 前言|版本声明&#xff1a;山河君&#xff0c;未经博主允许&#xff0c;禁止转载 一、什么是模拟信号和数字信号信号模拟信号数字信号数字和模拟信号的区别一览 二、信号处理系统总结 前言 所有软件的运行都得益于硬件上的突破&#xff0c;数字信号是从40年前就开始高…

达梦数据库 物理备份还原

达梦的物理备份还原 1.背景2.要求3.实验步骤3.1 相关术语3.2 准备工作3.3 联机备份还原3.3.1 数据备份3.3.1.1 手动备份3.3.1.2 定时备份 3.3.2 管理备份3.3.2.1 备份目录管理3.3.2.2 备份集校验与删除 3.3.3 数据还原 3.4 脱机备份还原3.4.1 DMRMAN工具3.4.2 数据备份3.4.2.1 …

https://ffmpeg.org/

https://ffmpeg.org/ https://www.gyan.dev/ffmpeg/builds/ https://github.com/BtbN/FFmpeg-Builds/releases F:\Document_ffmpeg F:\Document_ffmpeg\ffmpeg-master-latest-win64-gpl-shared\bin

python模式设计代码之观察者模式

1、观察者模式 话题订阅模式。观察者模式有两个角色&#xff0c;分别是话题发布者和话题订阅者&#xff08;即观察者&#xff09;。发布者就是把消息发送给话题&#xff0c;观察者就是订阅这个话题从而得到最新的资讯。这个模式的作用就拿手机的消息推送来说&#xff0c;app身…

深入C# .NET核心:委托与事件机制全解析

摘要&#xff1a; 在C# .NET编程中&#xff0c;委托和事件是实现异步编程和对象间通信的关键机制。理解它们的工作原理对于编写高效、响应式的应用程序至关重要。本文将深入探讨C# .NET中的委托与事件&#xff0c;从基础概念到高级应用&#xff0c;为读者提供全面的指导。 正文…

如何提高游戏的可玩性和趣味性?

提高游戏的可玩性和趣味性是吸引玩家并保持他们长期参与的关键。以下是一些策略和建议&#xff0c;可以帮助您增强游戏的吸引力和娱乐价值&#xff1a; 1. 独特的游戏机制 创新玩法&#xff1a;开发新颖、独特的游戏机制&#xff0c;让玩家在体验中感受到前所未有的乐趣。避免…

【网络编程】字节序,IP地址、点分十进制、TCP与UDP的异同

记录学习&#xff0c;思维导图绘制 目录 1、字节序​编辑 2、IP地址 3、点分十进制 4、TCP与UDP的异同 1、字节序 2、IP地址 3、点分十进制 4、TCP与UDP的异同

STL源码刨析:红黑树(RB-tree)

目录 1.前言 2.RB-tree的简单介绍 3.RB-tree的插入节点操作 4.RB-tree的删除节点操作 5.RB-tree的节点设计 6.RB-tree的迭代器设计 7.RB-tree的数据结构 8.RB-tree的构造与内存管理 9.RB-treed的元素操作 前言 在文章《STL源码刨析&#xff1a;树的导览》中&#xff0c;曾简单的…

使用 MongoDB 构建 AI:Flagler Health 的 AI 旅程如何彻底改变患者护理

Flagler Health 致力于为慢性病患者提供支持&#xff0c;为其匹配合适的医生以提供合适的护理。 通常&#xff0c;身患严重病痛的患者面临的选择有限&#xff0c;他们往往需要长期服用阿片类药物&#xff0c;或寻求成本高昂的侵入性外科手术干预。遗憾的是&#xff0c;后一种方…

linux小组件:git

git是什么&#xff1f; git是版本控制器&#xff08;去中心化的分布式系统&#xff09;可以快速高效地处理从小型到大型的各种项目。易于学习&#xff0c;占地面积小&#xff0c;性能极快。它具有廉价的本地库&#xff0c;方便的暂存区域和多个工作流分支等特性。 什么叫版本…

【数据结构七夕专属版】单链表及单链表的实现【附源码和源码讲解】

本篇是博主在学习数据结构时的心得&#xff0c;希望能够帮助到大家&#xff0c;也许有些许遗漏&#xff0c;但博主已经尽了最大努力打破信息差&#xff0c;如果有遗漏还请见谅&#xff0c;嘻嘻&#xff0c;前路漫漫&#xff0c;我们一起前进&#xff01;&#xff01;&#xff0…