关于SnowFlake-雪花算法的实现和思考

最近在重构公司的点评系统,为了解决分布式唯一主键ID问题,于是仔细研究了一把snowflake算法,在此做个笔记方便下次使用。

1.雪花算法(snowflake)

分布式雪花算法是Twitter公司为了解决分布式唯一主键ID问题,而酝酿出来的算法。最初Twitter把存储系统从MySQL迁移到Cassandra,因为Cassandra没有顺序ID生成机制,所以twitter的snowflake大神就开发了这样一套分布式系统全局唯一ID生成服务。snowflake巧妙有效的运用了一个64-bit数据类型,以划分命名空间来生成ID的一种算法,这个方法把64-bit分别划分成多段,分开来标示机器、时间等。如下图我们先来看看snowflake的构成:

2.snowflake结构描述

  • (1)最高位是符号位,始终是0(因为snowflake始终是正数),不可用。
  • (2)41位的时间戳,精确到毫秒级,41位的长度可以使用69年,时间位有个重要的作用就是能保证生成的全局唯一ID可以根据时间进行排序。需要注意的是时间戳存储的是时间戳的差值(当前时间截 - 开始时间截) 后得到的值,开始时间戳一般是我们id生成器开始使用的时间。41位时间戳可以使用的年限计算:[(1L << 41) - (当前时间戳 - 开始时间戳) ] / (1000L * 60 * 60 * 24 * 365) ,当开始时间戳接近当前时间戳时,公式约等于 (1L << 41)  / (1000L * 60 * 60 * 24 * 365) = 69年。
  • (3)10位的机器标识由5位机房id(datacenterId) 和 5位机器id(workerId)组成,10位的长度最多支持部署1024个节点(32个IDC * 32台机器 = 1024个节点)。
  • (4)12位的计数序列号,序列号是一系列的自增id,即支持同一节点同一毫秒生成多个ID序号,12位的计数序列号支持每个节点每毫秒产生4096个序号ID,理论上snowflake方案的QPS约为409.6w/s。

3.算法实现

最近在重构公司的点评系统,用到唯一ID进行分库分表,唯一ID的生成就使用到了snowflake算法;不过公司只有M6和亦庄两个机房、8台服务器,对IDC划分没有需求。我们为了计算方便就没有将10bit的工作机器ID再拆分,而是用一个machineId来代替不同的机器,这样做减少了维护的成本。具体实现代码如下:

public class SnowflakeIdWorker {
    /**
     * 开始时间戳(2020-01-01 00:00:00)
     */
    private long startTimeStamp = 1577808000000L;
    
    /**
     * 序列号占的位数
     */
    private long sequenceBites = 12;

    /**
     * 机器Id占的位数
     */
    private long machineIdBites = 10;
    
    /**
     * 时间戳位移位数
     */
    private long timestampOffset  = sequenceBites + machineIdBites;
    
    /**
     * 序列号
     */
    private volatile long sequence;

    /**
     * 序列号号最大值12位(0b111111111111=0xfff=4095)
     */
    private long maxSequence = ~(-1L << sequenceBites);
    
    /**
     * 工作机器Id(根据自己业务需要,可以拆成: 5位机房id(dataCenterId) + 5位机器id(workerId))
     */
    private int machineId;
    
    /**
     * 工作机器Id最大值10位(0d1111111111=0x3ff=1023)
     */
    private int maxMachineId = ~(-1<<machineIdBites);
    
    /**
     * 上次获取序列号的时间
     */
    private volatile long lastTimestamp;


    public SnowflakeIdWorker(int machineId) {
        //判断机器id是否达到上线
        if(machineId < 0 || machineId > maxMachineId){
            throw new IllegalArgumentException(String.format("machine Id can't be greater than %d or less than 0", maxMachineId));
        }
        this.machineId = machineId;
    }

    /**
     * 同一台服务器上同步获取序列号id
     * @return
     */
    public synchronized long getSequenceId(){
        long timestamp = System.currentTimeMillis();
        if(lastTimestamp == timestamp){
            //1ms内出现并发,增加序列号
            sequence++;
            //1ms内出现溢出
            if((maxSequence & sequence) == 0){
                //阻塞取下一个毫秒时间
                timestamp = getNextTimeMillis();
            }
        }else {
            sequence = 0;
        }
        lastTimestamp = timestamp;
        
        //移位加运算拼到一起组成64位的ID
        return (timestamp - startTimeStamp) << timestampOffset 
                | (machineId << sequenceBites) 
                | sequence;
    }

    /**
     * 获取下一个毫秒时间
     * @return
     */
    public long getNextTimeMillis(){
        long timestamp = System.currentTimeMillis();
        //当一毫秒内序列号溢出时,自旋取下一个毫秒
        while (timestamp <= lastTimestamp){
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
}

10-bit机器标识可以分别表示1024台机器,如果我们对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,可以根据自身需求定义。

4.雪花算法优缺点

(1)优点

  • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
  • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
  • 可以根据自身业务特性分配bit位,非常灵活。

(2)缺点

  • 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

5.机器id分配方案

  • (1)Apollo配置中心

当服务集群数量较小的情况下,维护比较简单完全可以手动配置;可以使用Apollo配置中心来维护机器Id(例子中的machineId)。大致思路是在Apollo里配置一个Map,key=服务器ip、value=machineId,每次获取machineId时,先获取服务器ip,再从配置里获取machineId,就可以实现每台服务器的machineId不一样。缺点就是过度依赖于Apollo配置中心,如果配置中心挂掉了服务不可用。

  • (2)关系型数据库

当集群规模增大时,工作机器ID(例子中的machineId)可以用关系型数据库来维护。大致思路是创建一个registry表(字段有主键ID、服务器hostname、create_time、update_time)来存储服务器的hostname。每次获取machineId时,线程获取服务器的hostname再去查询registry表的主键ID得到唯一的machineId。缺点就是过度依赖DB数据库,如果数据库挂掉了服务不可用。

  • (3)ZK持久顺序节点

当服务集群规模较大时,动手配置成本太高,可以使用Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerID,为了弱依赖ZooKeeper,会在本机文件系统上缓存一个工作机器Id(例子中的machineId)文件。当ZK出现问题,恰好机器出现问题需要重启时,能保证服务能够正常启动,这样做到了对三方组件的弱依赖,一定程度上提高了SLA。(美团Leaf的解决方案)

6.常见问题总结

  • (1)同一台机器上为了防止并发获取序列号重复问题,需要在获取序列号方法上加锁synchronized 或 lock。
  • (2)有很多人在使用snowflake时,41位的时间戳存储的并不是时间差值,而是当前系统时间戳。这样虽然能正常使用,但时间久了会出现溢出问题,最多能顶个19年。
public static void main(String[] args){
    //使用当前时间戳存储,雪花算法使用的年限
    System.out.println(((1L<<41) - System.currentTimeMillis()) / (1000L * 60 * 60 * 24 * 365));
}

//输出结果:19
  • (3)一般的公司节点数达不到1024个节点,可以适当减少机器标识位数,增加计数序列号位数,提高1毫秒内的并发量。比如:机器标识位用5bit,序列号位用17bit,这时每个节点每毫秒产生131072个序号ID。不过一般的公司达也不到这个量。

                                                                                                                     2020年05月26号   于北京记

猜你喜欢

转载自blog.csdn.net/Seky_fei/article/details/106343452