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 }
View Code

四、备注:

    1、每个位段存储的信息大家可以根据自己业务修改,可以根据自己需求适当调整每段的大小以及存储的信息。
    2、解密id,由于id的每段都保存了特定的信息,所以拿到一个id,应该可以尝试反推出原始的每个段的信息。反推可以帮我们分析出一些有用的订单。比如ID的生成日期,负责处理的数据中心,负责处理的机器ID等等。
    3、workerId可以获取本机的IP及关键词拼成KEY,然后从redis中等获取单一机器的自定义的自增值或初始值等

 

posted @ 2022-01-18 21:33  xuzhujack  阅读(477)  评论(0编辑  收藏  举报
;