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

【分布式】分布式唯一 ID 的 几种生成方案以及优缺点snowflake优化方案

在互联网的业务系统中,涉及到各种各样的ID,如在支付系统中就会有支付ID、退款ID等。那一般生成ID都有哪些解决方案呢?特别是在复杂的分布式系统业务场景中,我们应该采用哪种适合自己的解决方案是十分重要的。下面我们一一来列举一下,不一定全部适合,这些解决方案仅供参考,或许对你有用。

一、分布式ID

1.什么是分布式ID

日常开发中,我们需要对系统中的各种数据使用 ID 唯一表示,比如 用户 ID 对应且仅对应一个人,商品 ID 对应且仅对应一件商品,订 单 ID 对应且仅对应一个订单。

拿MySQL数据库举个例子:
在我们业务数据量不大的时候,单库单表完全可以支撑现有业务,数据再大一点搞个MySQL主从同步读写分离也能对付。
但随着数据日渐增长,主从同步也扛不住了,就需要对数据库进行分库分表,但分库分表后需要有一个唯一ID来标识一条数据,数据库的自增ID显然不能满足需求;特别一点的如订单、优惠券也都需要有唯一ID做标识。此时一个能够生成全局唯一ID的系统是非常必要的。那么这个全局唯一ID就叫分布式ID。

2.分布式ID的特性

  • 唯一性:确保生成的ID是全网唯一的。
  • 有序递增性:确保生成的ID是对于某个用户或者业务是按一定的数字有序递增的。
  • 高可用性:确保任何时候都能正确的生成ID。
  • 带时间:ID里面包含时间,一眼扫过去就知道哪天的交易。

二、分布式ID的生成方案

1. UUID

算法的核心思想是结合机器的网卡、当地时间、一个随记数来生成UUID。

优点:

  • 代码实现简单
  • 本地生成,没有性能问题,没有高可用风险
  • 全球唯一的,数据迁移容易

缺点:

  • 长度过长,存储冗余,且无序不可读,查询效率低
  • 每次生成的ID是无序的,不满足趋势递增
  • UUID是字符串,而且比较长,占用空间大,查询效率低
  • ID没有含义,可读性差

2. 数据库自增ID

使用数据库的id自增策略,如 MySQL 的 auto_increment。并且可以使用两台数据库分别设置不同步长,生成不重复ID的策略来实现高可用。

  • 优点:数据库生成的ID绝对有序,高可用实现方式简单
  • 缺点:需要独立部署数据库实例,成本高,有性能瓶颈

3. 批量生成ID

一次按需批量生成多个ID,每次生成都需要访问数据库,将数据库修改为最大的ID值,并在内存中记录当前值及最大值。

  • 优点:避免了每次生成ID都要访问数据库并带来压力,提高性能
  • 缺点:属于本地生成策略,存在单点故障,服务重启造成ID不连续

4. Redis生成ID

Redis的所有命令操作都是单线程的,本身提供像 incr 和 increby 这样的自增原子命令,所以能保证生成的 ID 肯定是唯一有序的。

  • 优点:有序递增,可读性强,性能较高。不依赖于数据库,灵活方便,且性能优于数据库;数字ID天然排序,对分页或者需要排序的结果很有帮助。
  • 缺点:占用带宽,依赖Redis:如果系统中没有Redis,还需要引入新的组件,增加系统复杂度;需要编码和配置的工作量比较大。
    考虑到单节点的性能瓶颈,可以使用 Redis 集群来获取更高的吞吐量。假如一个集群中有5台 Redis。可以初始化每台 Redis 的值分别是1, 2, 3, 4, 5,然后步长都是 5。各个 Redis 生成的 ID 为:
A:1, 6, 11, 16, 21
B:2, 7, 12, 17, 22
C:3, 8, 13, 18, 23
D:4, 9, 14, 19, 24
E:5, 10, 15, 20, 25

随便负载到哪个机确定好,未来很难做修改。步长和初始值一定需要事先确定。使用 Redis 集群也可以防止单点故障的问题。另外,比较适合使用 Redis 来生成每天从0开始的流水号。比如订单号 = 日期 + 当日自增长号。可以每天在 Redis 中生成一个 Key ,使用 INCR 进行累加。

5. Twitter的snowflake算法

Twitter开源的snowflake,以​时间戳+机器+递增序列​组成,基本趋势递增,且性能很高。

snowflake生成的是一个Long类型的值,Long类型的数据占用8个 字节,也就是64位。SnowFlake将64进行拆分,每个部分具有不同 的含义,当然机器码、序列号的位数可以自定义也可以。

把64-bit分别划分成多段,分开来标示机器、时间等,比如在snowflake中的64-bit分别表示如下图(图片来自网络)所示:

  • 符号位 (1bit):预留的符号位,恒为零。(由于 long 类型在 java 中带符号的,最高位为符号位,正数为 0,负数为 1,且实际系统中所使用的ID一般都是正数,所以最高位为 0)
  • 时间戳位 (41bit):41 位的时间戳可以容纳的毫秒数是 2 的 41 次幂,一年所使用的毫秒数是:365 * 24 * 60 * 60 * 1000。通过计算可知:Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L);结果约等于 69.73 年

ShardingSphere 的雪花算法的时间纪元从 2016 年 11 月 1 日零点开始,可以使用到 2086 年,相信能满足绝大部分系统的要求。

  • 工作进程位 (10bit):该标志在 Java 进程内是唯一的,如果是分布式应用部署应保证每个工作进程的 id 是不同的。该值默认为 0,可通过属性设置。10-bit机器可以分别表示1024台机器。如果我们对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,可以根据自身需求定义。
  • 序列号位 (12bit):该序列是用来在同一个毫秒内生成不同的 ID。如果在这个毫秒内生成的数量超过 4096 (2 的 12 次幂),那么生成器会等待到下个毫秒继续生成。这 12 位计数支持每个节点每毫秒(同一台机器,同一时刻)最多生成 1 << 12 = 4096个ID

优点:本地生成,不依赖中间件。 生成的分布式id足够小,只有8个字节,而且是递增的

  • 能满足高并发分布式系统环境下ID不重复
  • 生成效率高
  • 基于时间戳,可以保证基本有序递增
  • 不依赖于第三方的库或者中间件
  • 生成的id具有时序性和唯一性

缺点:时钟回拨问题,强烈依赖于服务器的时间,如果时间出现时间回拨 就可能出现重复的id

6. 百度UidGenerator

具体可以参考官网说明:https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md

7. 美团Leaf

Leaf 是美团开源的分布式ID生成器,能保证全局唯一性、趋势递增、单调递增、信息安全,里面也提到了几种分布式方案的对比,但也需要依赖关系数据库、ZooKeeper等中间件。

https://github.com/Meituan-Dianping/Leaf/tree/master/leaf-core

具体可以参考官网说明:https://tech.meituan.com/2017/04/21/mt-leaf.html

8.滴滴(Tinyid)

Tinyid由滴滴开发,Github地址:https://github.com/didi/tinyid。
Tinyid是基于号段模式原理实现的与Leaf如出一辙,每个服务获取一个号段(1000,2000]、(2000,3000]、(3000,4000]

在这里插入图片描述

说了这么多,我们今天就主要说一下 Twitter开源的snowflake

三、snowflake

1.流程

2.java 代码实现

 如下示例,41bit给时间戳,5bit给IDC,5bit给工作机器,12bit给序列号,代码中是写死的,如果某些bit需要动态调整,可在成员属性定义。计算过程需要一些位运算基础。

import java.util.Date;/*** @Author allen* @Description TODO* @Date 2023-07-26 9:51* @Version 1.0*/
public class SnowFlakeUtil {private static SnowFlakeUtil snowFlakeUtil;static {snowFlakeUtil = new SnowFlakeUtil();}// 初始时间戳(纪年),可用雪花算法服务上线时间戳的值// 1650789964886:2022-04-24 16:45:59private static final long INIT_EPOCH = 1650789964886L;// 时间位取&private static final long TIME_BIT = 0b1111111111111111111111111111111111111111110000000000000000000000L;// 记录最后使用的毫秒时间戳,主要用于判断是否同一毫秒,以及用于服务器时钟回拨判断private long lastTimeMillis = -1L;// dataCenterId占用的位数private static final long DATA_CENTER_ID_BITS = 5L;// dataCenterId占用5个比特位,最大值31// 0000000000000000000000000000000000000000000000000000000000011111private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);// dataCenterIdprivate long dataCenterId;// workId占用的位数private static final long WORKER_ID_BITS = 5L;// workId占用5个比特位,最大值31// 0000000000000000000000000000000000000000000000000000000000011111private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);// workIdprivate long workerId;// 最后12位,代表每毫秒内可产生最大序列号,即 2^12 - 1 = 4095private static final long SEQUENCE_BITS = 12L;// 掩码(最低12位为1,高位都为0),主要用于与自增后的序列号进行位与,如果值为0,则代表自增后的序列号超过了4095// 0000000000000000000000000000000000000000000000000000111111111111private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);// 同一毫秒内的最新序号,最大值可为 2^12 - 1 = 4095private long sequence;// workId位需要左移的位数 12private static final long WORK_ID_SHIFT = SEQUENCE_BITS;// dataCenterId位需要左移的位数 12+5private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;// 时间戳需要左移的位数 12+5+5private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;/*** 无参构造*/public SnowFlakeUtil() {this(1, 1);}/*** 有参构造* @param dataCenterId* @param workerId*/public SnowFlakeUtil(long dataCenterId, long workerId) {// 检查dataCenterId的合法值if (dataCenterId < 0 || dataCenterId > MAX_DATA_CENTER_ID) {throw new IllegalArgumentException(String.format("dataCenterId 值必须大于 0 并且小于 %d", MAX_DATA_CENTER_ID));}// 检查workId的合法值if (workerId < 0 || workerId > MAX_WORKER_ID) {throw new IllegalArgumentException(String.format("workId 值必须大于 0 并且小于 %d", MAX_WORKER_ID));}this.workerId = workerId;this.dataCenterId = dataCenterId;}/*** 获取唯一ID* @return*/public static Long getSnowFlakeId() {return snowFlakeUtil.nextId();}/*** 通过雪花算法生成下一个id,注意这里使用synchronized同步* @return 唯一id*/public synchronized long nextId() {long currentTimeMillis = System.currentTimeMillis();//System.out.println(currentTimeMillis);// 当前时间小于上一次生成id使用的时间,可能出现服务器时钟回拨问题if (currentTimeMillis < lastTimeMillis) {throw new RuntimeException(String.format("可能出现服务器时钟回拨问题,请检查服务器时间。当前服务器时间戳:%d,上一次使用时间戳:%d", currentTimeMillis,lastTimeMillis));}if (currentTimeMillis == lastTimeMillis) {// 还是在同一毫秒内,则将序列号递增1,序列号最大值为4095// 序列号的最大值是4095,使用掩码(最低12位为1,高位都为0)进行位与运行后如果值为0,则自增后的序列号超过了4095// 那么就使用新的时间戳sequence = (sequence + 1) & SEQUENCE_MASK;if (sequence == 0) {currentTimeMillis = getNextMillis(lastTimeMillis);}} else { // 不在同一毫秒内,则序列号重新从0开始,序列号最大值为4095sequence = 0;}// 记录最后一次使用的毫秒时间戳lastTimeMillis = currentTimeMillis;// 核心算法,将不同部分的数值移动到指定的位置,然后进行或运行// <<:左移运算符, 1 << 2 即将二进制的 1 扩大 2^2 倍// |:位或运算符, 是把某两个数中, 只要其中一个的某一位为1, 则结果的该位就为1// 优先级:<< > |return// 时间戳部分((currentTimeMillis - INIT_EPOCH) << TIMESTAMP_SHIFT)// 数据中心部分| (dataCenterId << DATA_CENTER_ID_SHIFT)// 机器表示部分| (workerId << WORK_ID_SHIFT)// 序列号部分| sequence;}/*** 获取指定时间戳的接下来的时间戳,也可以说是下一毫秒* @param lastTimeMillis 指定毫秒时间戳* @return 时间戳*/private long getNextMillis(long lastTimeMillis) {long currentTimeMillis = System.currentTimeMillis();while (currentTimeMillis <= lastTimeMillis) {currentTimeMillis = System.currentTimeMillis();}return currentTimeMillis;}/*** 获取随机字符串,length=13* @return*/public static String getRandomStr() {return Long.toString(getSnowFlakeId(), Character.MAX_RADIX);}/*** 从ID中获取时间* @param id 由此类生成的ID* @return*/public static Date getTimeBySnowFlakeId(long id) {return new Date(((TIME_BIT & id) >> 22) + INIT_EPOCH);}public static void main(String[] args) {SnowFlakeUtil snowFlakeUtil = new SnowFlakeUtil();long id = snowFlakeUtil.nextId();System.out.println(id);Date date = SnowFlakeUtil.getTimeBySnowFlakeId(id);System.out.println(date);long time = date.getTime();System.out.println(time);System.out.println(getRandomStr());/*        System.out.println("============================");long startTime = System.currentTimeMillis();for (int i = 0; i < 10000000; i++) {long id2 = snowFlakeUtil.nextId();System.out.println(id2);}System.out.println(System.currentTimeMillis() - startTime);*/}}

主要就这个: long id = snowFlakeUtil.nextId();

 时钟同步问题解决方案:

雪花算法-Java实现-解决时钟回拨的一种方法_雪花算法时钟回拨_fierys的博客-CSDN博客

这个代码是借鉴另外一位博主的方案,可以借鉴一下,仅供参考

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;/**相较于标准算法,加入了时钟回拨解决方法,仅单机研究,仅个人思考,仅供参考*/
public class SnowFlow {//因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。//机器ID  2进制5位  32位减掉1位 31个private long workerId;//机房ID 2进制5位  32位减掉1位 31个private long datacenterId;//代表一毫秒内生成的多个id的最新序号  12位 4096 -1 = 4095 个private long sequence;//设置一个时间初始值    2^41 - 1   差不多可以用69年private long twepoch = 1420041600000L;//5位的机器idprivate long workerIdBits = 5L;//5位的机房id;。‘private long datacenterIdBits = 5L;//每毫秒内产生的id数 2 的 12次方private long sequenceBits = 12L;// 这个是二进制运算,就是5 bit最多只能有31个数字,也就是说机器id最多只能是32以内private long maxWorkerId = -1L ^ (-1L << workerIdBits);// 这个是一个意思,就是5 bit最多只能有31个数字,机房id最多只能是32以内private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);private long workerIdShift = sequenceBits;private long datacenterIdShift = sequenceBits + workerIdBits;private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;// -1L 二进制就是1111 1111  为什么?// -1 左移12位就是 1111  1111 0000 0000 0000 0000// 异或  相同为0 ,不同为1// 1111  1111  0000  0000  0000  0000// ^// 1111  1111  1111  1111  1111  1111// 0000 0000 1111 1111 1111 1111 换算成10进制就是4095private long sequenceMask = -1L ^ (-1L << sequenceBits);//记录产生时间毫秒数,判断是否是同1毫秒private long lastTimestamp = -1L;public long getWorkerId(){return workerId;}public long getDatacenterId() {return datacenterId;}public long getTimestamp() {return System.currentTimeMillis();}//是否发生了时钟回拨private boolean isBackwordsFlag = false;//是否是第一次发生时钟回拨, 用于设置时钟回拨时间点private boolean isFirstBackwordsFlag = true;//记录时钟回拨发生时间点, 用于判断回拨后的时间达到回拨时间点时, 跳过 已经用过的 时钟回拨发生时间点 之后的时间 到 未来时间的当前时间点private long backBaseTimestamp = -1L;public SnowFlow() {}public SnowFlow(long workerId, long datacenterId, long sequence) {// 检查机房id和机器id是否超过31 不能小于0if (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));}this.workerId = workerId;this.datacenterId = datacenterId;this.sequence = sequence;}// 这个是核心方法,通过调用nextId()方法,// 让当前这台机器上的snowflake算法程序生成一个全局唯一的idpublic synchronized long nextId() {// 这儿就是获取当前时间戳,单位是毫秒long timestamp = timeGen();//--20220813--1---------------------------------------if (isBackwordsFlag) {//当回拨时间再次叨叨回拨时间点时, 跳过回拨这段时间里已经使用了的未来时间if (timestamp >= backBaseTimestamp && timestamp < lastTimestamp) {//直接将当前时间设置为最新的未来时间timestamp = lastTimestamp;} else if(timestamp > lastTimestamp) {//当前时间已经大于上次时间, 重置时钟回拨标志isBackwordsFlag = false;isFirstBackwordsFlag = true;System.out.println("时间已恢复正常-->" + timestamp);} else {// timestamp == lastTimestamp 等于的情况在后面}}//--20220813--1----------------------------------------// 判断是否小于上次时间戳,如果小于的话,就抛出异常if (timestamp < lastTimestamp) {System.err.printf("lastTimestamp=%d, timestamp=%d, l-t=%d \n", lastTimestamp, timestamp, lastTimestamp - timestamp);
//            throw new RuntimeException(
//                    String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
//                            lastTimestamp - timestamp));//--20220813--2---------------------------------------//这里不再抛出异常, 改为记录时钟回拨发生时间点//发生时钟回拨后, 当前时间 timestamp 就变成了 过去的时间//此时将 timestamp 设置为 上一次时间, 即相对于当前时间的未来时间timestamp = lastTimestamp;isBackwordsFlag = true;//记录时钟回拨发生的时间点, 后续需要跳过已经使用的未来时间段if (isFirstBackwordsFlag) {backBaseTimestamp = timestamp;isFirstBackwordsFlag = false;System.out.println("时钟回拨已发生-->" + backBaseTimestamp);}//--20220813--2---------------------------------------}// 下面是说假设在同一个毫秒内,又发送了一个请求生成一个id// 这个时候就得把seqence序号给递增1,最多就是4096if (timestamp == lastTimestamp) {// 这个意思是说一个毫秒内最多只能有4096个数字,无论你传递多少进来,//这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围sequence = (sequence + 1) & sequenceMask;//当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生IDif (sequence == 0) {//timestamp = tilNextMillis(lastTimestamp);//--20220813--3---------------------------------------//这里也不能阻塞了, 因为阻塞方法中需要用到当前时间, 改为将此时代表未来的时间 加 1if (isBackwordsFlag) {lastTimestamp++;//根据博友评论反馈, 这里可能需要重新赋值, 如果有人看到这个, 可以验证//timestamp = lastTimestamp++;} else {timestamp = tilNextMillis(lastTimestamp);}//--20220813--3---------------------------------------}} else {//sequence = 0;//每毫秒的序列号都从0开始的话,会导致没有竞争情况返回的都是偶数。解决方法是用时间戳&1,这样就会随机得到1或者0。sequence = timestamp & 1;}// 这儿记录一下最近一次生成id的时间戳,单位是毫秒//lastTimestamp = timestamp;//--20220813--4---------------------------------------if(isBackwordsFlag) {//什么都不做} else {lastTimestamp = timestamp;}//--20220813--4---------------------------------------// 这儿就是最核心的二进制位运算操作,生成一个64bit的id// 先将当前时间戳左移,放到41 bit那儿;将机房id左移放到5 bit那儿;将机器id左移放到5 bit那儿;将序号放最后12 bit// 最后拼接起来成一个64 bit的二进制数字,转换成10进制就是个long型long sn = ((timestamp - twepoch) << timestampLeftShift) |(datacenterId << datacenterIdShift) |(workerId << workerIdShift) | sequence;if (isBackwordsFlag) {System.out.printf("sn=%d\n", sn);}return sn;}/*** 当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID* @param lastTimestamp* @return*/private long tilNextMillis(long lastTimestamp) {long timestamp = timeGen();while (timestamp <= lastTimestamp) {timestamp = timeGen();}return timestamp;}//获取当前时间戳private long timeGen(){return System.currentTimeMillis();}/***  main 测试类* @param args*/public static void main(String[] args) throws IOException, InterruptedException {SnowFlow snowFlow = new SnowFlow(1, 1, 1);int count = 10000000;//int count = 100;for (int i = 0; i < count; i++) {//实际测试发现遍历太快输出日志过多导致卡顿, 增加睡眠时间, 或输出到文件snowFlow.nextId();//Thread.sleep(100);//            System.out.println(snowFlow.nextId());//            if (i == 1000) {//不具有管理员权限的用户, 修改不成功//testClockMvoedBackwords(30);
//            }//改为 手动修改,  右键cmd,以管理员权限打开后,使用time命令即可, time 16:15:00}System.out.println(System.currentTimeMillis());/*** 这里为什么特意输出一个开始时间呢, 其实就是一个运行了两年的程序突然有一天出bug了,导致了严重的生产事件!* 那么时间初始化影响什么呢, 答案是 序列的长度* 有人就说了, 这个一般是作为 主键用的, 长度貌似影响不大, 确实是这样的* 这次的bug不是雪花算法本身的问题, 而是程序里面有个功能是严格长度截取的, 并且只考虑了长度不够的情况, 没有考虑到变长的情况* 最根本的原因是 本人截取的时候 序列的长度一直是18位, 然后截取9位的代码是这么写的 substring(9);* 当未来的某一天序列长度增加到了19位,那么这个截取就会返回10位长度, 最终导致一个大范围的交易失败......* 锅当然是本人背, 这里提出这种情况, 供大家参考.* 经过仔细研究所谓的序列可以使用69年, 序列的长度变化是这样的, 假设以当前时间为初始化值* 12 13 14 15 16 17 18(约7年) 19(约58年)* 长度随时间增加, 长度越长, 保持相同长度的时间越长*/DateTimeFormatter dtf2 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");String dateString = "2015-01-01 00:00:00";LocalDateTime localDateTime = LocalDateTime.parse(dateString,dtf2);System.out.println(localDateTime.toInstant(ZoneOffset.ofHours(8)).toEpochMilli());}//windows os 模拟时钟回拨, 将当前时间减去几秒private static void testClockMvoedBackwords(long seconds) throws IOException {System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));LocalDateTime localDateTime = LocalDateTime.now();String backTime = localDateTime.minusSeconds(seconds).format(DateTimeFormatter.ofPattern("HH:mm:ss"));System.out.println(backTime);if (System.getProperty("os.name").contains("Windows")) {String cmd = "cmd /c start time 15:41:56";// + backTime;//不具有管理员权限的用户, 修改不生效, 提示 客户端无所需特权Runtime.getRuntime().exec(cmd);
//            Runtime.getRuntime().exec("cmd /c notepad");System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));}}
}

3.Snowflake生产方案 时钟回拨问题解决思路

第一种办法,就是关闭时钟同步,避免产生时钟同步问题,不过这个不太现实,因为强依赖时间的系统,一般都得做时钟同步,避免时间严重错误,在虚拟机上部署一些东西,玩儿虚拟机,休眠,再次恢复之后往往虚拟机里的时间和宿主机的时间是不同步的导致一些大数据的分布式系统会崩溃掉,节点之间通信会依赖时间戳进行比对,心跳过慢,就会导致节点挂掉


第二种办法,记录下来上一次生成ID的时间,如果发现本次生成ID的时候,时间戳小于上次的时间戳,说明时钟回拨了,此时就这个时间内不允许生成ID,一直等,等待到当前时间追上上一次生成时间,问题在于,万一回拨的时间太多了呢?可能要等很久,影响了系统的可用性,所以也不是特别好的办法内存里可以存上一次生成唯一ID的时间戳,时钟回拨了,把当前时间戳会回拨到上次时间戳之前,请求过来,要生成唯一ID,你不要直接就返回一个ID给他,你先做一个比较,如果你发现当前时间戳跟上一次生成唯一ID的时间戳想比,比他小判定,时钟回拨,只要你生成ID,就有可能会发生ID的重复可用性这么差的话,人家业务服务万一此时是要生成一个账单数据,申请一个ID,此时你好不容易等待了几百毫秒之后,你还告诉他你内部异常,没法获取到唯一ID,反复的重试,你会影响他的业务服务的运行。
 

第三种办法,针对第二种办法的优化,如果发现时钟回拨太狠了,比如超过了1分钟,此时直接就报警,同时不再对外提供服务,把自己从集群里摘了,比如你要是基于微服务注册中心进行注册的,就得主动做一个下线当你发现当前时间戳比上一次生成ID的时间戳要小,发现时钟回拨了,判断一下,回拨了多少毫秒,比如说回拨时间在500ms以内,此时可以hang住请求,等待500ms,等到500ms之后,当前时间戳比上一次生成ID的时间戳要大了
此时就可以正常生成唯一ID返回给业务方了,对于业务方而言,仅仅是在个别少数的时钟回拨的情况之下,请求平时只要50ms,500ms,还在接受范围之内,所以说还是可以的,只不过请求慢了一些
如果你要是发现你当前时间戳和上一次生成唯一ID的时间戳想比,你一比较,就发现超过了500ms了,超过了500ms了,但是在5s之内,此时你可以返回一个异常状态+异常持续时间给客户端,不要说有问题,可以通知他自行进行重试
重试机制,最好不要让业务方自己去做,你完全可以去封装一个你的唯一ID生成服务的客户端,基于RPC请求你的接口,但是你在自己的客户端里封装一个自动重试的机制,他一旦发现某台服务器返回的响应说自己短时间内没法提供服务,他自动就去请求其他机器上的服务获取唯一ID了
如果要解决时钟回拨,一般是第二种和第三种结合在一起来用,但是被动等待甚至主动下线,总是影响系统可用性的,都不是特别好
服务端的时钟回拨检测机制 + 客户端自己封装
1s以内:阻塞请求等待,客户端的超时时间,应该也是1s,暴露1s内每一毫秒生成过的唯一ID最大的序号,根据当前时间戳的毫秒,定位到之前生成过ID的这一毫秒的最大ID序号,此时继续生成ID,直接在之前生成过的这一毫秒的最大ID序号基础上递增就可以了,优化之后,就可以保证不需要阻塞等待
1s~10s之间:返回异常码和异常持续时间,客户端在指定时间内不请求这台机器
10s以上:返回故障码,请求服务注册中心让自己下线,客户端收到故障码之后,就直接把这个机器从服务机器列表里剔除掉,不请求他了,后续等到那台机器部署的ID服务他发现自己的时间可能过了几秒钟,缓过来了,恢复了,可用了,就可以再次进行服务注册,你客户端刷新服务注册列表的时候,就会发现他,此时可以再次去请求他
 

第四种办法,要在内存里维护最近几秒内生成的ID值,一般时钟回拨都是几十毫秒到几百毫秒,很少会超过秒的,所以保存最近几秒的就行了,然后如果发生了时钟回拨,此时就看看回拨到了哪一毫秒,因为时间戳是毫秒级的,接着就看那一毫秒
从那一毫秒生产过的ID序号往后继续生成就可以了,后续每一毫秒都是依次类推,这样就可以完美避免重复问题,还不用等待
但是这里也得做一个兜底机制,就是比如你保留最近10s内每一毫秒生成的ID,那么万一时钟回拨碰巧超过了10s呢?此时这种概率很低,你可以跟二三两个方案结合,设置几个阈值,比如说,你保留最近10s的ID,回拨10s内都可以保证不重复,不停顿;如果超过10s,在60s内,可以有一个等待的过程,让他时间前进到你之前保留过的10s范围内去;如果回拨超过了60s,直接下线
上一次生成唯一ID的时间戳也没了,最近1s内每一毫秒的最大ID序号也没了,重启之后,出现了时间回拨,发现不了时间回拨问题,其次也没有办法继续按照之前的思路去生成不重复的唯一ID了

4.时钟回拨优化

1、我们一般需要打开时钟同步功能,这样ID才能够最大化的保证按照时间有序,但是时钟同步打开后,就可能会时钟回拨了,如果时钟回拨了,那么生成的ID就会重复,为此我们一般打开时钟同步的同时关闭时钟回拨功能;
2、序列号的位数有限,能表示的ID个数有限,时钟同步的时候,如果某台服务器快了很多,虽然关闭了时钟回拨,但是在时间追赶上前,ID可能已经用完,当自增序列号用完了,我们可以做如下的工作:停止ID生成服务并告警、如果时钟回拨小于一定的阈值则等待、如大于一定的阈值则通过第三方组件如ZK重新生成一个workerid或者自增时间戳借用下一个时间戳的ID;
3、服务重启后,ID可能会重复,为此我们一般需要定期保存时间戳,重启后的时间戳必须大于保存的时间戳+几倍保存间隔时间(如3倍),为什么要几倍呢,主要是考虑到数据丢失的情况,但是如果保存到本地硬盘且每次保存都fsync,此时1倍即可。重启后如果小于可以像第二点那样类似处理;
4、如果请求ID的QPS不高,比如每毫秒一个,那么每次获取的ID的尾号都是0,那么基于ID做分库分表,可能数据分布就会不均,此时我们可以增加时间戳的时间间隔或者序列号每次从随机一个值开始自增。

主要是生成id的这个方法,我把这个再优化一下,这个是根据美团leaf做了进一步优化,请参考

    /*** 通过雪花算法生成下一个id,注意这里使用synchronized同步* @return 唯一id*/public synchronized long nextId() {long currentTimeMillis = System.currentTimeMillis();//System.out.println(currentTimeMillis);// 当前时间小于上一次生成id使用的时间,可能出现服务器时钟回拨问题//long timestamp = timeGen();if (currentTimeMillis < lastTimeMillis) {long offset = lastTimeMillis - currentTimeMillis;if (offset <= 5) {try {wait(offset << 1);currentTimeMillis = timeGen();if (currentTimeMillis < lastTimeMillis) {throw new RuntimeException("id生成失败");}} catch (InterruptedException e) {throw new RuntimeException("生成id时出现错误");}} else {throw new RuntimeException(String.format("可能出现服务器时钟回拨问题,请检查服务器时间。当前服务器时间戳:%d,上一次使用时间戳:%d", currentTimeMillis,lastTimeMillis));}}/*if (currentTimeMillis < lastTimeMillis) {throw new RuntimeException(String.format("可能出现服务器时钟回拨问题,请检查服务器时间。当前服务器时间戳:%d,上一次使用时间戳:%d", currentTimeMillis,lastTimeMillis));}*/if (currentTimeMillis == lastTimeMillis) {// 还是在同一毫秒内,则将序列号递增1,序列号最大值为4095// 序列号的最大值是4095,使用掩码(最低12位为1,高位都为0)进行位与运行后如果值为0,则自增后的序列号超过了4095// 那么就使用新的时间戳sequence = (sequence + 1) & SEQUENCE_MASK;if (sequence == 0) {currentTimeMillis = getNextMillis(lastTimeMillis);}} else { // 不在同一毫秒内,则序列号重新从0开始,序列号最大值为4095sequence = 0;}// 记录最后一次使用的毫秒时间戳lastTimeMillis = currentTimeMillis;// 核心算法,将不同部分的数值移动到指定的位置,然后进行或运行// <<:左移运算符, 1 << 2 即将二进制的 1 扩大 2^2 倍// |:位或运算符, 是把某两个数中, 只要其中一个的某一位为1, 则结果的该位就为1// 优先级:<< > |return// 时间戳部分((currentTimeMillis - INIT_EPOCH) << TIMESTAMP_SHIFT)// 数据中心部分| (dataCenterId << DATA_CENTER_ID_SHIFT)// 机器表示部分| (workerId << WORK_ID_SHIFT)// 序列号部分| sequence;}

【分布式】分布式唯一 ID 的 8 种生成方案

SnowFlake 雪花算法详解与实现 - 掘金

雪花算法(SnowFlake)_文丑颜不良啊的博客-CSDN博客

分布式唯一Id(雪花算法),原理+对比+方案 - 简书

雪花算法snowflake分布式id生成原理详解,以及对解决时钟回拨问题几种方案讨论_51CTO博客_snowflake 分布式id

Leaf——美团点评分布式ID生成系统 - 美团技术团队

 https://github.com/Meituan-Dianping/Leaf/tree/master/leaf-core

Snowflake生产方案 时钟回拨问题解决思路_启动报错 snowflake 时钟回拨解决方法_都是底层的博客-CSDN博客

相关文章:

【分布式】分布式唯一 ID 的 几种生成方案以及优缺点snowflake优化方案

在互联网的业务系统中&#xff0c;涉及到各种各样的ID&#xff0c;如在支付系统中就会有支付ID、退款ID等。那一般生成ID都有哪些解决方案呢&#xff1f;特别是在复杂的分布式系统业务场景中&#xff0c;我们应该采用哪种适合自己的解决方案是十分重要的。下面我们一一来列举一…...

FFmpeg5.0源码阅读——av_interleaved_write_frame

摘要&#xff1a;本文主要详细描述FFmpeg中封装时写packet到媒体文件的函数av_interleaved_write_frame的实现。   关键字&#xff1a;av_interleaved_write_frame   读者须知&#xff1a;读者需要熟悉ffmpeg的基本使用。 1 基本调用流程 av_interleaved_write_frame的基本…...

力扣 70. 爬楼梯

题目来源&#xff1a;https://leetcode.cn/problems/climbing-stairs/description/ C题解&#xff08;来源代码随想录&#xff09;&#xff1a; 本质上是一道斐波那契数题。 动规五部曲&#xff1a;定义一个一维数组来记录不同楼层的状态 确定dp数组以及下标的含义。dp[i]&am…...

AVFoundation - 媒体捕捉

文章目录 注意使用 NSCameraUsageDescriptioniOS 的摄像头可能比 Mac 更多功能特性@interface Capture ()<AVCaptureFileOutputRecordingDelegate>@property (strong, nonatomic) AVCaptureSession *captureSession; @property (weak, nonatomic) AVCaptureDeviceInput *…...

【新版系统架构补充】-嵌入式技术

嵌入式微处理体系结构 冯诺依曼结构 传统计算机采用冯诺依曼结构&#xff0c;也称普林斯顿结构&#xff0c;是一种将程序指令存储器和数据存储器合并在一起的存储器结构 冯诺依曼的计算机程序和数据共用一个存储空间&#xff0c;程序指令存储地址和数据存储地址指向同一个存…...

fpga开发--蜂鸣器发出连续不同的音调

描述 使用fpga蜂鸣器连续发出do&#xff0c;re&#xff0c;mi&#xff0c;fa&#xff0c;so&#xff0c;la&#xff0c;xi七个不同的音调&#xff0c;每个音调的持续时间为0.5s。 思路 采用状态机实现音调的转化&#xff0c;当do状态持续了0.5s之后转移到re状态&#xff0c;…...

Redis 主从同步原理

一、什么是主从同步&#xff1f; 主从同步&#xff0c;就是将数据冗余备份&#xff0c;主库&#xff08;Master&#xff09;将自己库中的数据&#xff0c;同步给从库&#xff08;Slave&#xff09;。 从库可以一个&#xff0c;也可以多个&#xff0c;如图所示&#xff1a; 二…...

opencv-28 自适应阈值处理-cv2.adaptiveThreshold()

什么是自适应阈值处理? 对于色彩均衡的图像&#xff0c;直接使用一个阈值就能完成对图像的阈值化处理。但是&#xff0c;有时图像的色彩是不均衡的&#xff0c;此时如果只使用一个阈值&#xff0c;就无法得到清晰有效的阈值分割结果图像。 有一种改进的阈值处理技术&#xff…...

Java泛型5——泛型通配符

注&#xff1a;以下内容基于Java 8&#xff0c;所有代码都已在Java 8环境下测试通过 目录&#xff1a; Java泛型1——概述Java泛型2——泛型类Java泛型3——泛型接口Java泛型4——泛型方法Java泛型5——泛型通配符Java泛型6——类型擦除 什么是通配符 在Java中&#xff0c;类…...

牛客 AB25 ranko的手表 JAVA 枚举

描述 ranko 的手表坏了&#xff0c;正常应该显示 xx:xx 的形式&#xff08;4 个数字&#xff09;&#xff0c;比如下午 1 点半应该显示 13:30 &#xff0c;但现在经常会有一些数字有概率无法显示。 ranko 在 &#xfffd;1t1​ 时刻看了下时间&#xff0c;过了一段时间在 &am…...

常微分方程建模R包ecode(二)——绘制相速矢量场

本节中我们考虑一个更为复杂的常微分方程模型&#xff0c; d X C d t ν ( X A Y A ) − β ⋅ X C ⋅ ( Y C Y A ) − ( μ g ) ⋅ X C , ( 1 ) d Y C d t β ⋅ X C ⋅ ( Y C Y A ) − ( μ g ρ ) ⋅ Y C , ( 2 ) d X A d t g ⋅ X C − β ⋅ X A ⋅ ( Y C Y A …...

学习C#编写上位机的基础知识和入门步骤:

00001. 掌握C#编程语言基础和.NET框架的使用。 00002. 学习WinForm窗体应用程序开发技术&#xff0c;包括控件的使用和事件驱动编程。 00003. 熟悉基本的数据结构和算法知识&#xff0c;如链表、栈、队列等。 00004. 理解串口通信协议和通信方法&#xff0c;用于与底层硬件设…...

简单高效!低代码搭建销售自动化程序的方法与实践

在当今数字化时代&#xff0c;销售自动化成为了提高销售效率和业绩的重要手段之一。而低代码平台的兴起&#xff0c;使得搭建销售自动化程序变得更加简单和高效。本文将介绍低代码平台及其优势&#xff0c;并探讨如何利用低代码平台搭建销售自动化程序。 1、低代码平台 1&…...

第九十三回 在Flutter中mock数据

文章目录 概念介绍使用方法示例代码 我们在上一章回中介绍了"在Flutter中解析JSON数据"相关的内容&#xff0c;本章回中将介绍 如何mock数据.闲话休提&#xff0c;让我们一起Talk Flutter吧。 概念介绍 我们在本章回中介绍的mock数据主要是通过相关的代码模拟服务器…...

进程与线程的区别与联系

多进程已经可以很好的实现并发编程的效果了&#xff0c;但是仍然有一个明显的缺点&#xff1a;进程太重了&#xff0c;进程消耗的资源更多&#xff0c;速度更慢。如果进程创建销毁不频繁&#xff0c;那么还好&#xff0c;一旦需要大规模创建和销毁进程&#xff0c;开销就比较大…...

使用gadl对土地利用栅格重分类

要使用Python语言进行土地利用栅格的重分类&#xff0c;可以使用gadl库&#xff08;GDAL的Python绑定&#xff09;来实现。gadl库提供了一组功能强大的函数和类&#xff0c;可用于读取、处理和分析栅格数据。 首先&#xff0c;确保已经安装了gadl库。可以使用以下命令通过pip进…...

SQL-每日一题【1141. 查询近30天活跃用户数】

题目 活动记录表&#xff1a;Activity 请写SQL查询出截至 2019-07-27&#xff08;包含2019-07-27&#xff09;&#xff0c;近 30 天的每日活跃用户数&#xff08;当天只要有一条活动记录&#xff0c;即为活跃用户&#xff09;。 以 任意顺序 返回结果表。 查询结果示例如下。…...

Java小型操作系统模拟(采用策略模式结合反射进行搭建,支持一些简单的命令)

Java小型操作系统模拟 项目说明第一阶段&#xff1a;反射结合策略模式搭建基本的命令结构第二阶段&#xff1a;注解结合反射与策略模式&#xff0c;将结构进一步规范第三阶段&#xff1a;开启新的窗口&#xff0c;将控制台输入切换到新窗口中&#xff0c;同时创建右键菜单&…...

VsCode与Idea编辑器更换背景图

目录 VsCode Idea VsCode 需要安装background插件 安装完成后&#xff0c;打开设置&#xff0c;搜索background 然后就可以在json文件进行图片设置&#xff0c;透明度等等 Idea 打开File -> Settings 然后找到Appearance &#xff0c; 往下滑&#xff0c;找到BackGround …...

Visual Studio 快捷键

记录一下VS的快捷键,用Xcode几个星期后回到VS一下子有点乱,还好有条件反射在,过了会就都恢复了 目录 跳转快捷键查找快捷键编辑快捷键代码折叠书签操作记忆来源VS一定要装VAssistX插件,下面的快捷键部分是VX提供的。 跳转快捷键 快速打开文件 Alt + Shift + O 快速打开对…...

IT技术面试中常见的问题及解答技巧

在IT技术面试中&#xff0c;面试官常常会问到一些常见的问题&#xff0c;针对这些问题&#xff0c;我们可以充分准备和提前准备一些解答技巧。下面我将分享一些我个人的经验和观察&#xff0c;希望对大家有所帮助。 请介绍一下你的项目经验。 在回答这个问题时&#xff0c;我们…...

Java使用hive连接kyuubi

一、Maven依赖 <dependency><groupId>org.apache.hive</groupId><artifactId>hive-jdbc</artifactId><version>2.3.9</version> </dependency> 二、相关配置信息 驱动类&#xff1a;org.apache.hive.jdbc.HiveDriver连接UR…...

性能测试基础知识(三)性能指标

性能测试基础知识&#xff08;三&#xff09;性能指标 前言一、时间特性1、响应时间2、并发数3、吞吐量&#xff08;TPS&#xff09; 二、资源特性1、CPU利用率2、内存利用率3、I/O利用率4、网络带宽使用率5、网络传输速率&#xff08;MB/s&#xff09; 三、实例场景 前言 性能…...

【 Redis】的乱码问题

问题描述&#xff1a; 使用RedisTemplate存储的数据&#xff0c;在 redis-cli 客户端查看时&#xff0c;key 和 value 都会携带类似\xac\xad\这样的字符串。 原因&#xff1a; 由于默认使用了 jdk 的序列化方式。以下是支持的序列化方式 项目一般都会有缓存&#xff0c;常常…...

虚拟机安装的问题

CentOS7报错: Host SMBus Controller not enabled! 1.在上图界面中直接输入root用户的密码登录到系统 2.输入命令&#xff0c;lsmod | grep i2c 3.输入命令&#xff0c;vi /etc/modprobe.d/blacklist.conf 创建黑名单&#xff0c;添加以下内容&#xff1a; blacklist i2c_piix…...

seldom之数据驱动

seldom之数据驱动 如果自动化某个功能&#xff0c;测试数据不一样而操作步骤是一样的&#xff0c;那么就可以使用参数化来节省测试代码。 seldom是我在维护一个Web UI自动化测试框&#xff0c;这里跟大家分享seldom参数化的实现。 GitHub&#xff1a;GitHub - SeldomQA/seld…...

设计模式:生成器模式

这个模式书上讲的比较简单&#xff0c;但是感觉精华应该是讲到了。 引用下其它博客的总结&#xff1a;生成器模式的核心在于分离构建算法和具体的构造实现&#xff0c;从而使得构建算法可以重用。 【设计模式】建造者模式_鼠晓的博客-CSDN博客...

Gradle同步任务一直不动问题(非网络情况)

最近更新ComposeViews的Kotlin和Compose版本,升级到Kotlin1.9和Compose1.4.3时遇见一个问题,Gradle同步时始终会卡在一个位置,同步了一晚上也没用 然后又试了两次还是不行,猜测可能是Gradle的问题,于是使用命令行进行同步,并打印debug日志 ./gradlew -debug -refresh-dependen…...

STM32使用HAL库BH1750光照度传感器

开发环境 单片机&#xff1a;STM32F103C8T6 光照度传感器&#xff1a;BH1750 IDE&#xff1a;KEILSTM32CUBEMX 单片机配置 1、STM32CUBEMX BH1750代码 1、头文件 /* ************************************************* BH1750光照数据计算&#xff08;LUX&#xff09; …...

qt代码练习

计时器练习 namespace Ui { class third; }class third : public QWidget {Q_OBJECTpublic:explicit third(QWidget *parent nullptr);~third();QLabel *labth1 new QLabel(this);QTextEdit *txtth1 new QTextEdit("闹钟",this);QLineEdit *leth1 new QLineEdit(t…...