分布式自增ID算法snowflake

Snowflake算法是一种分布式ID生成算法,生成的ID具有唯一性、有序性和高效率的特点。该算法将ID分为时间戳、数据中心ID、机器ID和序列号四部分,通过合理分配位数,确保了在分布式环境下ID的全局唯一性和生成效率。测试显示,Snowflake算法每秒能生成约26万个ID。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的,作为索引非常不好,严重影响性能。

snowflake的结构如下(每部分用-分开):

0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000

  • 第一个部分,是 1 个 bit:0,这个是无意义的。

  • 第二个部分是 41 个 bit:表示的是时间戳。

  • 第三个部分是 5 个 bit:表示的是机房 id,10001。

  • 第四个部分是 5 个 bit:表示的是机器 id,1 1001。

  • 第五个部分是 12 个 bit:表示的序号,就是某个机房某台机器上这一毫秒内同时生成的 id 的序号,0000 00000000。

snowflake生成的ID整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和workerId作区分),并且效率较高。经测试snowflake每秒能够产生26万个ID。

  1 /**
  2  * Twitter_Snowflake<br>
  3  * SnowFlake的结构如下(每部分用-分开):<br>
  4  * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 -
  5  * 000000000000 <br>
  6  * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
  7  * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
  8  * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T
  9  * = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
 10  * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
 11  * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
 12  * 加起来刚好64位,为一个Long型。<br>
 13  * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
 14  */
 15 public class SnowflakeId {
 16     /** 开始时间截 (2015-01-01) */
 17     private final long twepoch = 1420041600000L;
 18 
 19     /** 机器id所占的位数 */
 20     private final long workerIdBits = 5L;
 21 
 22     /** 数据标识id所占的位数 */
 23     private final long datacenterIdBits = 5L;
 24 
 25     /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
 26     private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
 27 
 28     /** 支持的最大数据标识id,结果是31 */
 29     private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
 30 
 31     /** 序列在id中占的位数 */
 32     private final long sequenceBits = 12L;
 33 
 34     /** 机器ID向左移12位 */
 35     private final long workerIdShift = sequenceBits;
 36 
 37     /** 数据标识id向左移17位(12+5) */
 38     private final long datacenterIdShift = sequenceBits + workerIdBits;
 39 
 40     /** 时间截向左移22位(5+5+12) */
 41     private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
 42 
 43     /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
 44     private final long sequenceMask = -1L ^ (-1L << sequenceBits);
 45 
 46     /** 工作机器ID(0~31) */
 47     private long workerId;
 48 
 49     /** 数据中心ID(0~31) */
 50     private long datacenterId;
 51 
 52     /** 毫秒内序列(0~4095) */
 53     private long sequence = 0L;
 54 
 55     /** 上次生成ID的时间截 */
 56     private long lastTimestamp = -1L;
 57 
 58     /**
 59      * @param workerId     工作ID (0~31)
 60      * @param datacenterId 数据中心ID (0~31)
 61      */
 62     public SnowflakeId(long workerId, long datacenterId) {
 63         if (workerId > maxWorkerId || workerId < 0) {
 64             throw new IllegalArgumentException(
 65                     String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
 66         }
 67         if (datacenterId > maxDatacenterId || datacenterId < 0) {
 68             throw new IllegalArgumentException(
 69                     String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
 70         }
 71         this.workerId = workerId;
 72         this.datacenterId = datacenterId;
 73     }
 74 
 75     /**
 76      * 获得下一个ID (该方法是线程安全的)
 77      * 
 78      * @return SnowflakeId
 79      */
 80     public synchronized long nextId() {
 81         long timestamp = timeGen();
 82 
 83         // 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
 84         if (timestamp < lastTimestamp) {
 85             throw new RuntimeException(String.format(
 86                     "Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
 87         }
 88 
 89         // 如果是同一时间生成的,则进行毫秒内序列
 90         if (lastTimestamp == timestamp) {
 91             sequence = (sequence + 1) & sequenceMask;
 92             // 毫秒内序列溢出
 93             if (sequence == 0) {
 94                 // 阻塞到下一个毫秒,获得新的时间戳
 95                 timestamp = tilNextMillis(lastTimestamp);
 96             }
 97         }
 98         // 时间戳改变,毫秒内序列重置
 99         else {
100             sequence = 0L;
101         }
102 
103         // 上次生成ID的时间截
104         lastTimestamp = timestamp;
105 
106         // 移位并通过或运算拼到一起组成64位的ID
107         return ((timestamp - twepoch) << timestampLeftShift) //
108                 | (datacenterId << datacenterIdShift) //
109                 | (workerId << workerIdShift) //
110                 | sequence;
111     }
112 
113     /**
114      * 阻塞到下一个毫秒,直到获得新的时间戳
115      * 
116      * @param lastTimestamp 上次生成ID的时间截
117      * @return 当前时间戳
118      */
119     protected long tilNextMillis(long lastTimestamp) {
120         long timestamp = timeGen();
121         while (timestamp <= lastTimestamp) {
122             timestamp = timeGen();
123         }
124         return timestamp;
125     }
126 
127     /**
128      * 返回以毫秒为单位的当前时间
129      * 
130      * @return 当前时间(毫秒)
131      */
132     protected long timeGen() {
133         return System.currentTimeMillis();
134     }
135 
136     /** 测试 */
137     public static void main(String[] args) {
138         long startTime = System.currentTimeMillis();
139         SnowflakeId snowflakeId = new SnowflakeId(0, 0);
140         for (int i = 0; i < 1000; i++) {
141             long id = snowflakeId.nextId();
142             System.out.println(Long.toBinaryString(id));
143             System.out.println(id);
144         }
145         long endTime = System.currentTimeMillis();
146         System.out.println("生成1000个id的时间:" + (endTime - startTime));
147     }
148 }

运行结果:

...
生成1000个id的时间:16

效率非常高,1000个id才花费16ms

 

高并发分布式系统中生成全局唯一Id汇总 数据在分片时,典型的是分库分表,就有一个全局ID生成的问题。 单纯的生成全局ID并不是什么难题,但是生成的ID通常要满足分片的一些要求: 1 不能有单点故障。 2 以时间为序,或者ID里包含时间。这样一是可以少一个索引,二是冷热数据容易分离。 3 可以控制ShardingId。比如某一个用户的文章要放在同一个分片内,这样查询效率高,修改也容易。 4 不要太长,最好64bit。使用long比较好操作,如果是96bit,那就要各种移位相当的不方便,还有可能有些组件不能支持这么大的ID。 一 twitter twitter在把存储系统从MySQL迁移到Cassandra的过程中由于Cassandra没有顺序ID生成机制,于是自己开发了一套全局唯一ID生成服务:Snowflake1 41位的时间序列(精确到毫秒,41位的长度可以使用69年) 2 10位的机器标识(10位的长度最多支持部署1024个节点) 3 12位的计数顺序号(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号) 最高位是符号位,始终为0。 优点:高性能,低延迟;独立的应用;按时间有序。 缺点:需要独立的开发和部署。 原理 java 实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 public class IdWorker { private final long workerId; private final static long twepoch = 1288834974657L; private long sequence = 0L; private final static long workerIdBits = 4L; public final static long maxWorkerId = -1L ^ -1L << workerIdBits; private final static long sequenceBits = 10L; private final static long workerIdShift = sequenceBits; private final static long timestampLeftShift = sequenceBits + workerIdBits; public final static long sequenceMask = -1L ^ -1L < this.maxWorkerId || workerId < 0) { throw new IllegalArgumentException(String.format( "worker Id can't be greater than %d or less than 0", this.maxWorkerId)); } this.workerId = workerId; } public synchronized long nextId() { long timestamp = this.timeGen(); if (this.lastTimestamp == timestamp) { this.sequence = (this.sequence + 1) & this.sequenceMask; if (this.sequence == 0) { System.out.println("###########" + sequenceMask); timestamp = this.tilNextMillis(this.lastTimestamp); } } else { this.sequence = 0; } if (timestamp < this.lastTimestamp) { try { throw new Exception( String.format( "Clock moved backwards. Refusing to generate id for %d milliseconds", this.lastTimestamp - timestamp)); } catch (Exception e) { e.printStackTrace(); } } this.lastTimestamp = timestamp; long nextId = ((timestamp - twepoch << timestampLeftShift)) | (this.workerId << this.workerIdShift) | (this.sequence); System.out.println("timestamp:" + timestamp + ",timestampLeftShift:" + timestampLeftShift + ",nextId:" + nextId + ",workerId:" + workerId + ",sequence:" + sequence); return nextId; } private long tilNextMillis(final long lastTimestamp) { long timestamp = this.timeGen(); while (timestamp <= lastTimestamp) { timestamp = this.timeGen(); } return timestamp; } private long timeGen() { return System.currentTimeMillis(); } public static void main(String[] args){ IdWorker worker2 = new IdWorker(2); System.out.println(worker2.nextId()); } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值