Java学习篇(一)| 如何生成分布式全局唯一ID
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 计数器实现
Redis
的String
结构提供了计数器自增功能,类似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
## NO:Hash结构中的field
127.0.0.1:6379> hincrby GENERATEID NO 1
(integer) 1
2、通过代码实现分布式全局唯一ID工具 (正式使用)
这里
使用Redis 计数器实现
,自增序列以天为单位存储,
在实际业务中,比如生成订单编号组成规则都类似NO1699631999000-1
(业务标识key+当前时间戳+自增序列),这个规则可以自己定义,保证最终生成的订单编号不重复即可,不建议直接一个自增序列干到底。
订单编号这类型的数据都是有长度限制的,或者是要求生成20字符的订单编号,如果增长的过长反而不好处理。
3、编写获取工具
@Component
public class RedisGenerateIDUtils {@Resourceprivate 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 {@Resourceprivate RedisGenerateIDUtils redisGenerateIDUtils;@Testpublic 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(() -> {// 获取全局唯一IDlong 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 workerIdif (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);@PostConstructpublic void init() {try {// 将网络ip转换成longworkerId = 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生成【雪花算法、分布式应用】
相关文章:

Java学习篇(一)| 如何生成分布式全局唯一ID
Java学习篇(一)| 如何生成分布式全局唯一ID 一、使用场景二、常用方法1、UUID (尽量不要用)2、数据库自增 (用的最多-但不适合做分布式ID)3、Redis 生成ID (可用)1、原因2、通过代码…...
c++ 弹窗办法
在C中,创建弹窗的方法有多种。下面我会逐一介绍几种常见的方法,并提供相应的代码示例。 使用Windows API创建弹窗 使用Windows API可以创建原生的Windows弹窗。前面已经提供了一个示例代码,下面我会逐步解释该代码的关键部分: …...
小程序使用this.animate实现3维动画切换
小程序使用this.animate实现3维动画切换 这里以三张图片为例 话不多说,直接上代码 wxml <view class"container"><view class"carousel" id"carousel_id"><view class"box" wx:for"{{items}}"…...
【区块链+社会公益】第一反应互助急救链 | FISCO BCOS应用案例
第一反应互助急救链将现有业务体系和系统与 FISCO BCOS 的区块链底层技术相结合,使得公益行为及其证据、 积分、奖励过程以区块链的方式进行记录,确保公正、透明、不可篡改。此外,项目将赛事保障、公益急救培训、 社区互助急救、AED 勘察等社…...
leetcode 136. 只出现一次的数字
https://leetcode.cn/problems/single-number/description/?envTypestudy-plan-v2&envIdtop-interview-150 136. 只出现一次的数字 已解答 简单 相关标签 相关企业 提示 给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出…...

可扩展架构设计:策略与最佳实践
在快速变化的数字世界中,构建可扩展的架构设计已经成为企业成功的关键因素之一。随着业务规模的不断扩大,系统需求也随之增加,因此,能够灵活适应变化的系统架构变得尤为重要。本文将详细介绍可扩展架构设计的核心概念、重要性以及…...

一图胜千言|图解Pandas常用操作!
Pandas 展示 请看下表: 它描述了一个在线商店的不同产品线,共有四种不同的产品。与前面的例子不同,它可以用NumPy数组或Pandas DataFrame表示。但让我们看一下它的一些常见操作。 1. 排序 使用Pandas按列排序更具可读性,如下所示: 这里arg…...

ue5正确导入资源 content(内容),content只能有一个
把资源content下的东西,全部拷贝,放在项目的content下 content只能有一个...

HTTP协议基础知识【后端 4】
HTTP协议基础知识 HTTP(Hyper Text Transfer Protocol,超文本传输协议)是互联网上应用最为广泛的一种网络协议,它定义了客户端(如浏览器)与服务器之间数据传输的格式和规则。无论是浏览网页、在线购物还是使…...

2024/8/10 英语每日一段
Microsoft, Meta Platforms and Alphabet have spent billions on generative AI, spurred on by the release of startup OpenAI’s ChatGPT software released in late 2022. The world’s biggest technology companies have yet to unveil a strategy that points to relia…...

深入探索 Wireshark——网络封包分析的利器
一、引言 在当今数字化的时代,网络通信变得日益复杂和关键。无论是企业的网络运维,还是网络安全研究,都需要深入了解网络中传输的数据。Wireshark 作为一款强大的网络封包分析工具,成为了网络工程师、安全研究人员和技术爱好者不…...
VS2022使用.Net Framework4.0方法
从安装了低版本VS的电脑中 C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework \ 目录下 复制.Net Framework 4.0 等需要的目标包 如果打开项目仍然需要升级目标包的,先升级,加载项目后仍可更改目标包为.Net Framework 4…...
创建一个简单的贪吃蛇游戏:HTML、CSS和JavaScript教程
在本教程中,我们将逐步构建一个简单的贪吃蛇游戏。这个项目适合初学者,可以帮助你理解HTML、CSS和JavaScript的基础知识,并掌握如何将它们结合起来创建一个完整的游戏。 准备工作 在开始之前,请确保你已经安装了一个代码编辑器&…...

全面讲解电子齿轮比与脉冲数计算
一、什么是编码器分辨率 编码器(encoder)是将信号(如比特流)或数据进行编制、转换为可用以通讯、传输和存储的信号形式的设备。编码器把角位移或直线位移转换为电信号,按照读出方式编码器可以分为接触式和非接触式两种…...

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

SpringBoot企业人事管理系统-附源码与配套论文
1.1引言 随着计算机技术的飞速发展,计算机在各种单位机构管理中应用的普及﹐管理信息系统的开发在强调管理、强调信息的现代社会中也显得越来越重要。因此,利用计算机高效率地完成人事管理的日常事务,是适应现代各种单位机构制度要求、推动各种单位机构…...

用window计算器实现定点数的十进制和二进制之间相互转换
目录 前言 已知十进制定点数,求二进制定点数 正数 整数部分 小数部分 负数 已知二进制定点数,求十进制定点数 正数 负数 前言 windows 10自带的计算器,具有程序员模式,可以方便的进行进制的转换,但是由于二进制模式下,无法输入十进制的小数,所以无法直接实现定…...

搬砖人2024年的智能工作伙伴 —— 4款思维导图软件种草集!
幕布思维导图这玩意儿特别厉害,成了很多学生学习的好帮手,在学习中经常觉得信息太多太乱,不好理清楚。这时候用幕布思维导图,我们可以把那些复杂的知识点整理得有条有理。每个主题、每个小点都清清楚楚,学习的时候一眼…...

【Python第三方库】Requests全面解析
文章目录 安装基本用法测试网站发送GET请求发送POST请求更多请求请求参数请求头其他常用请求属性处理响应响应状态码响应内容 处理超时处理异常 requests 是一个非常流行的 Python HTTP 库,用于发送所有类型的 HTTP 请求。它简洁易用,能够处理复杂的请求…...
基于CNN的医学X-Ray图像分类全程解析
数据集 我们所使用的数据集是胸部 X 光图像,它包含 2 个类别:肺炎和正常。该数据集由 Paulo Breviglieri 发布,是 Paul Mooney 最受欢迎数据集的修订版,此更新版本的数据集在验证集和测试集中的图像分布更加均衡。数据集分为 3 个文件夹(训练、测试、验证),包含肺炎和正…...
设计模式和设计原则回顾
设计模式和设计原则回顾 23种设计模式是设计原则的完美体现,设计原则设计原则是设计模式的理论基石, 设计模式 在经典的设计模式分类中(如《设计模式:可复用面向对象软件的基础》一书中),总共有23种设计模式,分为三大类: 一、创建型模式(5种) 1. 单例模式(Sing…...
树莓派超全系列教程文档--(62)使用rpicam-app通过网络流式传输视频
使用rpicam-app通过网络流式传输视频 使用 rpicam-app 通过网络流式传输视频UDPTCPRTSPlibavGStreamerRTPlibcamerasrc GStreamer 元素 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 使用 rpicam-app 通过网络流式传输视频 本节介绍来自 rpica…...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...

Nuxt.js 中的路由配置详解
Nuxt.js 通过其内置的路由系统简化了应用的路由配置,使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...
论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(一)
宇树机器人多姿态起立控制强化学习框架论文解析 论文解读:交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(一) 论文解读:交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化…...
什么是EULA和DPA
文章目录 EULA(End User License Agreement)DPA(Data Protection Agreement)一、定义与背景二、核心内容三、法律效力与责任四、实际应用与意义 EULA(End User License Agreement) 定义: EULA即…...

【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)
骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术,它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton):由层级结构的骨头组成,类似于人体骨骼蒙皮 (Mesh Skinning):将模型网格顶点绑定到骨骼上,使骨骼移动…...
代理篇12|深入理解 Vite中的Proxy接口代理配置
在前端开发中,常常会遇到 跨域请求接口 的情况。为了解决这个问题,Vite 和 Webpack 都提供了 proxy 代理功能,用于将本地开发请求转发到后端服务器。 什么是代理(proxy)? 代理是在开发过程中,前端项目通过开发服务器,将指定的请求“转发”到真实的后端服务器,从而绕…...
代码随想录刷题day30
1、零钱兑换II 给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带…...

uniapp手机号一键登录保姆级教程(包含前端和后端)
目录 前置条件创建uniapp项目并关联uniClound云空间开启一键登录模块并开通一键登录服务编写云函数并上传部署获取手机号流程(第一种) 前端直接调用云函数获取手机号(第三种)后台调用云函数获取手机号 错误码常见问题 前置条件 手机安装有sim卡手机开启…...