Snowflake

Snowflake

Snowflake算法产生于Twitter的高并发场景下,其需要为每秒上万条消息的请求分配一条唯一的ID,这些ID还需要一些大致的顺序以方便客户端排序,并且在分布式系统中不同机器产生的ID必须不同。Snowflake算法的核心思想在于,使用一个64 bitlong型的数字作为全局唯一ID。这64bit中,其中1bit是不用的,然后用其中的41 bit作为毫秒数,用10 bit作为工作机器id12 bit作为序列号。

首位bit不可用,是因为二进制里第一个bit为如果是1,那么都是负数,但是我们生成的id都是正数,所以第一个bit统一都是0。除了最高位bit标记为不可用以外,其余三组bit占位均可浮动,看具体的业务需求而定。默认情况下41bit的时间戳可以支持该算法使用到2082年,10bit的工作机器id可以支持1023台机器,序列号支持1毫秒产生4095个自增序列id。由此也可看出,Snowflake限制workid最多能有1024,也就是说,应用规模不能超过1024;虽然可以进行细微的调整,但是总是有数量的限制。

在《Database-Notes》一文中,我们也讨论了该算法的作用。

时间戳

这里时间戳的细度是毫秒级,具体代码如下,建议使用64linux系统机器,因为有vdsogettimeofday()在用户态就可以完成操作,减少了进入内核态的损耗。

uint64_t generateStamp()
{
    timeval tv;
    gettimeofday(&tv, 0);
    return (uint64_t)tv.tv_sec * 1000 + (uint64_t)tv.tv_usec / 1000;
}

默认情况下有41bit可以供使用,那么一共有T(1llu « 41)毫秒供你使用分配,年份= T / (3600 _ 24 _ 365 * 1000) = 69.7年。如果你只给时间戳分配39bit使用,那么根据同样的算法最后年份= 17.4年。总体来说,是一个很高效很方便的GUID产生算法,一个int64_t字段就可以胜任,不像现在主流128bitGUID算法,即使无法保证严格的id序列性,但是对于特定的业务,比如用做游戏服务器端的GUID产生会很方便。另外,在多线程的环境下,序列号使用atomic可以在代码实现上有效减少锁 的密度。

工作机器ID

严格意义上来说这个bit段的使用可以是进程级,机器级的话你可以使用MAC地址来唯一标示工作机器,工作进程级可以使用IP+Path来区分工作进程。如果工作机器比较少,可以使用配置文件来设置这个id是一个不错的选择,如果机器过多配置文件的维护是一个灾难性的事情。 这里的解决方案是需要一个工作id分配的进程,可以使用自己编写一个简单进程来记录分配id,或者利用Mysql auto_increment机制也可以达到效果。

工作进程与工作id分配器只是在工作进程启动的时候交互一次,然后工作进程可以自行将分配的id数据落文件,下一次启动直接读取文件里的id使用。PS:这个工作机器idbit段也可以进一步拆分,比如用前5bit标记进程id,后5bit标记线程id之类。

序列号

序列号就是一系列的自增id(多线程建议使用atomic),为了处理在同一毫秒内需要给多条消息分配id,若同一毫秒把序列号用完了,则“等待至下一毫秒”。

uint64_t waitNextMs(uint64_t lastStamp)
{
    uint64_t cur = 0;
    do {
        cur = generateStamp();
    } while (cur <= lastStamp);
    return cur;
}

代码实现

Java

public class IdWorker {
  private long workerId; // 这个就是代表了机器id
  private long datacenterId; // 这个就是代表了机房id
  private long sequence; // 这个就是代表了一毫秒内生成的多个id的最新序号

  public IdWorker(long workerId, long datacenterId, long sequence) {
    // sanity check for workerId
    // 这儿不就检查了一下,要求就是你传递进来的机房id和机器id不能超过32,不能小于0
    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;
    this.sequence = sequence;
  }

  private long twepoch = 1288834974657L;
  private long workerIdBits = 5L;
  private long datacenterIdBits = 5L;

  // 这个是二进制运算,就是5 bit最多只能有31个数字,也就是说机器id最多只能是32以内
  private long maxWorkerId = -1L ^ (-1L << workerIdBits);

  // 这个是一个意思,就是5 bit最多只能有31个数字,机房id最多只能是32以内
  private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
  private long sequenceBits = 12L;
  private long workerIdShift = sequenceBits;
  private long datacenterIdShift = sequenceBits + workerIdBits;
  private long timestampLeftShift =
    sequenceBits + workerIdBits + datacenterIdBits;
  private long sequenceMask = -1L ^ (-1L << sequenceBits);
  private long lastTimestamp = -1L;

  public long getWorkerId() {
    return workerId;
  }

  public long getDatacenterId() {
    return datacenterId;
  }

  public long getTimestamp() {
    return System.currentTimeMillis();
  }

  // 这个是核心方法,通过调用nextId()方法,让当前这台机器上的snowflake算法程序生成一个全局唯一的id
  public synchronized long nextId() {
    // 这儿就是获取当前时间戳,单位是毫秒
    long timestamp = timeGen();
    if (timestamp < lastTimestamp) {
      System.err.printf(
        "clock is moving backwards. Rejecting requests until %d.",
        lastTimestamp
      );
      throw new RuntimeException(
        String.format(
          "Clock moved backwards. Refusing to generate id for %d milliseconds",
          lastTimestamp - timestamp
        )
      );
    }

    // 下面是说假设在同一个毫秒内,又发送了一个请求生成一个id
    // 这个时候就得把seqence序号给递增1,最多就是4096
    if (lastTimestamp == timestamp) {
      // 这个意思是说一个毫秒内最多只能有4096个数字,无论你传递多少进来,
      //这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围
      sequence = (sequence + 1) & sequenceMask;
      if (sequence == 0) {
        timestamp = tilNextMillis(lastTimestamp);
      }
    } else {
      sequence = 0;
    }

    // 这儿记录一下最近一次生成id的时间戳,单位是毫秒
    lastTimestamp = timestamp;

    // 这儿就是最核心的二进制位运算操作,生成一个64bit的id
    // 先将当前时间戳左移,放到41 bit那儿;将机房id左移放到5 bit那儿;将机器id左移放到5 bit那儿;将序号放最后12 bit
    // 最后拼接起来成一个64 bit的二进制数字,转换成10进制就是个long型
    return (
      ((timestamp - twepoch) << timestampLeftShift) |
      (datacenterId << datacenterIdShift) |
      (workerId << workerIdShift) |
      sequence
    );
  }

  private long tilNextMillis(long lastTimestamp) {
    long timestamp = timeGen();

    while (timestamp <= lastTimestamp) {
      timestamp = timeGen();
    }
    return timestamp;
  }

  private long timeGen() {
    return System.currentTimeMillis();
  }

  //---------------测试---------------
  public static void main(String[] args) {
    IdWorker worker = new IdWorker(1, 1, 1);

    for (int i = 0; i < 30; i++) {
      System.out.println(worker.nextId());
    }
  }
}
上一页
下一页