数据存储:MYSQL之雪花算法

背景

        在数据量指数级增长的公司中,单机数据库已经不能满足需求了,开始使用了分布式架构。但是分布式架构带来了一系列问题,ID的生成方式就变成了其中一个问题。传统的auto_crement在分布式中会造成id冲突,而UUID,又会造成广泛的页分裂。雪花算法便是广泛应用的解决方案。

结构

雪花算法是Twitter公司采用的开源的id生成算法。雪花算法晖生成一个64位的long行整数,这里的位是二进制位,不是十进制位。

        1位的符号位:1表示负数,0表示正数,所以基本不用关心。

        41位的时间戳:精确到毫秒,可以保障id的顺序性。

        10位的机器码:最多可以支持配置1024台机器。包括5位的数据中心id和5位的机器id。

        12位的序列号:每毫秒都会重置,表示1毫秒内生成的第几个id。

private Long twepoch = 1288834974657L;
...
public synchronized Long nextId() {
    ...
    ((timestamp - twepoch) << timestampLeftShift)
    ...
}

        在计算时间戳的时候,引入了一个变量twepoch。引入twepoch是为了让算法可以使用的尽量久一些。如果单纯使用41位表示当前时间戳的话,41位的最大值转换为十进制是2199023255551。再转换成时间:2039-09-07 23:47:35,意味着这个算法只能使用到2039年。引入twepoch后,根据twepoch设置的值,41位时间戳足可以使用69年。

public synchronized Long nextId() {
    ...
    // 出去当前毫秒内,序列号不需要重置
    if (lastTimestamp == timestamp) {
        // 确保序列号处于0~4095
        sequence = (sequence + 1) & sequenceMask;
        if (sequence == 0) {
            timestamp = tilNextMillis(lastTimestamp);
        }
    } else {
        sequence = 0;
    }
    ...
}
...

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

         这块是计算序列号逻辑的代码。对于当前时间戳与上次生成id的时间戳相等,sequence就会继续+1,否则sequence会重置为0。当然会出现序列号分配不够的问题,为了预防这种情况的发生,代码中使用(sequence + 1)与sequenceMask进行按位与运算,保证sequence处于0~4095之间。如果4095不够了,那么只能调用tilNextMillis函数等到下一毫秒再继续生成了。

代码

public class IdWorker {

    // 机器id
    private long workerId;
    // 数据中心id
    private long datacenterId;
    // 12位的序列号
    private long sequence;

    // 初始时间戳-自定义
    private Long twepoch = 1288834974657L;

    // 机器ID二进制位数-5位
    private Long workerIdBits = 5L;

    // 数据中心ID二进制位数-5位
    private Long datacenterIdBits = 5L;

    // 机器ID的最大值-31
    private Long maxWorkerId = -1L ^ (-1L << workerIdBits);

    // 数据中心ID的最大值-31
    private Long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    // 序列号二进制位数-12位
    private Long sequenceBits = 12L;

    // 机器ID左移位数-12位
    private Long workerIdShift = sequenceBits;

    // 数据中心ID左移位数-17位
    private Long datacenterIdShift = sequenceBits + workerIdBits;

    // 时间戳左移位数-22位
    private Long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    // 序列号最大值-4095
    private Long sequenceMask = -1L ^ (-1L << sequenceBits);

    // 上次时间戳
    private Long lastTimestamp = -1L;

    public IdWorker(long workerId, long datacenterId, long sequence) {
        // 校验workId合法性
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        // 校验datacenterId合法性
        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;
    }

    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));
        }
        // 出去当前毫秒内,序列号不需要重置
        if (lastTimestamp == timestamp) {
            // 确保序列号处于0~4095
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }

        // 刷新上次时间戳。
        lastTimestamp = timestamp;

        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 idWorker = new IdWorker(10, 20, 0);
        for (int i = 0; i <10000; i++) {
            log.info("" + idWorker.nextId());
        }
    }
}

雪花算法比UUID强在哪?

雪花算法是单机有序的,UUID是完全无序的。顺序ID的优势是减少页分裂。

代码方面参考自:理解分布式id生成算法SnowFlake - SegmentFault 思否

你可能感兴趣的:(MySQL,mysql,java,数据库)