一、概述

    在学习Mysql调优的schema与数据类型优化内容时,有讲到数据库表id的设计,分布式系统中我们如何保证可以利用id进行时间排序呢,那么就需要我们今天的主角--雪花算法。

二、分析

1、常见主键生成策略

    一般对于系统的实体类主键,我们一般采用如下两种策略:

  • int 变量自增:采用数据库自增功能,id采用整数类型进行自增。
  • 字符串 UUID:采用UUID生成工具可生成随机字符串。

2、问题

  • int类型的主键:该方式生成的主键一般是连续的数字,主键有一定的顺序,可排序。但是在进行数据迁移时,很容易产生主键冲突。
  • UUID:这种方式可以有效避免在数据迁移时因为主键冲突而导致失败的问题。但是该方式生成的主键是无序的,需要指定某字段进行排序,才可以知道插入数据的顺序。就算加了时间戳,但是在分布式系统中,当并发量高的情况下,根据时间戳排序就显得不那么准确了。

3、有序唯一的主键生成策略-雪花算法

3.1、雪花算法简介及核心思想

    SnowFlake 算法,是 Twitter 开源的分布式 id 生成算法。其核心思想就是:使用一个 64 bit 的 long 型的数字作为全局唯一 id。在分布式系统中的应用十分广泛,且ID 引入了时间戳,基本上保持自增的。

3.2、原理

3.2.1、算法图示

MySQL雪花id实现 mysql雪花算法索引_MySQL雪花id实现

3.2.2、雪花算法的简单描述:

  • 最高位是符号位,始终为0,不可用。
  • 41位的时间序列,精确到毫秒级,41位的长度可以使用69年。时间位还有一个很重要的作用是可以根据时间进行排序。
  • 10位的机器标识,10位的长度最多支持部署1024个节点。
  • 12位的计数序列号,序列号即一系列的自增id,可以支持同一节点同一毫秒生成多个ID序号,12位的计数序列号支持每个节点每毫秒产生4096个ID序号。

3.2.3、如果单节点并发找过最大生成序列号

    通过上边表述,我们知道,单节点每毫秒最大生成4096个ID,如果极端情况,如果并发量特别大,超过了4096个。这是我们可采用线程阻塞的方式,阻塞1毫秒。等下一毫秒在生成id。

4、算法实现

4.1、雪花算法实现

package com.xblog.commons.utils;
 
/**
 * desc: 雪花id生成器
 * author: xuebin3765@163.com
 * date: 2019/11/28
 */
class SnowflakeIdWorker {
 
    //得到二进制样例 10111100110111110011001010100001100111111100001000000000000
 
    /** 开始时间截 (2015-01-01) */
    private final long twepoch = 1420041600000L;
 
    //每一部分占用的位数
    /** 机器id所占的位数 */
    private final byte workerIdBits = 5;
    /** 数据标识id所占的位数 */
    private final byte dataCenterIdBits = 5;
    /** 序列在id中占的位数 */
    private final byte sequenceBits = 12;
 
    //每一部分的最大值
    /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
    /** 支持的最大数据标识id,结果是31 */
    private final long maxDatacenterId = -1L ^ (-1L << dataCenterIdBits);
    /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);
 
    //每一部分向左的位移
    /** 机器ID向左移12位 */
    private final long workerIdShift = sequenceBits;
    /** 数据标识id向左移17位(12+5) */
    private final long datacenterIdShift = sequenceBits + workerIdBits;
    /** 时间截向左移22位(5+5+12) */
    private final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;
 
    /** 工作机器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 工作组
     * @param dataCenterId 数据中心
     */
    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", workerId));
        }
        if (dataCenterId > maxDatacenterId || dataCenterId < 0){
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", dataCenterId));
        }
        this.workerId = workerId;
        this.dataCenterId = dataCenterId;
    }
 
    /**
     * 获取雪花id
     * @return uuid
     */
    synchronized long nextId(){
        // 获取当前毫秒值
        long timestamp = getCurrentTime();
 
        //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if (timestamp < lastTimestamp){
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }
 
        //如果是同一时间生成的,则进行毫秒内序列
        if (lastTimestamp == timestamp){
            sequence = (sequence + 1) & sequenceMask;//相同毫秒内,序列号自增
            //毫秒内序列溢出 //同一毫秒的序列数已经达到最大
            if (sequence == 0) {
                //阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }else {
            //时间戳改变,毫秒内序列重置
            sequence = 0L;
        }
 
        //上次生成ID的时间截
        lastTimestamp = timestamp;
 
        return ((timestamp - twepoch) << timestampLeftShift) //
                | (dataCenterId << datacenterIdShift) //
                | (workerId << workerIdShift) //
                | sequence;
 
    }
 
    /**
     * 阻塞到下一个毫秒,直到获得新的时间戳
     * @param lastTimestamp 上次生成ID的时间截
     * @return 当前时间戳
     */
    private long tilNextMillis(long lastTimestamp){
        long timestamp = getCurrentTime();
        while (timestamp <= lastTimestamp){
            timestamp = getCurrentTime();
        }
        return timestamp;
    }
 
    /**
     * 获取系统时间戳
     * @return
     */
    private long getCurrentTime(){
        return System.currentTimeMillis();
    }
}

4.2、单例模式实现雪花ID

package com.xblog.commons.utils;
 
/**
 * Description:
 * Created by Administrator
 * Date 2019/11/28 22:52
 */
public class SnowflakeUUIDUtil {
 
    private final static SnowflakeIdWorker snowflakeIdWorker = new SnowflakeIdWorker(1,1);
 
    private SnowflakeUUIDUtil (){}
 
    public static String getUuid(){
        return String.valueOf(snowflakeIdWorker.nextId());
    }
}

三、总结