分布式id(2)— snowflake(雪花算法)

面对自增主键与UUID产生的问题,有没有解决方案呢?答案是有的,即 Snowflake(雪花算法)。

雪花算法(Snowflake)是 Twitter 公司分布式项目采用的 ID 生成算法。他呢主要是根据时间顺序,结合机器ID与序列,生成一个定长的数字。

这个数字就可以作为数据库的主键,他既可以保证是连续的也可以保证在分布式系统中是唯一的。Snowflake 每毫秒可以生成 416 万个 ID,这对于我们在分布式系统中使用的场景已经足够了。

该算法生成的是一个64位的ID,故在Java下正好可以通过8字节的long类型存放。所生成的ID结构如下所示:
在这里插入图片描述
符号位
最高位是符号位,为保证生成的ID是正数,故不使用,其值恒为0

时间戳
用来记录时间戳的毫秒数。一般地,我们会选用系统上线的时间作为时间戳的相对起点,而不使用JDK默认的时间戳起点(1970-01-01 00:00:00)。41位长度的时间戳可以保证使用69年。对于一般的项目而言,这个时间长度绝对是够用了

时间戳
用来记录时间戳的毫秒数。一般地,我们会选用系统上线的时间作为时间戳的相对起点,而不使用JDK默认的时间戳起点(1970-01-01 00:00:00)。41位长度的时间戳可以保证使用69年。对于一般的项目而言,这个时间长度绝对是够用了

序列号
最低的12位为序列号,可用于标识、区分同一个计算机在相同毫秒时间内的生产的ID

综上所述,Snowflake 雪花算法生成的ID不是随机的,而是按时间顺序升序排列的;且可以保证在分布式高并发环境下生成的ID不会发生重复

Java实现:

/**
 * Snowflake 基于雪花算法的ID生成器
 */
public class SnowflakeIdGenerator {

    /**
     * ID中41位时间戳的起点 (2020-01-01 00:00:00.00)
     * @apiNote 一般地,选用系统上线的时间
     */
    private final long startPoint = 1577808000000L;

    /**
     * 序列号位数
     */
    private final long sequenceBits = 12L;

    /**
     * 机器ID位数
     */
    private final long workerIdBits = 5L;

    /**
     * 数据中心ID位数
     */
    private final long dataCenterIdBits = 5L;

    /**
     * 序列号最大值, 4095
     * @apiNote 4095 = 0xFFF,其相当于是序列号掩码
     */
    private final long sequenceMask = -1L^(-1L<<sequenceBits);

    /**
     * 机器ID最大值, 31
     */
    private final long maxWorkerId = -1L^(-1L<<workerIdBits);

    /**
     * 数据中心ID最大值, 31
     */
    private final long maxDataCenterId = -1L^(-1L<<dataCenterIdBits);

    /**
     * 机器ID左移位数, 12
     */
    private final long workerIdShift = sequenceBits;

    /**
     * 数据中心ID左移位数, 12+5
     */
    private final long dataCenterIdShift = sequenceBits + workerIdBits;

    /**
     * 时间戳左移位数, 12+5+5
     */
    private final long timeStampShift = sequenceBits + workerIdBits + dataCenterIdBits;

    /**
     * 数据中心ID, Value Range: [0,31]
     */
    private long dataCenterId;

    /**
     * 机器ID, Value Range: [0,31]
     */
    private long workerId;

    /**
     * 相同毫秒内的序列号, Value Range: [0,4095]
     */
    private long sequence = 0L;

    /**
     * 上一个生成ID的时间戳
     */
    private long lastTimeStamp = -1L;

    /**
     * 构造器
     * @param dataCenterId  数据中心ID
     * @param workerId 机器中心ID
     */
    public SnowflakeIdGenerator(Long dataCenterId, Long workerId) {
        if(dataCenterId==null || dataCenterId<0 || dataCenterId>maxDataCenterId
            || workerId==null || workerId<0 || workerId>maxWorkerId) {
            throw new IllegalArgumentException("输入参数错误");
        }
        this.dataCenterId = dataCenterId;
        this.workerId = workerId;
    }

    /**
     * 获取ID
     * @return
     */
    public synchronized long nextId() {
        long currentTimeStamp = System.currentTimeMillis();
        //当前时间小于上一次生成ID的时间戳,系统时钟被回拨
        if( currentTimeStamp < lastTimeStamp ) {
            throw new RuntimeException("系统时钟被回拨");
        }

        // 当前时间等于上一次生成ID的时间戳,则通过序列号来区分
        if( currentTimeStamp == lastTimeStamp ) {
            // 通过序列号掩码实现只取 (sequence+1) 的低12位结果,其余位全部清零
            sequence = (sequence + 1) & sequenceMask;
            if(sequence == 0) { // 该时间戳下的序列号已经溢出
                // 阻塞等待下一个毫秒,并获取新的时间戳
                currentTimeStamp = getNextMs(lastTimeStamp);
            }
        } else {    // 当前时间大于上一次生成ID的时间戳,重置序列号
            sequence = 0;
        }

        // 更新上次时间戳信息
        lastTimeStamp = currentTimeStamp;

        // 生成此次ID
        long nextId = ((currentTimeStamp-startPoint) << timeStampShift)
                | (dataCenterId << dataCenterIdShift)
                | (workerId << workerIdShift)
                | sequence;

        return nextId;
    }

    /**
     * 阻塞等待,直到获取新的时间戳(下一个毫秒)
     * @param lastTimeStamp
     * @return
     */
    private long getNextMs(long lastTimeStamp) {
        long timeStamp = System.currentTimeMillis();
        while(timeStamp<=lastTimeStamp) {
            timeStamp = System.currentTimeMillis();
        }
        return timeStamp;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值