分布式id解决方案

╰+攻爆jí腚メ 2022-12-03 01:59 285阅读 0赞

分布式高效ID生产黑科技(sequence)

开源产品介绍(微服务基础设施)

  • 微服务神经元(neural)
  1. GITHUB:https://github.com/yu120/neural
  2. 码云:https://git.oschina.net/yu120/neural

简介

高效GUID产生算法(sequence),基于Snowflake实现64位自增ID算法。新增特性:

  • 支持自定义允许时间回拨的范围
  • 解决跨毫秒起始值每次为0开始的情况(避免末尾必定为偶数,而不便于取余使用问题)
  • 解决高并发场景中获取时间戳性能问题

Twitter-Snowflake算法产生的背景相当简单,为了满足Twitter每秒上万条消息的请求,每条消息都必须分配一条唯一的id,这些id还需要一些大致的顺序(方便客户端排序),并且在分布式系统中不同机器产生的id必须不同。

性能测试数据:

在这里插入图片描述
在这里插入图片描述

Snowflake算法核心

把时间戳,工作机器id,序列号组合在一起。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nqd8tHeC-1598942097759)(docs/snowflake-64bit.jpg)]

除了最高位bit标记为不可用以外,其余三组bit占位均可浮动,看具体的业务需求而定。默认情况下41bit的时间戳可以支持该算法使用到2082年,10bit的工作机器id可以支持1023台机器,序列号支持1毫秒产生4095个自增序列id。下文会具体分析。

Snowflake – 时间戳

这里时间戳的细度是毫秒级,具体代码如下,建议使用64位linux系统机器,因为有vdso,gettimeofday()在用户态就可以完成操作,减少了进入内核态的损耗。

Snowflake – 工作机器id

严格意义上来说这个bit段的使用可以是进程级,机器级的话你可以使用MAC地址来唯一标示工作机器,工作进程级可以使用IP+Path来区分工作进程。如果工作机器比较少,可以使用配置文件来设置这个id是一个不错的选择,如果机器过多配置文件的维护是一个灾难性的事情。

Snowflake – 序列号

序列号就是一系列的自增id(多线程建议使用atomic),为了处理在同一毫秒内需要给多条消息分配id,若同一毫秒把序列号用完了,则“等待至下一毫秒”。

实现:

  1. package com.mtons.mblog.modules.id;
  2. import java.net.InetAddress;
  3. import java.util.concurrent.ThreadLocalRandom;
  4. /** * 基于Twitter的Snowflake算法实现分布式高效有序ID生产黑科技(sequence)——升级版Snowflake * * <br> * SnowFlake的结构如下(每部分用-分开):<br> * <br> * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br> * <br> * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br> * <br> * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截) * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下START_TIME属性)。41位的时间截,可以使用69年,年 * T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br> * <br> * 10位的数据机器位,可以部署在1024个节点,包括5位dataCenterId和5位workerId<br> * <br> * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br> * <br> * <br> * 加起来刚好64位,为一个Long型。<br> * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。 * <p> * <p> * 特性: * 1.支持自定义允许时间回拨的范围<p> * 2.解决跨毫秒起始值每次为0开始的情况(避免末尾必定为偶数,而不便于取余使用问题)<p> * 3.解决高并发场景中获取时间戳性能问题<p> * 4.支撑根据IP末尾数据作为workerId * 5.时间回拨方案思考:1024个节点中分配10个点作为时间回拨序号(连续10次时间回拨的概率较小) * * @author lry * @version 3.0 */
  5. public final class Sequence {
  6. /** * 起始时间戳 **/
  7. private final static long START_TIME = 1519740777809L;
  8. /** * dataCenterId占用的位数:2 **/
  9. private final static long DATA_CENTER_ID_BITS = 2L;
  10. /** * workerId占用的位数:8 **/
  11. private final static long WORKER_ID_BITS = 8L;
  12. /** * 序列号占用的位数:12(表示只允许workId的范围为:0-4095) **/
  13. private final static long SEQUENCE_BITS = 12L;
  14. /** * workerId可以使用范围:0-255 **/
  15. private final static long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
  16. /** * dataCenterId可以使用范围:0-3 **/
  17. private final static long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);
  18. private final static long WORKER_ID_SHIFT = SEQUENCE_BITS;
  19. private final static long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
  20. private final static long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
  21. /** * 用mask防止溢出:位与运算保证计算的结果范围始终是 0-4095 **/
  22. private final static long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
  23. private final long workerId;
  24. private final long dataCenterId;
  25. private long sequence = 0L;
  26. private long lastTimestamp = -1L;
  27. private static byte LAST_IP = 0;
  28. private final boolean clock;
  29. private final long timeOffset;
  30. private final boolean randomSequence;
  31. private final ThreadLocalRandom tlr = ThreadLocalRandom.current();
  32. public Sequence(long dataCenterId) {
  33. this(dataCenterId, 0x000000FF & getLastIPAddress(), false, 5L, false);
  34. }
  35. public Sequence(long dataCenterId, boolean clock, boolean randomSequence) {
  36. this(dataCenterId, 0x000000FF & getLastIPAddress(), clock, 5L, randomSequence);
  37. }
  38. /** * 基于Snowflake创建分布式ID生成器 * * @param dataCenterId 数据中心ID,数据范围为0~255 * @param workerId 工作机器ID,数据范围为0~3 * @param clock true表示解决高并发下获取时间戳的性能问题 * @param timeOffset 允许时间回拨的毫秒量,建议5ms * @param randomSequence true表示使用毫秒内的随机序列(超过范围则取余) */
  39. public Sequence(long dataCenterId, long workerId, boolean clock, long timeOffset, boolean randomSequence) {
  40. if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
  41. throw new IllegalArgumentException("Data Center Id can't be greater than " + MAX_DATA_CENTER_ID + " or less than 0");
  42. }
  43. if (workerId > MAX_WORKER_ID || workerId < 0) {
  44. throw new IllegalArgumentException("Worker Id can't be greater than " + MAX_WORKER_ID + " or less than 0");
  45. }
  46. this.workerId = workerId;
  47. this.dataCenterId = dataCenterId;
  48. this.clock = clock;
  49. this.timeOffset = timeOffset;
  50. this.randomSequence = randomSequence;
  51. }
  52. /** * 获取ID * * @return long */
  53. public synchronized Long nextId() {
  54. long currentTimestamp = this.timeGen();
  55. // 闰秒:如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过,这个时候应当抛出异常
  56. if (currentTimestamp < lastTimestamp) {
  57. // 校验时间偏移回拨量
  58. long offset = lastTimestamp - currentTimestamp;
  59. if (offset > timeOffset) {
  60. throw new RuntimeException("Clock moved backwards, refusing to generate id for [" + offset + "ms]");
  61. }
  62. try {
  63. // 时间回退timeOffset毫秒内,则允许等待2倍的偏移量后重新获取,解决小范围的时间回拨问题
  64. this.wait(offset << 1);
  65. } catch (Exception e) {
  66. throw new RuntimeException(e);
  67. }
  68. // 再次获取
  69. currentTimestamp = this.timeGen();
  70. // 再次校验
  71. if (currentTimestamp < lastTimestamp) {
  72. throw new RuntimeException("Clock moved backwards, refusing to generate id for [" + offset + "ms]");
  73. }
  74. }
  75. // 同一毫秒内序列直接自增
  76. if (lastTimestamp == currentTimestamp) {
  77. // randomSequence为true表示随机生成允许范围内的序列起始值并取余数,否则毫秒内起始值为0L开始自增
  78. long tempSequence = sequence + 1;
  79. if (randomSequence && tempSequence > SEQUENCE_MASK) {
  80. tempSequence = tempSequence % SEQUENCE_MASK;
  81. }
  82. // 通过位与运算保证计算的结果范围始终是 0-4095
  83. sequence = tempSequence & SEQUENCE_MASK;
  84. if (sequence == 0) {
  85. currentTimestamp = this.tilNextMillis(lastTimestamp);
  86. }
  87. } else {
  88. // randomSequence为true表示随机生成允许范围内的序列起始值,否则毫秒内起始值为0L开始自增
  89. sequence = randomSequence ? tlr.nextLong(SEQUENCE_MASK + 1) : 0L;
  90. }
  91. lastTimestamp = currentTimestamp;
  92. long currentOffsetTime = currentTimestamp - START_TIME;
  93. /* * 1.左移运算是为了将数值移动到对应的段(41、5、5,12那段因为本来就在最右,因此不用左移) * 2.然后对每个左移后的值(la、lb、lc、sequence)做位或运算,是为了把各个短的数据合并起来,合并成一个二进制数 * 3.最后转换成10进制,就是最终生成的id */
  94. return (currentOffsetTime << TIMESTAMP_LEFT_SHIFT) |
  95. // 数据中心位
  96. (dataCenterId << DATA_CENTER_ID_SHIFT) |
  97. // 工作ID位
  98. (workerId << WORKER_ID_SHIFT) |
  99. // 毫秒序列化位
  100. sequence;
  101. }
  102. /** * 保证返回的毫秒数在参数之后(阻塞到下一个毫秒,直到获得新的时间戳)——CAS * * @param lastTimestamp last timestamp * @return next millis */
  103. private long tilNextMillis(long lastTimestamp) {
  104. long timestamp = this.timeGen();
  105. while (timestamp <= lastTimestamp) {
  106. // 如果发现时间回拨,则自动重新获取(可能会处于无限循环中)
  107. timestamp = this.timeGen();
  108. }
  109. return timestamp;
  110. }
  111. /** * 获得系统当前毫秒时间戳 * * @return timestamp 毫秒时间戳 */
  112. private long timeGen() {
  113. return clock ? SystemClock.INSTANCE.currentTimeMillis() : System.currentTimeMillis();
  114. }
  115. /** * 用IP地址最后几个字节标示 * <p> * eg:192.168.1.30->30 * * @return last IP */
  116. public static byte getLastIPAddress() {
  117. if (LAST_IP != 0) {
  118. return LAST_IP;
  119. }
  120. try {
  121. InetAddress inetAddress = InetAddress.getLocalHost();
  122. byte[] addressByte = inetAddress.getAddress();
  123. LAST_IP = addressByte[addressByte.length - 1];
  124. } catch (Exception e) {
  125. throw new RuntimeException("Unknown Host Exception", e);
  126. }
  127. return LAST_IP;
  128. }
  129. }
  130. package com.mtons.mblog.modules.id;
  131. import java.sql.Timestamp;
  132. import java.util.concurrent.ScheduledExecutorService;
  133. import java.util.concurrent.ScheduledThreadPoolExecutor;
  134. import java.util.concurrent.TimeUnit;
  135. import java.util.concurrent.atomic.AtomicLong;
  136. /** * System Clock * <p> * 利用ScheduledExecutorService实现高并发场景下System.curentTimeMillis()的性能问题的优化. * * @author lry */
  137. public enum SystemClock {
  138. // ====
  139. INSTANCE(1);
  140. private final long period;
  141. private final AtomicLong nowTime;
  142. private boolean started = false;
  143. private ScheduledExecutorService executorService;
  144. SystemClock(long period) {
  145. this.period = period;
  146. this.nowTime = new AtomicLong(System.currentTimeMillis());
  147. }
  148. /** * The initialize scheduled executor service */
  149. public void initialize() {
  150. if (started) {
  151. return;
  152. }
  153. this.executorService = new ScheduledThreadPoolExecutor(1, r -> {
  154. Thread thread = new Thread(r, "system-clock");
  155. thread.setDaemon(true);
  156. return thread;
  157. });
  158. executorService.scheduleAtFixedRate(() -> nowTime.set(System.currentTimeMillis()),
  159. this.period, this.period, TimeUnit.MILLISECONDS);
  160. Runtime.getRuntime().addShutdownHook(new Thread(this::destroy));
  161. started = true;
  162. }
  163. /** * The get current time milliseconds * * @return long time */
  164. public long currentTimeMillis() {
  165. return started ? nowTime.get() : System.currentTimeMillis();
  166. }
  167. /** * The get string current time * * @return string time */
  168. public String currentTime() {
  169. return new Timestamp(currentTimeMillis()).toString();
  170. }
  171. /** * The destroy of executor service */
  172. public void destroy() {
  173. if (executorService != null) {
  174. executorService.shutdown();
  175. }
  176. }
  177. }

调用:

  1. package cn.ms.sequence;
  2. import org.junit.Test;
  3. public class SequenceTest1 {
  4. @Test
  5. public void name() {
  6. try {
  7. int times = 0, maxTimes = 1000;
  8. Sequence sequence = new Sequence(0);
  9. for (int i = 0; i < maxTimes; i++) {
  10. long id = sequence.nextId();
  11. System.out.println("生成的分布式id :" + id);
  12. if(id%2==0){
  13. times++;
  14. }
  15. Thread.sleep(10);
  16. }
  17. System.out.println("偶数:" + times + ",奇数:" + (maxTimes - times) + "!");
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. }
  21. }
  22. }

生成的分布式id(18位):

  1. 生成的分布式id 332180831769219072
  2. 生成的分布式id 332180831811162112
  3. 生成的分布式id 332180831853105152
  4. 生成的分布式id 332180831895048192
  5. 生成的分布式id 332180831936991232
  6. 生成的分布式id 332180831983128576
  7. 生成的分布式id 332180832025071616
  8. 生成的分布式id 332180832071208960
  9. 生成的分布式id 332180832113152000
  10. 生成的分布式id 332180832155095040
  11. 生成的分布式id 332180832201232384
  12. 生成的分布式id 332180832243175424
  13. 生成的分布式id 332180832285118464
  14. 生成的分布式id 332180832327061504
  15. 生成的分布式id 332180832369004544
  16. 生成的分布式id 332180832410947584
  17. 生成的分布式id 332180832452890624
  18. 生成的分布式id 332180832494833664
  19. 生成的分布式id 332180832536776704
  20. 生成的分布式id 332180832578719744
  21. 生成的分布式id 332180832620662784
  22. 生成的分布式id 332180832662605824
  23. 生成的分布式id 332180832704548864
  24. 生成的分布式id 332180832746491904
  25. 生成的分布式id 332180832792629248
  26. 生成的分布式id 332180832834572288
  27. 生成的分布式id 332180832876515328
  28. 生成的分布式id 332180832918458368

发表评论

表情:
评论列表 (有 0 条评论,285人围观)

还没有评论,来说两句吧...

相关阅读

    相关 分布式id解决方案

    最近在做分布式任务调度系统,遇到分布式id的问题,我们需要一个全局唯一的id,但是服务又部署在多台服务器上面。因为之前没有什么分布式系统的经验,想当然的就是用了全局分布式锁来保

    相关 分布式Id生成方案

    系统唯一ID是我们在设计一个系统的时候常常会遇见的问题,也常常为这个问题而纠结。生成ID的方法有很多,适应不同的场景、需求以及性能要求。所以有些比较复杂的系统会有多个ID生成的