神刀安全网

全局唯一ID设计

在分布式系统中,经常需要使用 全局唯一ID 查找对应的数据。产生这种ID需要保证系统全局唯一,而且要高性能以及占用相对较少的空间。

全局唯一ID在数据库中一般会被设成 主键 ,这样为了保证数据插入时索引的快速建立,还需要保持一个有序的趋势。

这样全局唯一ID就需要保证这两个需求:

  • 全局唯一
  • 趋势有序

全局ID产生的几种方式

数据库自增

当服务使用的数据库只有单库单表时,可以利用数据库的 auto_increment 来生成全局唯一递增ID.

优势:

  • 简单,无需程序任何附加操作
  • 保持定长的增量
  • 在单表中能保持唯一性

劣势:

  • 高并发下性能不佳,主键产生的性能上限是数据库服务器单机的上限。
  • 水平扩展困难,在分布式数据库环境下,无法保证唯一性。

UUID

一般的语言中会自带UUID的实现,比如Java中UUID方式 UUID.randomUUID().toString() ,可以通过服务程序本地产生,ID的生成不依赖数据库的实现。

优势:

  • 本地生成ID,不需要进行远程调用。
  • 全局唯一不重复。
  • 水平扩展能力非常好。

劣势:

  • ID有128 bits,占用的空间较大,需要存成字符串类型,索引效率极低。
  • 生成的ID中没有带Timestamp,无法保证趋势递增

Twitter Snowflake

snowflake 是twitter开源的分布式ID生成算法,其核心思想是:产生一个long型的ID,使用其中41bit作为毫秒数,10bit作为机器编号,12bit作为毫秒内序列号。这个算法单机每秒内理论上最多可以生成 1000*(2^12) 个,也就是大约 400W 的ID,完全能满足业务的需求。

根据 snowflake 算法的思想,我们可以根据自己的业务场景,产生自己的全局唯一ID。因为Java中 long 类型的长度是64bits,所以我们设计的ID需要控制在64bits。

比如我们设计的ID包含以下信息:

| 41 bits: Timestamp | 3 bits: 区域 | 10 bits: 机器编号 | 10 bits: 序列号 |

产生唯一ID的 Java 代码:

import java.security.SecureRandom;  /**  * 自定义 ID 生成器  * ID 生成规则: ID长达 64 bits  *  * | 41 bits: Timestamp (毫秒) | 3 bits: 区域(机房) | 10 bits: 机器编号 | 10 bits: 序列号 |  */ public class CustomUUID {     // 基准时间     private long twepoch = 1288834974657L; //Thu, 04 Nov 2010 01:42:54 GMT     // 区域标志位数     private final static long regionIdBits = 3L;     // 机器标识位数     private final static long workerIdBits = 10L;     // 序列号识位数     private final static long sequenceBits = 10L;      // 区域标志ID最大值     private final static long maxRegionId = -1L ^ (-1L << regionIdBits);     // 机器ID最大值     private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);     // 序列号ID最大值     private final static long sequenceMask = -1L ^ (-1L << sequenceBits);      // 机器ID偏左移10位     private final static long workerIdShift = sequenceBits;     // 业务ID偏左移20位     private final static long regionIdShift = sequenceBits + workerIdBits;     // 时间毫秒左移23位     private final static long timestampLeftShift = sequenceBits + workerIdBits + regionIdBits;      private static long lastTimestamp = -1L;      private long sequence = 0L;     private final long workerId;     private final long regionId;      public CustomUUID(long workerId, long regionId) {          // 如果超出范围就抛出异常         if (workerId > maxWorkerId || workerId < 0) {             throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0");         }         if (regionId > maxRegionId || regionId < 0) {             throw new IllegalArgumentException("datacenter Id can't be greater than %d or less than 0");         }          this.workerId = workerId;         this.regionId = regionId;     }      public CustomUUID(long workerId) {         // 如果超出范围就抛出异常         if (workerId > maxWorkerId || workerId < 0) {             throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0");         }         this.workerId = workerId;         this.regionId = 0;     }      public long generate() {         return this.nextId(false, 0);     }      /**      * 实际产生代码的      *      * @param isPadding      * @param busId      * @return      */     private synchronized long nextId(boolean isPadding, long busId) {          long timestamp = timeGen();         long paddingnum = regionId;          if (isPadding) {             paddingnum = busId;         }          if (timestamp < lastTimestamp) {             try {                 throw new Exception("Clock moved backwards.  Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");             } catch (Exception e) {                 e.printStackTrace();             }         }          //如果上次生成时间和当前时间相同,在同一毫秒内         if (lastTimestamp == timestamp) {             //sequence自增,因为sequence只有10bit,所以和sequenceMask相与一下,去掉高位             sequence = (sequence + 1) & sequenceMask;             //判断是否溢出,也就是每毫秒内超过1024,当为1024时,与sequenceMask相与,sequence就等于0             if (sequence == 0) {                 //自旋等待到下一毫秒                 timestamp = tailNextMillis(lastTimestamp);             }         } else {             // 如果和上次生成时间不同,重置sequence,就是下一毫秒开始,sequence计数重新从0开始累加,             // 为了保证尾数随机性更大一些,最后一位设置一个随机数             sequence = new SecureRandom().nextInt(10);         }          lastTimestamp = timestamp;          return ((timestamp - twepoch) << timestampLeftShift) | (paddingnum << regionIdShift) | (workerId << workerIdShift) | sequence;     }      // 防止产生的时间比之前的时间还要小(由于NTP回拨等问题),保持增量的趋势.     private long tailNextMillis(final long lastTimestamp) {         long timestamp = this.timeGen();         while (timestamp <= lastTimestamp) {             timestamp = this.timeGen();         }         return timestamp;     }      // 获取当前的时间戳     protected long timeGen() {         return System.currentTimeMillis();     } }

使用自定义的这种方法需要注意的几点:

  • 为了保持增长的趋势,要避免有些服务器的时间早,有些服务器的时间晚,需要控制好所有服务器的时间,而且要避免 NTP时间服务器 回拨服务器的时间。
  • 在跨毫秒时,序列号总是归0,会使得序列号为0的ID比较多,导致生成的ID取模后不均匀,所以序列号不是每次都归0,而是归一个0到9的随机数。
  • 使用这个 CustomUUID 类,最好在一个系统中能保持单例模式运行。

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » 全局唯一ID设计

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址