雪花算法(Snowflake Algorithm)实现原理与详解

一、引言

在分布式系统中,生成全局唯一ID是一个常见的需求。传统的UUID(通用唯一识别码)虽然能够生成全局唯一的ID,但由于其无序性,在数据库插入数据时会导致频繁的页分裂,从而影响性能。雪花算法(Snowflake Algorithm)由Twitter开源,用于生成全局唯一的64位ID,具有高性能、有序性、可扩展性等特点。

二、雪花算法的整体设计

雪花算法生成的64位ID由以下几部分组成:

  1. 第一位(符号位):由于ID都是正整数,所以第一位始终为0。

  2. 时间戳(41位):记录时间戳的差值(相对于某个固定时间点),单位是毫秒。41位时间戳可以使用69年(从1970年开始,可用至2039年)。

  3. 工作机器id(10位):用于标识不同的工作机器(如不同的服务器实例),支持在同一数据中心内部署最多1024台机器。

    如果存在跨机房部署的情况下可以把这10个bit位,拆分成两个5bit,前5个bit表示机房id,后面5个表示机器的id

  4. 序列号(12位):用于在同一毫秒内产生不同的ID,支持每个工作机器在同一毫秒内产生最多4096个ID。

三、实现方式

1. 初始化参数

在启动服务时,需要为每个工作机器分配一个唯一的工作机器id和一个数据中心id。这些id通常在服务配置中指定,并且在整个服务运行期间保持不变。

2. 生成时间戳

每次生成ID时,首先获取当前时间戳(毫秒级),并计算其与起始时间戳的差值。起始时间戳是一个固定的时间点,通常设置为服务的启动时间或某个固定的时间点。

3. 分配工作机器id和数据中心id

根据服务的配置,将工作机器id和数据中心id分别放入ID的相应位置。这些id在初始化时分配,并在整个服务运行期间保持不变。

4. 生成序列号

在同一毫秒内,如果多次调用生成ID的方法,需要使用序列号来区分不同的ID。序列号从0开始递增,当序列号达到最大值(4095)时,需要等待下一毫秒才能继续生成ID。

5. 组装ID

将时间戳差值、工作机器id、数据中心id和序列号按照指定的位数进行位移和或运算,生成最终的64位ID。

6. 处理时钟回拨问题

由于时钟误差或网络延迟等原因,可能会出现时钟回拨的情况。为了处理这种情况,雪花算法通常使用一个缓存机制来存储最近生成的时间戳。如果当前时间戳小于缓存中的时间戳,则拒绝生成ID并等待一段时间再试。这样可以避免由于时钟回拨导致的ID冲突问题。

### 7. 分布式环境中的部署

在分布式环境中,每个工作机器都需要运行一个雪花算法实例,并分配一个唯一的工作机器id和数据中心id。这样,每个实例都可以独立地生成全局唯一的ID,并且不同实例生成的ID之间不会发生冲突。

实现算法

以下是一个简化版的雪花算法实现,使用Java语言编写。这个版本省略了一些边缘情况的详细处理(如时钟回拨的复杂处理),但展示了算法的基本结构和位运算操作。

 public class SnowflakeIdWorker {  
 ​
   
 ​
     // 起始的时间戳(毫秒),通常是系统上线的时间  
 ​
     private long twepoch = 1609459200000L; // 示例:2021-01-01 00:00:00  
 ​
   
 ​
     // 机器ID所占的位数  
 ​
     private final long workerIdBits = 5L;  
 ​
   
 ​
     // 数据中心ID所占的位数  
 ​
     private final long datacenterIdBits = 5L;  
 ​
   
 ​
     // 最大机器ID  
 ​
     private final long maxWorkerId = -1L ^ (-1L << workerIdBits);  
 ​
   
 ​
     // 最大数据中心ID  
 ​
     private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);  
 ​
   
 ​
     // 序列号所占的位数  
 ​
     private final long sequenceBits = 12L;  
 ​
   
 ​
     // 机器ID左移位数  
 ​
     private final long workerIdShift = sequenceBits;  
 ​
   
 ​
     // 数据中心ID左移位数  
 ​
     private final long datacenterIdShift = sequenceBits + workerIdBits;  
 ​
   
 ​
     // 时间截左移位数  
 ​
     private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;  
 ​
   
 ​
     // 序列号掩码  
 ​
     private final long sequenceMask = -1L ^ (-1L << sequenceBits);  
 ​
   
 ​
     // 上次生成ID的时间戳  
 ​
     private long lastTimestamp = -1L;  
 ​
   
 ​
     // 同一毫秒内的序列号  
 ​
     private long sequence = 0L;  
 ​
   
 ​
     // 工作机器ID和数据中心ID  
 ​
     private final long workerId;  
 ​
     private final long datacenterId;  
 ​
   
 ​
     /**  
 ​
      * 构造函数,初始化工作机器ID和数据中心ID  
 ​
      *  
 ​
      * @param workerId     工作机器ID  
 ​
      * @param datacenterId 数据中心ID  
 ​
      */  
 ​
     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", maxWorkerId));  
 ​
         }  
 ​
         if (datacenterId > maxDatacenterId || datacenterId < 0) {  
 ​
             throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));  
 ​
         }  
 ​
         this.workerId = workerId;  
 ​
         this.datacenterId = datacenterId;  
 ​
     }  
 ​
   
 ​
     /**  
 ​
      * 生成下一个ID  
 ​
      *  
 ​
      * @return 下一个ID  
 ​
      */  
 ​
     public synchronized long nextId() {  
 ​
         long timestamp = timeGen();  
 ​
   
 ​
         // 如果当前时间小于上一次ID生成的时间戳,则等待到下一毫秒  
 ​
         if (timestamp < lastTimestamp) {  
 ​
             throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));  
 ​
         }  
 ​
   
 ​
         // 如果是同一时间戳,则进行序列号自增  
 ​
         if (lastTimestamp == timestamp) {  
 ​
             sequence = (sequence + 1) & sequenceMask;  
 ​
             // 序列号溢出,等待下一毫秒  
 ​
             if (sequence == 0) {  
 ​
                 timestamp = tilNextMillis(lastTimestamp);  
 ​
             }  
 ​
         } else {  
 ​
             // 如果是新的时间戳,序列号重置为0  
 ​
             sequence = 0L;  
 ​
         }  
 ​
   
 ​
         // 更新上一次ID生成的时间戳  
 ​
         lastTimestamp = timestamp;  
 ​
   
 ​
         // 移位并通过或运算拼到一起组成64位的ID  
 ​
         return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence;  
 ​
     }  
 ​
   
 ​
     /**  
 ​
      * 等待直到下一毫秒  
 ​
      *  
 ​
      * @param lastTimestamp 上次生成ID的时间戳  
 ​
      * @return 下一毫秒的时间戳  
 ​
      */  
 ​
     protected long tilNextMillis(long lastTimestamp) {  
 ​
         long timestamp = timeGen();  
 ​
         while (timestamp <= lastTimestamp) {  
 ​
             timestamp = timeGen();  
 ​
         }  
 ​
         return timestamp;  
 ​
     }  
 ​
   
 ​
     /**  
 ​
      * 获取系统当前毫秒时间戳  
 ​
      *  
 ​
      * @return 当前毫秒时间戳  
 ​
      */  
 ​
     protected long timeGen() {  
 ​
         return System.currentTimeMillis();  
 ​
     }  
 ​
   
 ​
      public static void main(String[] args) {  
 ​
         // 假设我们有一个工作机器ID为1,数据中心ID为1的环境  
 ​
         long workerId = 1L;  
 ​
         long datacenterId = 1L;  
 ​
   
 ​
         // 创建一个SnowflakeIdWorker实例  
 ​
         SnowflakeIdWorker idWorker = new SnowflakeIdWorker(workerId, datacenterId);  
 ​
   
 ​
         // 生成并打印10个ID作为示例  
 ​
         for (int i = 0; i < 10; i++) {  
 ​
             long id = idWorker.nextId();  
 ​
             System.out.println(id);  
 ​
         }  
 ​
     }  

在这个main方法中,我们首先定义了两个长整型变量workerIddatacenterId,它们分别表示工作机器ID和数据中心ID。然后,我们使用这两个ID创建了一个SnowflakeIdWorker的实例。最后,我们使用一个循环来生成并打印10个ID作为示例。

请注意,这个简化的雪花算法实现没有处理时钟回拨的复杂情况。在实际应用中,如果系统时钟发生回拨,可能会导致ID生成器生成重复的ID。为了解决这个问题,通常需要更复杂的逻辑来检测并处理时钟回拨的情况。此外,这个实现也假设了workerIddatacenterId在系统中是唯一的,并且它们的值在创建SnowflakeIdWorker实例之后不会改变。如果这些假设不成立,那么生成的ID可能会发生冲突。

四、总结

雪花算法通过精心设计的位分配和运算规则,在分布式系统中生成了全局唯一的ID。其有序性使得数据库插入数据时能够减少页分裂,提高性能。同时,雪花算法还具有高性能、可扩展性强等优点,因此在分布式系统中得到了广泛应用。需要注意的是,在实际应用中,需要合理设置起始时间戳、工作机器id和数据中心id等参数,以确保生成的ID满足业务需求。

  • 38
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值