雪花算法:分布式唯一 ID 生成利器

db82827b8a27e7b556a400e9ec1e280d.png

无论是在分布式系统中的 ID 生成,还是在业务系统中请求流水号这一类唯一编号的生成,都是软件开发人员经常会面临的一场景。而雪花算法便是这些场景的一个解决方案。

以分布式 ID 为例,它的生成往往会在唯一性、递增性、高可用性、高性能等方面都有所要求。并且在业务处理时,还要防止爬虫根据 ID 的自增进行数据爬取。而雪花算法,在这些方面表现得都不错。

市面上比较常见的分布式 ID 生成算法及类库有如下 4 种:

  1. UUID :Java 自带 API,生成一串唯一随机 36 位字符串(32 个字符串 + 4 个 “-”)。可以保证唯一性,但可读性差,无法有序递增。

  2. SnowFlake :雪花算法,Twitter 开源的由 64 位整数组成分布式 ID,性能较高,并且在单机上递增。GitHub 上官方地址:https://github.com/twitter-archive/snowflake/tree/snowflake-2010 。

  3. UidGenerator :百度开源的分布式 ID 生成器,基于雪花算法。GitHub 参考链接:https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md 。该项目的说明文档及测试案例都值得深入学习一下。

  4. Leaf :美团开源的分布式 ID 生成器,能保证全局唯一,趋势递增,但需要依赖关系数据库、Zookeeper 等中间件。相关实现可参考该文:https://tech.meituan.com/2017/04/21/mt-leaf.html 。

今天我们重点关注「雪花算法」这种分布式 ID 的生成方式。

雪花(snowflake),美丽、独特又变幻莫测。在大自然中几乎找不到两片完全一样的雪花。雪花的这些特性正好在雪花算法上有所展示。

SnowFlake 算法是 Twitter 开源的分布式 ID 生成算法。核心思想就是:使用一个 64 bit 的 long 型的数字作为全局唯一 ID。

算法中还引入了时间戳,基本上保证了自增特性。最初的版本的雪花算法是基于 scala 写的,当然,不同的编程语言都可以根据其算法逻辑进行实现。

算法原理

SnowFlake 算法生成 ID 的结果是一个 64bit 大小的整数,结构如下图:

94e1d95c9d8d565ec99ffd132629c8b3.png

算法解析:

  1. 第一个部分:1 个 bit。 无意义,固定为 0。二进制中最高位是符号位,1 表示负数,0 表示正数。ID 都是正整数,所以固定为 0。

  2. 第二个部分:41 个 bit。 表示时间戳,精确到毫秒,可以使用 69 年。时间戳带有自增属性。

  3. 第三个部分:10 个 bit。 表示 10 位的机器标识,最多支持 1024 个节点。此部分也可拆分成 5 位 datacenterId 和 5 位 workerId,datacenterId 表示机房 ID,workerId 表示机器 ID。

  4. 第四部分:12 个 bit。 表示序列化,即一些列的自增 ID,可以支持同一节点同一毫秒生成最多  4096 个 ID 序号。

由于在 Java 中 64bit 的整数是 long 类型,所以在 Java 中 SnowFlake 算法生成的 id 就是 long 来存储的。

Java 实现

雪花算法 Java 工具类实现:

public class SnowFlake {

 /**
  * 起始的时间戳(可设置当前时间之前的邻近时间)
  */
 private final static long START_STAMP = 1480166465631L;

 /**
  * 序列号占用的位数
  */
 private final static long SEQUENCE_BIT = 12;
 /**
  * 机器标识占用的位数
  */
 private final static long MACHINE_BIT = 5;
 /**
  * 数据中心占用的位数
  */
 private final static long DATA_CENTER_BIT = 5;

 /**
  * 每一部分的最大值
  */
 private final static long MAX_DATA_CENTER_NUM = ~(-1L << DATA_CENTER_BIT);
 private final static long MAX_MACHINE_NUM = ~(-1L << MACHINE_BIT);
 private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);

 /**
  * 每一部分向左的位移
  */
 private final static long MACHINE_LEFT = SEQUENCE_BIT;
 private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
 private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;

 /**
  * 数据中心ID(0~31)
  */
 private final long dataCenterId;
 /**
  * 工作机器ID(0~31)
  */
 private final long machineId;
 /**
  * 毫秒内序列(0~4095)
  */
 private long sequence = 0L;
 /**
  * 上次生成ID的时间截
  */
 private long lastStamp = -1L;

 public SnowFlake(long dataCenterId, long machineId) {
  if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
   throw new IllegalArgumentException("dataCenterId can't be greater than MAX_DATA_CENTER_NUM or less than " +
     "0");
  }
  if (machineId > MAX_MACHINE_NUM || machineId < 0) {
   throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
  }
  this.dataCenterId = dataCenterId;
  this.machineId = machineId;
 }

 /**
  * 产生下一个ID
  */
 public synchronized long nextId() {
  long currStamp = getNewStamp();
  if (currStamp < lastStamp) {
   throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
  }

  if (currStamp == lastStamp) {
   //相同毫秒内,序列号自增
   sequence = (sequence + 1) & MAX_SEQUENCE;
   //同一毫秒的序列数已经达到最大
   if (sequence == 0L) {
    //阻塞到下一个毫秒,获得新的时间戳
    currStamp = getNextMill();
   }
  } else {
   //不同毫秒内,序列号置为0
   sequence = 0L;
  }

  lastStamp = currStamp;

  // 移位并通过或运算拼到一起组成64位的ID
  return (currStamp - START_STAMP) << TIMESTAMP_LEFT //时间戳部分
    | dataCenterId << DATA_CENTER_LEFT       //数据中心部分
    | machineId << MACHINE_LEFT             //机器标识部分
    | sequence;                             //序列号部分
 }

 private long getNextMill() {
  long mill = getNewStamp();
  while (mill <= lastStamp) {
   mill = getNewStamp();
  }
  return mill;
 }

 private long getNewStamp() {
  return System.currentTimeMillis();
 }

 public static void main(String[] args) {
  SnowFlake snowFlake = new SnowFlake(11, 11);

  long start = System.currentTimeMillis();
  for (int i = 0; i < 10; i++) {
   System.out.println(snowFlake.nextId());
  }

  System.out.println(System.currentTimeMillis() - start);
 }
}

上述代码中,在算法的核心方法上,通过加 synchronized 锁来保证线程安全。这样,同一服务器线程是安全的,生成的 ID 不会出现重复,而不同服务器由于机器码不同,就算同一时刻两台服务器都产生了雪花 ID,结果也是不一样的。

为什么最长是 69 年?

上面我们说过,64 位的雪花算法 ID 只能支持 69 年,为什么呢?这是因为 41 位的二进制,其最大的时间戳,转化成年就只有 69 年,即:(2^{41}-1) / (1000 * 60 * 60 * 24 *365) = 69 年

我们可以用如下代码验证一下:

public static void main(String[] args) {
   //41位二进制最小值
   String minTimeStampStr = "00000000000000000000000000000000000000000";
   //41位二进制最大值
   String maxTimeStampStr = "11111111111111111111111111111111111111111";
   //转10进制
   long minTimeStamp = new BigInteger(minTimeStampStr, 2).longValue();
   long maxTimeStamp = new BigInteger(maxTimeStampStr, 2).longValue();
   //一年总共多少毫秒
   long oneYearMills = 1L * 1000 * 60 * 60 * 24 * 365;
   //算出最大可以多少年
   System.out.println((maxTimeStamp - minTimeStamp) / oneYearMills);
}

所以,雪花算法生成的 ID 只能保证 69 年内不会重复,如果超过 69 年的话,那就考虑换个服务器(服务器 ID)部署,并且要保证该服务器的 ID 和之前都没有重复过。

前后端类型差异

在使用雪花算法时,由于生成的 ID 是 64 位,在传递给前端时,需要考虑以字符串的类型进行传递,否则可能会导致前端类型溢出,再回传到服务器时已经变成另外一个值。

这是因为 Number 类型的 ID 在 JS 中最大只支持 53 位,直接将雪花算法的生成的 ID 传递给 JS,会导致溢出。

总结

生成唯一性 ID(其他数据)是几乎在每个系统中都会有的场景,对其生成算法不仅要保证全局唯一性、趋势递增性,还要保证信息安全(比如被爬取数据),同时还要保证算法的高可用性(QPS、可行 5 个 9、平均延时、TP999 等指标)。这就对 ID 生成的算法有一定的要求,而雪花算法算是一个不错的选择。

但它也是有一定的缺点的,比如强依赖机器时钟,如果机器上的时钟回拨,会导致重复或服务不可用的问题,这也是我们在使用时需要注意的事项。

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
雪花算法是种生成分布式ID算法,它可以生成一个64位的ID,其中包含了时间戳、数据中心ID和机器ID等信息。下面是雪花算法生成分布式ID的软件设计模型: 1. 定义一个Snowflake类,该类包含以下属性: - datacenter_id: 数据中心ID,占5位,取值围为0~31。 - worker_id: 机器ID,占5位,取值范围为0~31。 - sequence: 序列号,占12位,取值范围为0~4095。 - last_timestamp: 上一次生成ID的时间戳。 2. 实现Snowflake类的构造函数,初始化datacenter_id和worker_id属性。 3. 实现一个next_id方法,该方法用于生成下一个ID。具体实现如下: - 获取当前时间戳,单位为毫秒。 - 如果当前时间戳小于上一次生成ID的时间戳,则说明系统时钟回退过,抛出异常。 - 如果当前时间戳等于上一次生成ID的时间戳,则将序列号加1。 - 如果当前时间戳大于上一次生成ID的时间戳,则将序列号重置为0,并将last_timestamp属性更新为当前时间戳。 - 将datacenter_id、worker_id、时间戳和序列号按照一定的位数组合成一个64位的ID。 - 返回生成ID。 4. 在分布式系统中,每个节点都需要创建一个Snowflake实例,并指定不同的datacenter_id和worker_id。每个节点生成ID都是唯一的,且具有时间顺序。 下面是一个Python实现的雪花算法生成分布式ID的代码示例: ```python import time class Snowflake: def __init__(self, datacenter_id, worker_id): self.datacenter_id = datacenter_id self.worker_id = worker_id self.sequence = 0 self.last_timestamp = -1 def next_id(self): timestamp = int(time.time() * 1000) if timestamp < self.last_timestamp: raise Exception("Clock moved backwards. Refusing to generate id") if timestamp == self.last_timestamp: self.sequence = (self.sequence + 1) & 4095 if self.sequence == 0: timestamp = self.wait_next_millis(self.last_timestamp) else: self.sequence = 0 self.last_timestamp = timestamp return ((timestamp - 1288834974657) << 22) | (self.datacenter_id << 17) | (self.worker_id << 12) | self.sequence def wait_next_millis(self, last_timestamp): timestamp = int(time.time() * 1000) while timestamp <= last_timestamp: timestamp = int(time.time() * 1000) return timestamp ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值