写在前面
最近学习也有点陷入迷茫状态,不知道学些什么,也不知道写点什么。那就想起什么就写点啥,就当是重新学习。
今天要来讲的是分布式系统中常用的一种算法,雪花算法。 
正文
什么是雪花算法
雪花算法(snowflake),它是Twitter公司使用的在分布式系统中生成唯一ID的一个算法,在2014年开源。
雪花算法是在高并发环境下生成为ID的算法,它能保证分布式环境下ID不重复,且基本有序递增,并且不依赖与其他的第三方库。  
雪花算法的原理
雪花算法的原理十分简单,它生成的ID由64bit组成,其中包括:
- 1bit的标志位,由于二进制中最高为1的都是负数,并且生成的id一般都是整数,所以这一位通常为0.
 
- 41bit的时间戳,可以存储69年的时间,一般来说起始时间戳为系统上线的时间戳。
 
- 10bit的机器id,可以部署1024个节点。
 
- 12bit序列号,用来表示同一时间戳下生成的不同id,在同一个时间戳下可以表示4095个序列号。  
 
因为刚好是64bit,所以在Java中刚好由long类型来存储。倒不如说是这个算法可能一开始就想好使用long类型来存储。
代码实现
当然最重要的还是如何生成ID啦,那生成的算法网上有很多,我就依照可能大家看的最多的代码,来讲讲自己的理解。
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 SnowFlake {          private final long startTimestamp = 1602172800L;
 
           private final long workerIdBits = 10L;          private final long maxWorkerId = ~(-1L << workerIdBits);          private final long sequenceBits = 12L;          private final long sequenceMask = ~(-1L << sequenceBits);
           private final long workerIdShift = sequenceBits;          private final long timestampLeftShift = sequenceBits + workerIdBits;
           private long workerId;          private long sequence = 0L;          private long lastTimestamp = -1L;
      public SnowFlake(long workerId) {                  if (workerId < 0 || workerId > maxWorkerId){             throw new RuntimeException("机器id不正确");         }         this.workerId = workerId;     }
           public synchronized long nextId(){         long timestamp = System.currentTimeMillis();                  if (timestamp < lastTimestamp){             throw new RuntimeException("系统时间被回退");         }                  if (lastTimestamp == timestamp){                          sequence = (sequence + 1) & sequenceMask;                          if (sequence == 0){                                  timestamp = getNextMillis(lastTimestamp);             }         }                  else{             sequence = 0L;         }         lastTimestamp = timestamp;                  return ((timestamp - startTimestamp) << timestampLeftShift)                 | (workerId << workerIdShift)                 | sequence;     }               private long getNextMillis(long lastTimestamp){         long timestamp = System.currentTimeMillis();         while (timestamp <= lastTimestamp){             timestamp = System.currentTimeMillis();         }         return timestamp;     } }
  | 
   
在代码中都有相应的注释。其中需要理解的可能是生成最大值的位运算,比如maxWorkerId = ~(-1L << workerIdBits);。
首先负数在计算机中是使用补码进行表示,补码是负数绝对值的原码,取反码再加一。-1的绝对值是1,1的原码就是最后一位是1,取反码,就是0变1,1变0,再加1,就得到了全是1的二进制。
然后左移workerIdBits位,也就是12位,然后再取反,就得到了最大值,即1023。
那其实取最大值有两种写法,一种是maxWorkerId = -1L ^ (-1L << workerIdBits);还有一种就是上面的写法,一种是异或一种是取反,得到的结果都是相同的。我个人也不确定哪种比较好,idea中提示我使用取反操作,那就当取反操作可能更好一些。当然这只是idea的建议,真正要区分的话还得看两种方法的底层速度比较,我对位运算不熟悉,希望有大佬能比较一下。  
后续一些思考
那雪花算法并不算难,通常情况下我们可以对雪花算法进行魔改。比如我可能不需要用到1024个节点啊,那么就把workIdBit减少,然后时间戳增加或者序列号增加。
又或者我这个系统可能用不到69年,那就可以适当减少时间戳比特位。
那还有就是workid的问题,即workid该怎么决定?那我们可以通过JVM传参,即-D选项,那如果是通过容器化部署的话,可能没办法传入不同参数,那可以通过某些规则来计算workid,比如可以通过该台机器的ip地址或者mac地址来计算workid,但是需要保证workid不重复,否则即使在并发不高的情况下,id也可能重复,至于实现,这里便不再展开,感兴趣的可以自己写写。