在分布式系统中,有一些需要使用全局唯一 ID 的场景,这种时候为了防止 ID 冲突可以使用 36 位的 UUID,但是 UUID 有一些缺点,首先他相对比较长,另外 UUID 一般是无序的

有些时候我们希望能使用一种简单些的 ID,并且希望 ID 能够按照时间有序生成

什么是雪花算法
Snowflake 中文的意思是雪花,所以常被称为雪花算法,是 Twitter 开源的分布式 ID 生成算法

Twitter 雪花算法生成后是一个 64bit 的 long 型的数值,组成部分引入了时间戳,基本保持了自增

SnowFlake 算法的优点:

高性能高可用:生成时不依赖于数据库,完全在内存中生成
高吞吐:每秒钟能生成数百万的自增 ID
ID 自增:存入数据库中,索引效率高

SnowFlake 算法的缺点:

依赖与系统时间的一致性,如果系统时间被回调,或者改变,可能会造成 ID 冲突或者重复

雪花算法组成
snowflake 结构如下图所示:

包含四个组成部分

不使用:1bit,最高位是符号位,0 表示正,1 表示负,固定为 0

时间戳:41bit,毫秒级的时间戳(41 位的长度可以使用 69 年)

标识位:5bit 数据中心 ID,5bit 工作机器 ID,两个标识位组合起来最多可以支持部署 1024 个节点

序列号:12bit 递增序列号,表示节点毫秒内生成重复,通过序列号表示唯一,12bit 每毫秒可产生 4096 个 ID

通过序列号 1 毫秒可以产生 4096 个不重复 ID,则 1 秒可以生成 4096 * 1000 = 409w ID

默认的雪花算法是 64 bit,具体的长度可以自行配置。如果希望运行更久,增加时间戳的位数;如果需要支持更多节点部署,增加标识位长度;如果并发很高,增加序列号位数

总结:雪花算法并不是一成不变的,可以根据系统内具体场景进行定制

雪花算法适用场景
因为雪花算法有序自增,保障了 MySQL 中 B+ Tree 索引结构插入高性能

所以,日常业务使用中,雪花算法更多是被应用在数据库的主键 ID 和业务关联主键

雪花算法生成 ID 重复问题
具体情况如下:一个分布式服务,通过雪花算法生成 ID,共部署四个节点,标识位一致(都设置为了0)

每天凌晨上游系统跑批调用此服务,此时有 200 并发,均匀散布四个节点,四个节点同一毫秒同一序列号下生成 ID,那么就会产生重复 ID。导致数据库主键冲突。

通过上述假设场景,可以知道雪花算法生成 ID 冲突存在一定的前提条件

服务通过集群的方式部署,其中部分机器标识位一致
业务存在一定的并发量,没有并发量无法触发重复问题
生成 ID 的时机:同一毫秒下的序列号一致

标识位如何定义
如果能保证标识位不重复,那么雪花 ID 也不会重复

通过上面的案例,知道了 ID 重复的必要条件。如果要避免服务内产生重复的 ID,那么就需要从标识位上动文章

我们先看看开源框架中使用雪花算法,如何定义标识位

Mybatis-Plus v3.4.2 雪花算法实现类 Sequence,提供了两种构造方法:无参构造,自动生成 dataCenterId 和 workerId;有参构造,创建 Sequence 时明确指定标识位

Hutool v5.7.9 参照了 Mybatis-Plus dataCenterId 和 workerId 生成方案,提供了默认实现
一起看下 Sequence 的创建默认无参构造,如何生成 dataCenterId 和 workerId

public static long getDataCenterId(long maxDatacenterId) {long id = 1L;final byte[] mac = NetUtil.getLocalHardwareAddress();if (null != mac) {id = ((0x000000FF & (long) mac[mac.length - 2])| (0x0000FF00 & (((long) mac[mac.length - 1]) << 8))) >> 6;id = id % (maxDatacenterId + 1);}return id;
}

入参 maxDatacenterId 是一个固定值,代表数据中心 ID 最大值,默认值 31

为什么最大值要是 31?因为 5bit 的二进制最大是 11111,对应十进制数值 31

获取 dataCenterId 时存在两种情况,一种是网络接口为空,默认取 1L;另一种不为空,通过 Mac 地址获取 dataCenterId

可以得知,dataCenterId 的取值与 Mac 地址有关

接下来再看看 workerId

public static long getWorkerId(long datacenterId, long maxWorkerId) {final StringBuilder mpid = new StringBuilder();mpid.append(datacenterId);try {mpid.append(RuntimeUtil.getPid());} catch (UtilException igonre) {//ignore}return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
}

入参 maxWorkderId 也是一个固定值,代表工作机器 ID 最大值,默认值 31;datacenterId 取自上述的 getDatacenterId 方法

name 变量值为 PID@IP,所以 name 需要根据 @ 分割并获取下标 0,得到 PID

通过 MAC + PID 的 hashcode 获取16个低位,进行运算,最终得到 workerId
分配标识位
Mybatis-Plus 标识位的获取依赖 Mac 地址和进程 PID,虽然能做到尽量不重复,但仍有小几率

标识位如何定义才能不重复?有两种方案:预分配和动态分配

预分配

应用上线前,统计当前服务的节点数,人工去申请标识位

这种方案,没有代码开发量,在服务节点固定或者项目少可以使用,但是解决不了服务节点动态扩容性问题

动态分配

通过将标识位存放在 Redis、Zookeeper、MySQL 等中间件,在服务启动的时候去请求标识位,请求后标识位更新为下一个可用的

通过存放标识位,延伸出一个问题:雪花算法的 ID 是 服务内唯一还是全局唯一

以 Redis 举例,如果要做服务内唯一,存放标识位的 Redis 节点使用自己项目内的就可以;如果是全局唯一,所有使用雪花算法的应用,要用同一个 Redis 节点

两者的区别仅是 不同的服务间是否公用 Redis。如果没有全局唯一的需求,最好使 ID 服务内唯一,因为这样可以避免单点问题

服务的节点数超过 1024,则需要做额外的扩展;可以扩展 10 bit 标识位,或者选择开源分布式 ID 框架

动态分配实现方案

Redis 存储一个 Hash 结构 Key,包含两个键值对:dataCenterId 和 workerId

在应用启动时,通过 Lua 脚本去 Redis 获取标识位。dataCenterId 和 workerId 的获取与自增在 Lua 脚本中完成,调用返回后就是可用的标示位。(Redis+lua 方案,会使获取与自增在同一个事物中完成。)

具体 Lua 脚本逻辑如下:

第一个服务节点在获取时,Redis 可能是没有 snowflake_work_id_key 这个 Hash 的,应该先判断 Hash 是否存在,不存在初始化 Hash,dataCenterId、workerId 初始化为 0
如果 Hash 已存在,判断 dataCenterId、workerId 是否等于最大值 31,满足条件初始化 dataCenterId、workerId 设置为 0 返回
dataCenterId 和 workerId 的排列组合一共是 1024,在进行分配时,先分配 workerId
判断 workerId 是否 != 31,条件成立对 workerId 自增,并返回;如果 workerId = 31,自增 dataCenterId 并将 workerId 设置为 0
dataCenterId、workerId 是一直向下推进的,总体形成一个环状。通过 Lua 脚本的原子性,保证 1024 节点下的雪花算法生成不重复。如果标识位等于 1024,则从头开始继续循环推进

开源分布式 ID 框架
Leaf 和 Uid 都有实现雪花算法,Leaf 额外提供了号段模式生成 ID

美团 Leaf:https://github.com/Meituan-Dianping/Leaf

百度 Uid:https://github.com/baidu/uid-generator

雪花算法可以满足大部分场景,如无必要,不建议引入开源方案增加系统复杂度

解决雪花算法时钟回拨问题

首先, SnowFlake的末尾12位是序列号, 用来记录同一毫秒内产生的不同id, 同一毫秒总共可以产生4096个id, 每一毫秒的序列号都是从0这个基础序列号开始递增

假设我们的业务系统在单机上的QPS为3w/s, 那么其实平均每毫秒只需要产生30个id即可, 远没有达到设计的4096, 也就是说通常情况下序列号的使用都是处在一个低水位, 当发生时钟回拨的时候, 这些尚未被使用的序号就可以派上用场了.

因此, 可以对给定的基础序列号稍加修改, 后面每发生一次时钟回拨就将基础序列号加上指定的步长, 例如开始时是从0递增, 发生一次时钟回拨后从1024开始递增, 再发生一次时钟回拨则从2048递增, 这样还能够满足3次的时钟回拨到同一时间点(发生这种操作就有点扯了).

    /** 步长, 1024 */private static long stepSize = 2 << 9;/** 基础序列号, 每发生一次时钟回拨, basicSequence += stepSize */private long basicSequence = 0L;private long handleMovedBackwards(long currStmp) {basicSequence += stepSize;if (basicSequence == MAX_SEQUENCE + 1) {basicSequence = 0;currStmp = getNextMill();}sequence = basicSequence;lastStmp = currStmp;return (currStmp - START_STMP) << TIMESTMP_LEFT| workId << WORK_LEFT | sequence; }

完整代码如下:

public class SnowFlakeWorker {private volatile static SnowFlakeWorker snowFlakeWorkerInstance;// 1位标识部分    -      41位时间戳部分        -         10位节点部分     12位序列号部分/** 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 *//*** 起始的时间戳*/private final static long START_STMP = 1288834974657L;/*** 每一部分占用的位数*/private final static long SEQUENCE_BIT = 12;  // 序列号占用的位数private final static long WORK_BIT = 10;    // 机器标识占用的位数/*** WORK_NUM最大值 1023*/private final static long MAX_WORK_NUM = -1L ^ (-1L << WORK_BIT);/*** SEQUENCE最大值 4095*/private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);/*** 每一部分向左的位移*/private final static long WORK_LEFT = SEQUENCE_BIT;private final static long TIMESTMP_LEFT = WORK_LEFT + WORK_BIT;private long workId;private long sequence = 0L;  //序列号private long lastStmp = -1L; //上一次时间戳/** 步长, 1024 */private static long stepSize = 2 << 9;/** 基础序列号, 每发生一次时钟回拨即改变, basicSequence += stepSize */private long basicSequence = 0L;private SnowFlakeWorker(long workId) {if (workId > MAX_WORK_NUM || workId < 0) {throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");}this.workId = workId;}protected synchronized static SnowFlakeWorker initSnowFlakeWorker(long workId) {snowFlakeWorkerInstance = new SnowFlakeWorker(workId);return snowFlakeWorkerInstance;}public static SnowFlakeWorker getInstance() {return snowFlakeWorkerInstance;}/*** 产生下一个ID*/public synchronized long nextId() {long currStmp = getNewstmp();if (currStmp < lastStmp) {return handleClockBackwards(currStmp);}if (currStmp == lastStmp) {// 相同毫秒内,序列号自增sequence = (sequence + 1) & MAX_SEQUENCE;// 同一毫秒的序列数已经达到最大if (sequence == 0L) {currStmp = getNextMill();}} else {// 不同毫秒内,序列号置为 basicSequencesequence = basicSequence;}lastStmp = currStmp;return (currStmp - START_STMP) << TIMESTMP_LEFT  // 时间戳部分| workId << WORK_LEFT                    // 节点部分| sequence;                              // 序列号部分}/*** 处理时钟回拨*/private long handleClockBackwards(long currStmp) {basicSequence += stepSize;if (basicSequence == MAX_SEQUENCE + 1) {basicSequence = 0;currStmp = getNextMill();}sequence = basicSequence;lastStmp = currStmp;return (currStmp - START_STMP) << TIMESTMP_LEFT  // 时间戳部分| workId << WORK_LEFT                    // 节点部分| sequence;                              // 序列号部分}private long getNextMill() {long mill = getNewstmp();while (mill <= lastStmp) {mill = getNewstmp();}return mill;}private long getNewstmp() {return System.currentTimeMillis();}}

回顾总结
文章通过图文并茂的方式帮助读者梳理了一遍什么是雪花算法,以及如何解决雪花算法生成 ID 冲突的问题

关于雪环算法生成 ID 冲突问题,文中给了一种方案:分配标示位;通过分配雪花算法的组成标识位,来达到默认 1024 节点下 ID 生成唯一

可以去看 Hutool 或者 Mybatis-Plus 雪花算法的具体实现,帮助大家更好的理解

雪花算法不是万能的,并不能适用于所有场景。如果 ID 要求全局唯一并且服务节点超出 1024 节点,可以选择修改算法本身的组成,即扩展标识位,或者选择开源方案:LEAF、UID

记一次错误使用雪花算法引起的数据库主键冲突和解决时钟回拨问题相关推荐

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

    文章目录 一.前言 二.雪花算法snowflake 1.基本定义 2.snowflake的优缺点 三.Java代码实现snowflake 1.组装生成id 2.计算最大值的几种方式 3.反解析ID 4 ...

  2. 分布式全局唯一ID生成算法(改进的雪花算法——解决时钟回拨问题)

    改进的雪花算法--解决时钟回拨问题 原创 公众号: 软件设计活跃区 改进的雪花算法--姑且称为梨花算法吧(忽如一夜春风来,千树万树梨花开). 改进目标:解决雪花算法的时钟回拨问题:部分避免机器id重复 ...

  3. 雪花算法-Java实现-解决时钟回拨的一种方法

    背景: 前不久发生了一次严重的生产事件, 与雪花算法有关,但不是雪花算法的问题 具体问题参考代码main中的注释, 结论如下 序列可以使用69年, 序列的长度变化是这样的, 假设以当前时间为初始化值 ...

  4. 雪花算法通过顺序号持久化解决时钟回拨

    在雪花算法自定义解决时钟回拨问题一文中,对雪花算法的时钟回拨解决思路进行了说明,由于顺序号保存在内存中,每次启动都是从初始值开始,在特定场景下,比如停止服务后进行了时钟回拨,在理论上,还是可能出现序列 ...

  5. 雪花算法解决时钟回拨问题

    SnowFlake算法 据国家大气研究中心的查尔斯·奈特称,一般的雪花大约由10^19个水分子组成.在雪花形成过程中,会形成不同的结构分支,所以说大自然中不存在两片完全一样的雪花,每一片雪花都拥有自己 ...

  6. 面试题:雪花算法(SnowFlake)如何解决时钟回拨问题

    1. 雪花算法 雪花算法是一种分布式ID生成算法,首先它生产的是一个64bit位的ID,这64bit位中划分成多段: 第1个bit位:保留位,无实际作用 第2-42的bit位:这41位表示时间戳,精确 ...

  7. 雪花算法自定义解决时钟回拨问题

    雪花算法默认算法生成一个 64bit的长整型(Long)数据.主要由 4部分组成,1bit符号位.41bit时间戳位.10bit工作进程位以及 12bit 序列号位. 正常情况下,该算法可以保证系统中 ...

  8. 分布式下使用雪花算法生成全局ID及解决时钟回拨问题

    简介 雪花算法是 64 位 的二进制,一共包含了四部分: 1位是符号位,也就是最高位,始终是0,没有任何意义,因为要是唯一计算机二进制补码中就是负数,0才是正数 41位是时间戳,具体到毫秒,41位的二 ...

  9. 【面向对象】记一次错误的Dijkstra算法优化—动态规划与贪心

    目录 Floyd(弗洛伊德)算法 Dijkstra(迪杰斯特拉)算法 联想:贪心与动态规划--不恰当的贪心导致出错 没有学过算法,请各位大佬们轻拍 本文将简单比较一下图论中最短路的两大最短路算法:Fl ...

最新文章

  1. 【OpenCV 】Remapping 重映射¶
  2. TeX下的Markdown包
  3. 蓝桥杯_算法训练_矩阵乘法
  4. python断点调试从哪里看数据_Python Pdb 断点调试 - 简明教程
  5. php接口前端安全,前端js的ajax 调用PHP写的API接口,如何卡主安全性,防止非法调用呢?...
  6. K-th Beautiful String CodeForces - 1328B(二分+数学)
  7. 音频视频解决方案:GStreamer/ffmpeg/ffdshow/directshow/vfw
  8. linux twm,linux 命令缩写解析
  9. php数据访问层,对数据访问层第一种实现(Acc+SQL)的重构
  10. 腾讯优测-优社区干货精选 |安卓适配之Camera拍照时快门咔嚓声
  11. mysql图片_往MySQL中存储图片的方法
  12. mysql影响行数解析_详解MySQL的数据行和行溢出机制
  13. Android自定义processor实现bindView功能
  14. 根据指定字段排序编号(SQL Server 2005,Update,Order By)
  15. oracle数据库应用(2)
  16. HDFS读写流程以及多节点、单节点磁盘负载均衡
  17. 为什么误差采取平方和形式
  18. 用byte数组表示RGB颜色
  19. SQL SERVER中PERCENTILE_CONT和PERCENTILE_DISC
  20. 【跨域】一篇文章彻底解决跨域设置cookie问题!

热门文章

  1. c语言程序设计证书有没有,从未学习过c语言程序设计,10天考取计算机二级c语言程序设计证书可能吗?...
  2. 51单片机扩展io口C语言编写,51单片机扩展IO口后扩展口的地址如何确定
  3. C/C++内存分区详解
  4. 小红书种草笔记对于品牌宣传的作用(原创图文)
  5. Python_map()函数,规范英文名字
  6. 中纬ZOOM35全站仪参数和使用说明书
  7. PatchMatch Stereo(一)Slunted Windows
  8. 利用鼠标在图像上画框并实时显示鼠标所点击处坐标
  9. 项目8 利用DHCP自动分配IP地址
  10. 基于springboot+vue的便利店库存管理系统