id长度 雪花算法_分布式全局ID雪花算法解决方案(简单防止时钟回拨)

本文介绍了Twitter的雪花算法,它是一种本地生成的全局唯一ID算法,适用于需要高性能、全局有序ID的场景。文章详细阐述了算法的结构和原理,包括时间戳、机器ID和循环位的分配。此外,还讨论了如何处理时钟回拨问题,提出了几种优化策略,以避免在回拨时生成重复ID。
摘要由CSDN通过智能技术生成

1.snowflake(雪花算法)简介

互联网快速发展的今天,分布式应用系统已经见怪不怪,在分布式系统中,我们需要各种各样的ID,既然是ID那么必然是要保证全局唯一,除此之外,不同当业务还需要不同的特性,比如像并发巨大的业务要求ID生成效率高,吞吐大;比如某些银行类业务,需要按每日日期制定交易流水号;又比如我们希望用户的ID是随机的,无序的,纯数字的,且位数长度是小于10位的。等等,不同的业务场景需要的ID特性各不一样,于是,衍生了各种ID生成器,但大多数利用数据库控制ID的生成,性能受数据库并发能力限制,那么有没有一款不需要依赖任何中间件(如数据库,分布式缓存服务等)的ID生成器呢?本着取之于开源,用之于开源的原则,今天,特此介绍Twitter开源的一款分布式自增ID算法snowflake,并附上算法原理推导和演算过程!

snowflake算法是一款本地生成的(ID生成过程不依赖任何中间件,无网络通信),保证ID全局唯一,并且ID总体有序递增,性能每秒生成300w+。

b4f0970f3cb6287a33662b3dfda9ffda.png

1bit:一般是符号位,不做处理

41bit:用来记录时间戳,这里可以记录69年,如果设置好起始时间比如今年是2018年,那么可以用到2089年,到时候怎么办?要是这个系统能用69年,我相信这个系统早都重构了好多次了。

10bit:10bit用来记录机器ID,总共可以记录1024台机器,一般用前5位代表数据中心ID,后面5位是某个数据中心的机器ID

12bit:循环位,用来对同一个毫秒之内产生不同的ID,12位可以最多记录4095个,也就是在同一个机器同一毫秒最多记录4095个,多余的需要进行等待下毫秒。

总体来说,在工作节点达到1024顶配的场景下,SnowFlake算法在同一毫秒内最多可以生成多少个全局唯一ID呢?这是一个简单的乘法:

同一毫秒的ID数量 = 1024 X 4096 = 4194304

400多万个ID,这个数字在绝大多数并发场景下都是够用的。

snowflake 算法中,第三个部分是工作机器ID,可以结合上一节的命名方法,并通过Zookeeper管理workId,免去手动频繁修改集群节点,去配置机器ID的麻烦。

上面只是一个将64bit划分的标准,当然也不一定这么做,可以根据不同业务的具体场景来划分,比如下面给出一个业务场景:

服务目前QPS10万,预计几年之内会发展到百万。

当前机器三地部署,上海,北京,深圳都有。

当前机器10台左右,预计未来会增加至百台。

这个时候我们根据上面的场景可以再次合理的划分62bit,QPS几年之内会发展到百万,那么每毫秒就是千级的请求,目前10台机器那么每台机器承担百级的请求,为了保证扩展,后面的循环位可以限制到1024,也就是2^10,那么循环位10位就足够了。

机器三地部署我们可以用3bit总共8来表示机房位置,当前的机器10台,为了保证扩展到百台那么可以用7bit 128来表示,时间位依然是41bit,那么还剩下64-10-3-7-41-1 = 2bit,还剩下2bit可以用来进行扩展。

适用场景:当我们需要无序不能被猜测的ID,并且需要一定高性能,且需要long型,那么就可以使用我们雪花算法。比如常见的订单ID,用雪花算法别人就发猜测你每天的订单量是多少。

上面定义了雪花算法的实现,在nextId中是我们生成雪花算法的关键。

2.防止时钟回拨

因为机器的原因会发生时间回拨,我们的雪花算法是强依赖我们的时间的,如果时间发生回拨,有可能会生成重复的ID,在我们上面的nextId中我们用当前时间和上一次的时间进行判断,如果当前时间小于上一次的时间那么肯定是发生了回拨,普通的算法会直接抛出异常,这里我们可以对其进行优化,一般分为两个情况:

如果时间回拨时间较短,比如配置5ms以内,那么可以直接等待一定的时间,让机器的时间追上来。

如果时间的回拨时间较长,我们不能接受这么长的阻塞等待,那么又有两个策略:

  1. 直接拒绝,抛出异常,打日志,通知RD时钟回滚。
  2. 利用扩展位,上面我们讨论过不同业务场景位数可能用不到那么多,那么我们可以把扩展位数利用起来了,比如当这个时间回拨比较长的时候,我们可以不需要等待,直接在扩展位加1。2位的扩展位允许我们有3次大的时钟回拨,一般来说就够了,如果其超过三次我们还是选择抛出异常,打日志。

通过上面的几种策略可以比较的防护我们的时钟回拨,防止出现回拨之后大量的异常出现。下面是修改之后的代码,这里修改了时钟回拨的逻辑:

package com.util; import java.lang.management.ManagementFactory;import java.net.InetAddress;import java.net.NetworkInterface;import java.util.*;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.CountDownLatch;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.LockSupport;import org.apache.log4j.Logger;/** * 分布式全局ID雪花算法解决方案 * * 防止时钟回拨 * 因为机器的原因会发生时间回拨,我们的雪花算法是强依赖我们的时间的,如果时间发生回拨, * 有可能会生成重复的ID,在我们上面的nextId中我们用当前时间和上一次的时间进行判断, * 如果当前时间小于上一次的时间那么肯定是发生了回拨, * 普通的算法会直接抛出异常,这里我们可以对其进行优化,一般分为两个情况: * 如果时间回拨时间较短,比如配置5ms以内,那么可以直接等待一定的时间,让机器的时间追上来。 * 如果时间的回拨时间较长,我们不能接受这么长的阻塞等待,那么又有两个策略: * 直接拒绝,抛出异常,打日志,通知RD时钟回滚。 * 利用扩展位,上面我们讨论过不同业务场景位数可能用不到那么多,那么我们可以把扩展位数利用起来了, * 比如当这个时间回拨比较长的时候,我们可以不需要等待,直接在扩展位加1。 * 2位的扩展位允许我们有3次大的时钟回拨,一般来说就够了,如果其超过三次我们还是选择抛出异常,打日志。 * 通过上面的几种策略可以比较的防护我们的时钟回拨,防止出现回拨之后大量的异常出现。下面是修改之后的代码,这里修改了时钟回拨的逻辑: */public class SnowflakeIdWorker { private static Logger log = Logger.getLogger(SnowflakeIdWorker.class);  /** * EPOCH是服务器第一次上线时间点, 设置后不允许修改 * 2018/9/29日,从此时开始计算,可以用到2089年 */ private static long EPOCH = 1538211907857L;  /** * 每台workerId服务器有3个备份workerId, 备份workerId数量越多, 可靠性越高, 但是可部署的sequence ID服务越少 */ private static final long BACKUP_COUNT = 3;  /** * worker id 的bit数,最多支持8192个节点 */ private static final long workerIdBits = 5L; /** * 数据中心标识位数 */ private static final long dataCenterIdBits = 5L; /** * 序列号,支持单节点最高每毫秒的最大ID数4096 * 毫秒内自增位 */ private static final long sequenceBits = 12L;  /** * 机器ID偏左移12位 */ private static final long workerIdShift = sequenceBits;  /** * 数据中心ID左移17位(12+5) */ private static final long dataCenterIdShift = sequenceBits + workerIdBits;  /** * 时间毫秒左移22位(5+5+12) */ private static final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits; /** * sequence掩码,确保sequnce不会超出上限 * 最大的序列号,4096 * -1 的补码(二进制全1)右移12位, 然后取反 * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */ private static final long sequenceMask = -1L ^ (-1L << sequenceBits);  //private final static long sequenceMask = ~(-1L << sequenceBits); /** * 实际的最大workerId的值 结果是31,8091 * workerId原则上上限为1024, 但是需要为每台sequence服务预留BACKUP_AMOUNT个workerId, * (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */ //private static final long maxWorkerId = (1L << workerIdBits) / (BACKUP_COUNT + 1);  //原来代码 -1 的补码(二进制全1)右移13位, 然后取反 private static final long maxWorkerId = -1L ^ (-1L << workerIdBits); //private final static long maxWorkerId = ~(-1L << workerIdBits);  /** * 支持的最大数据标识id,结果是31 */ private static final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits); /** * long workerIdBits = 5L; * -1L 的二进制: 1111111111111111111111111111111111111111111111111111111111111111 * -1L< workerIdLastTimeMap = new ConcurrentHashMap<>();  /** * 最大容忍时间, 单位毫秒, 即如果时钟只是回拨了该变量指定的时间, 那么等待相应的时间即可; * 考虑到sequence服务的高性能, 这个值不易过大 */ private static final long MAX_BACKWARD_MS = 3; private static SnowflakeIdWorker idWorker;  static { idWorker = new SnowflakeIdWorker(); }  static { Calendar calendar = Calendar.getInstance(); calendar.set(2018, Calendar.NOVEMBER, 1); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); // EPOCH是服务器第一次上线时间点, 设置后不允许修改 EPOCH = calendar.getTimeInMillis();  // 初始化workerId和其所有备份workerId与lastTimestamp // 假设workerId为0且BACKUP_AMOUNT为4, 那么map的值为: {0:0L, 256:0L, 512:0L, 768:0L} // 假设workerId为2且BACKUP_AMOUNT为4, 那么map的值为: {2:0L, 258:0L, 514:0L, 770:0L} /* for (int i = 0; i<= BACKUP_COUNT; i++){ workerIdLastTimeMap.put(workerId + (i * maxWorkerId), 0L); }*/ }  //成员类,IdGenUtils的实例对象的保存域 private static class SnowflakeIdGenHolder { private static final SnowflakeIdWorker instance = new SnowflakeIdWorker(); } //外部调用获取IdGenUtils的实例对象,确保不可变 public static SnowflakeIdWorker getInstance(){ return SnowflakeIdGenHolder.instance; }  /** * 静态工具类 * * @return */ public static Long generateId(){ long id = idWorker.nextId(); return id; }  //初始化构造,无参构造有参函数,默认节点都是0 public SnowflakeIdWorker(){ //this(0L, 0L); this.dataCenterId = getDataCenterId(maxDataCenterId); //获取机器编码 this.workerId = getWorkerId(dataCenterId, maxWorkerId); }  /** * 构造函数 * @param workerId 工作ID (0~31) * @param dataCenterId 数据中心ID (0~31) */ public SnowflakeIdWorker(long workerId, long dataCenterId) { if (workerId > maxWorkerId || workerId < 0) { throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值