Twitter的分布式ID生成雪花算法SnowFlake
一、产生背景:
在分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的SnowFlake解决了这种需求,最初Twitter把存储系统从MySQL迁移到Cassandra,因为Cassandra没有顺序ID生成机制,所以开发了这样一套全局唯一ID生成服务。
另外,越来越多的公司都在用分布式、微服务,那么对应的就会针对不同的服务进行数据库拆分,然后当数据量上来的时候也会进行分表,那么随之而来的就是分表以后id的问题。 例如之前单体项目中一个表中的数据主键id都是自增的,mysql是利用autoincrement来实现自增,而oracle是利用序列来实现的,但是当单表数据量上来以后就要进行水平分表,比如单表大于500w的时候就要分表,但是具体还是得看业务,如果索引用的好的话,单表千万的数据也是可以的。水平分表就是将一张表的数据分成多张表,那么问题就来了如果还是按照以前的自增来做主键id,那么就会出现id重复,这个时候就得考虑用什么方案来解决分布式id的问题了。
常见的分布式ID解决方案:
1、数据库表 可以在某个库中专门维护一张表,然后每次无论哪个表需要自增id的时候都去查这个表的记录,然后用for update锁表,然后取到的值加一,然后返回以后把再把值记录到表中,但是这个方法适合并发量比较小的项目,因此每次都得锁表。
2、redis 因为redis是单线程的,可以在redis中维护一个键值对,然后哪个表需要直接去redis中取值然后加一,但是这个跟上面一样由于单线程都是对高并发的支持不高,只适合并发量小的项目。
3、uuid 可以使用uuid作为不重复主键id,但是uuid有个问题就是其是无序的字符串,如果使用uuid当做主键,那么主键索引就会失效。
4、雪花算法 雪花算法是解决分布式id的一个高效的方案,大部分互联网公司都在使用雪花算法,当然分布式id生成算法的有很多种,Twitter的SnowFlake就是其中经典的一种。
二、原理
雪花算法就是使用64位long类型的数据存储id,最高位一位存储0或者1,0代表整数,1代表负数,一般都是0,所以最高位不变,41位存储毫秒级时间戳,10位存储机器码(包括5位datacenterId和5位workerId),12存储序列号。这样最大2的10次方的机器,也就是1024台机器,最多每毫秒每台机器产生2的12次方也就是4096个id。
但是一般我们没有那么多台机器,所以我们也可以使用53位来存储id。为什么要用53位?因为我们几乎都是跟web页面打交道,就需要跟js打交道,js支持最大的整型范围为53位,超过这个范围就会丢失精度,53之内可以直接由js读取,超过53位就需要转换成字符串才能保证js处理正确。53存储的话,32位存储秒级时间戳,5位存储机器码,16位存储序列化,这样每台机器每秒可以生产65536个不重复的id。
算法的优点:
1、高性能高可用:生成时不依赖于数据库,完全在内存中生成。
2、容量大:每秒中能生成数百万的自增ID。
3、ID自增:存入数据库中,索引效率高。
算法的缺点:
由于雪花算法严重依赖时间,所以当发生服务器时钟回拨的问题是会导致可能产生重复的id。当然几乎没有公司会修改服务器时间,修改以后会导致各种问题,公司宁愿新加一台服务器也不愿意修改服务器时间,但是不排除特殊情况。如何解决时钟回拨的问题?可以对序列化的初始值设置步长,每次触发时钟回拨事件,则其初始步长就加1K,将sequence的初始值设置为1000。
三、代码(Twitter官方给出的算法实现 是用Scala写的,这里给出Java的实现)
1 public class IdWorkerUtil { 2 3 //下面两个每个5位,加起来就是10位的工作机器id 4 private long workerId; //工作id 5 private long datacenterId; //数据id 6 //12位的序列号 7 private long sequence; 8 9 public IdWorkerUtil(long workerId, long datacenterId, long sequence){ 10 // sanity check for workerId 11 if (workerId > maxWorkerId || workerId < 0) { 12 throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0",maxWorkerId)); 13 } 14 if (datacenterId > maxDatacenterId || datacenterId < 0) { 15 throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId)); 16 } 17 System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d", 18 timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId); 19 20 this.workerId = workerId; 21 this.datacenterId = datacenterId; 22 this.sequence = sequence; 23 } 24 25 //初始时间戳 26 private long twepoch = 1288834974657L; 27 28 //长度为5位 29 private long workerIdBits = 5L; 30 private long datacenterIdBits = 5L; 31 //最大值 32 private long maxWorkerId = -1L ^ (-1L << workerIdBits); 33 private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); 34 //序列号id长度 35 private long sequenceBits = 12L; 36 //序列号最大值 37 private long sequenceMask = -1L ^ (-1L << sequenceBits); 38 39 //工作id需要左移的位数,12位 40 private long workerIdShift = sequenceBits; 41 //数据id需要左移位数 12+5=17位 42 private long datacenterIdShift = sequenceBits + workerIdBits; 43 //时间戳需要左移位数 12+5+5=22位 44 private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; 45 46 //上次时间戳,初始值为负数 47 private long lastTimestamp = -1L; 48 49 public long getWorkerId(){ 50 return workerId; 51 } 52 53 public long getDatacenterId(){ 54 return datacenterId; 55 } 56 57 public long getTimestamp(){ 58 return System.currentTimeMillis(); 59 } 60 61 //下一个ID生成算法 62 public synchronized long nextId() { 63 long timestamp = timeGen(); 64 65 //获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常 66 if (timestamp < lastTimestamp) { 67 System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp); 68 throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", 69 lastTimestamp - timestamp)); 70 } 71 72 //获取当前时间戳如果等于上次时间戳(同一毫秒内),则在序列号加一;否则序列号赋值为0,从0开始。 73 if (lastTimestamp == timestamp) { 74 sequence = (sequence + 1) & sequenceMask; 75 if (sequence == 0) { 76 timestamp = tilNextMillis(lastTimestamp); 77 } 78 } else { 79 sequence = 0; 80 } 81 82 //将上次时间戳值刷新 83 lastTimestamp = timestamp; 84 85 /** 86 * 返回结果: 87 * (timestamp - twepoch) << timestampLeftShift) 表示将时间戳减去初始时间戳,再左移相应位数 88 * (datacenterId << datacenterIdShift) 表示将数据id左移相应位数 89 * (workerId << workerIdShift) 表示将工作id左移相应位数 90 * | 是按位或运算符,例如:x | y,只有当x,y都为0的时候结果才为0,其它情况结果都为1。 91 * 因为个部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id 92 */ 93 return ((timestamp - twepoch) << timestampLeftShift) | 94 (datacenterId << datacenterIdShift) | 95 (workerId << workerIdShift) | 96 sequence; 97 } 98 99 //获取时间戳,并与上次时间戳比较 100 private long tilNextMillis(long lastTimestamp) { 101 long timestamp = timeGen(); 102 while (timestamp <= lastTimestamp) { 103 timestamp = timeGen(); 104 } 105 return timestamp; 106 } 107 108 //获取系统时间戳 109 private long timeGen(){ 110 return System.currentTimeMillis(); 111 } 112 113 //---------------测试--------------- 114 public static void main(String[] args) { 115 IdWorkerUtil worker = new IdWorkerUtil(12,21,31); 116 for (int i = 0; i < 100; i++) { 117 System.out.println(worker.nextId()); 118 } 119 } 120 121 }
四、备注:
1、每个位段存储的信息大家可以根据自己业务修改,可以根据自己需求适当调整每段的大小以及存储的信息。
2、解密id,由于id的每段都保存了特定的信息,所以拿到一个id,应该可以尝试反推出原始的每个段的信息。反推可以帮我们分析出一些有用的订单。比如ID的生成日期,负责处理的数据中心,负责处理的机器ID等等。
3、workerId可以获取本机的IP及关键词拼成KEY,然后从redis中等获取单一机器的自定义的自增值或初始值等