【分布式】分布式研究

原文:https://zhuanlan.zhihu.com/p/157978714

  1. 分布式id生成策略

生成分布式Id的方法主要有以下几种:

  1. 数据库水平拆分,设置初始值和相同的自增步长。
  2. 批量申请自增ID。
  3. UUID生成。
  4. Redis的方式。
  5. 雪花算法。
  6. 百度UidGenerator算法
  7. 美团Leaf算法
  1. 数据库水平拆分,设置初始值和相同的自增步长

 

  1. 「数据库水平拆分,设置初始值和相同的自增步长」

set @@auto_increment_offset = 1;     // 设置初始值
set @@auto_increment_increment = 2;  // 设置步长

上面我说的步长一般设置为数据库的数量,这是在确保后期不会扩容的情况下,若是确定后期会有扩容情况,在前期设计的的时候可以将步长设置长一点,「预留一些初始值给后续扩容使用」

缺点:「后期可能会面对无ID初始值可分的窘境,数据库总归是数据库,抗高并发也是有限的」

优点:就是算是解决了「DB单点的问题」

  1. 「批量申请自增ID」

我:「批量申请自增ID」的解决方案可以解决无ID可分的问题,它的原理就是一次性给对应的数据库上分配一批的id值进行消费,使用完了,再回来申请。

我:在设计的初始阶段可以设计一个有初始值字段,并有步长字段的表,当每次要申请批量ID的时候,就可以去该表中申请,每次申请后「初始值=上一次的初始值+步长」

我:这样就能保持初始值是每一个申请的ID的最大值,避免了ID的重复,并且每次都会有ID使用,一次就会生成一批的id来使用,这样访问数据库的次数大大减少。

我:但是这一种方案依旧有自己的缺点,依然不能抗真正意义上的高并发。

UUID生成

UUID的核心思想是使用「机器的网卡、当地时间、一个随机数」来生成UUID。

优点:只需要调用UUID.randomUUID().toString()就可以生成,本地生成,不会消耗网络

缺点:不利于存储,16字节128位,通常是以36位长度的字符串表示,很多的场景都不适合。

          并且UUID生成的无序的字符串,查询效率低下,没有实际的业务含义,不具备自增特性,所以都不会使用UUID作为分布式ID来使用。

Redis的方式(高并发)

为了解决上面纯关系型数据库生成分布式ID无法抗高并发的问题,可以使用Redis的方式来生成分布式ID。

我:Redis本身有incr和increby 这样自增的命令,保证原子性,生成的ID也是有序的。

我:Redis基于内存操作,性能高效,不依赖于数据库,数据天然有序,利于分页和排序

我:但是这个方案也会有自己的缺点,因为增加了中间件,需要自己编码实现工作量增大,增加复杂度。

我:使用Redis的方式还要考虑持久化,Redis的持久化有两种「RDB和AOF」「RDB是以快照的形式进行持久化,会丢失上一次快照至此时间的数据」

我:「AOF可以设置一秒持久化一次,丢失的数据是秒内的」,也会存在可能上一次自增后的秒内的ID没有持久化的问题。

我:但是这种方法相对于上面的关系型数据库生成分布式ID的方法而言,已经优越了许多。

我:若是数据量比较大的话,重启Redis的时间也会比较长,可以采用Redis的集群方式。

雪花算法

我:第六种方式是「雪花算法」,也是现在市面上比较流行的生成分布式ID的方法。

我:他是采用64bit作为id生成类型,并且将64bit划分为,如下图的几段。

我:第一位作为标识位,因为Java中long类型是有符号的,因为ID位正数,所以第一位位0。

我:接着的41bit是时间戳,毫秒级位单位,注意这里的时间戳并不是指当前时间的时间戳,而是值之间差(「当前时间-开始时间」)。

我:这里的开始时间一般是指ID生成器的开始时间,是由我们程序自己指定的。

我:接着后面的10bit:包括5位的「数据中心标识ID(datacenterId)和5位的机器标识ID(workerId)」,可以最多标识1024个节点(1<<10=1024)。

我:最后的12位是序列号,12位的计数顺序支持每个节点每毫秒差生4096序列号(1<<12=4096)。

我:雪花算法使用数据中心ID和机器ID作为标识,不会产生ID的重复,并且是在本地生成,不会消耗网络,效率高,有数据显示,每秒能生成26万个ID

缺点:因为雪花算法的计算依赖于时间,若是系统时间回拨,就会产生重复ID的情况。

面试官:那对于时间回拨产生重复ID的情况,你有什么比较好的解决方案吗?

我:在雪花算法的实现中,若是其前置的时间等于当前的时间,就抛出异常,也可以关闭掉时间回拨。

我:对于回拨时间比较短的,可以等待回拨时间过后再生成ID。

面试官:你可以帮我敲一个雪花算法吗?我这键盘给你。

/**
 * 雪花算法
 * @author:黎杜
 */
public class SnowflakeIdWorker {

    /** 开始时间截 */
    private final long twepoch = 1530051700000L;

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

    /** 数据标识id的位数 */
    private final long datacenterIdBits = 5L;

    /** 最大的机器id,结果是31 */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

    /** 最大的数据标识id,结果是31 */
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

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

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

    /** 数据标识id向左移17位 */
    private final long datacenterIdShift = sequenceBits + workerIdBits;

    /** 时间截向左移22位*/
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    /** 生成序列的掩码 */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

    /** 工作机器ID(0~31) */
    private long workerId;

    /** 数据中心ID(0~31) */
    private long datacenterId;

    /** 毫秒内序列(0~4095) */
    private long sequence = 0L;

    /** 上次生成ID的时间截 */
    private long lastTimestamp = -1L;

    /**
     * 构造函数
     * @param workerId 工作ID (0~31)
     * @param datacenterId 数据中心ID (0~31)
     */
    public SnowflakeIdWorker(long workerId, long datacenterId) {
        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;
    }

    /**
     * 获得下一个ID (该方法是线程安全的)
     * @return SnowflakeId
     */
    public synchronized long nextId() {
        long timestamp = getCurrentTime();

        //如果当前时间小于上一次生成的时间戳,说明系统时钟回退过就抛出异常
        if (timestamp < lastTimestamp) {
            throw new BusinessionException("回拨的时间为:"+lastTimestamp - timestamp);
        }

        //如果是同一时间生成的,则进行毫秒内序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            //毫秒内序列溢出
            if (sequence == 0) {
                //获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {  //时间戳改变,毫秒内序列重置
            sequence = 0L;
        }

        //上次生成ID的时间截
        lastTimestamp = timestamp;

        //移位并通过或运算拼到一起组成64位的ID
        return ((timestamp - twepoch) << timestampLeftShift) // 计算时间戳
                | (datacenterId << datacenterIdShift) // 计算数据中心
                | (workerId << workerIdShift) // 计算机器ID
                | sequence; // 序列号
    }

    /**
     *获得新的时间戳
     * @param lastTimestamp 上次生成ID的时间截
     * @return 当前时间戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = getCurrentTime();
        // 若是当前时间等于上一次的1时间就一直阻塞,知道获取到最新的时间(回拨后的时间)
        while (timestamp <= lastTimestamp) {
            timestamp = getCurrentTime();
        }
        return timestamp;
    }

    /**
     * 获取当前时间
     * @return 当前时间(毫秒)
     */
    protected long getCurrentTime() {
        return System.currentTimeMillis();
    }

Leaf和UidGenerator

我:最后两种确实没有深入了解,之前有看网上的资料说美团Leaf算法需要依赖于数据库,ZK,并且也能保证去全局ID的唯一性,单项递增。

我:而百度UidGenerator算法是基于雪花算法进行实现的,也是需要借助于数据库,与雪花算法不同的是,「UidGenerator支持自定义时间戳、主句中心ID和机器ID、序列号的位数」

面试官:嗯嗯,好的,小伙子今天的面试就到这里,下次我们再见吧。

作者:公众号非科班的科班
链接: https://juejin.im/post/5f050a426fb9a07e5d76c6f3

猜你喜欢

转载自blog.csdn.net/bandaoyu/article/details/107925853