雪花算法实现解析(附使用雪花算法主键冲突原因和解决方案)

背景:但是线上经常出现主键冲突问题导致数据插入失败,严重影响现有的主键生成都是采用雪花算法,业务。

雪花算法源码解析(以mybatisplus里源码参照):

if (lastTimestamp == timestamp) {
    // 相同毫秒内,序列号自增
    sequence = (sequence + 1) & sequenceMask;
    if (sequence == 0) {
        // 同一毫秒的序列数已经达到最大
        timestamp = tilNextMillis(lastTimestamp);
    }
} else {
    // 不同毫秒内,序列号置为 1 - 3 随机数
    sequence = ThreadLocalRandom.current().nextLong(1, 3);
}

lastTimestamp = timestamp;

// 时间戳部分 | 数据中心部分 | 机器标识部分 | 序列号部分
return ((timestamp - twepoch) << timestampLeftShift)
    | (datacenterId << datacenterIdShift)
    | (workerId << workerIdShift)
    | sequence;
 

雪花算法是一个64位的2进制数字组成,转换成10进制一般是19位数。其中 第一位数是不使用的,当前时间的毫秒数占其中的41位,5位是数据中心标识,5位是机器码标识,还有12位是自增序列,第一次生成的自增序列是1到3之前的随机数。

/**
 * 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
 */
private final long twepoch = 1288834974657L;
/**
 * 机器标识位数
 */
private final long workerIdBits = 5L;
private final long datacenterIdBits = 5L;
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/**
 * 毫秒内自增位
 */
private final long sequenceBits = 12L;
private final long workerIdShift = sequenceBits;
private final long datacenterIdShift = sequenceBits + workerIdBits;
/**
 * 时间戳左移动位
 */
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private final long sequenceMask = -1L ^ (-1L << sequenceBits);

private final long workerId;

/**
 * 数据标识 ID 部分
 */
private final long datacenterId;
/**
 * 并发控制
 */
private long sequence = 0L;
/**
 * 上次生产 ID 时间戳
 */
private long lastTimestamp = -1L;
 

在mybatisplus中采用的是雪花偏移算法,时间戳部分减去了一个1288834974657L的固定常量,然后向左固定位移22位,最大数据中心标识是一个5位二进制,向左位移17位,机器码也是一个5位二进制,向左位移12位,剩下12位是自增序列,如果时间戳部分相等 那么自增序列会加1,如果不想等 那么自增序列就是一个1-3之间的随机数。由此可知雪花算法可以满足每毫秒4096个主键生成。并且在生成主键时采用了synchronized关键字保证了方法的原子性。

public synchronized long nextId() {
    long timestamp = timeGen();
    //闰秒
    if (timestamp < lastTimestamp) {
        long offset = lastTimestamp - timestamp;
        if (offset <= 5) {
            try {
                wait(offset << 1);
                timestamp = timeGen();
                if (timestamp < lastTimestamp) {
                    throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        } else {
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
        }
    }

    if (lastTimestamp == timestamp) {
        // 相同毫秒内,序列号自增
        sequence = (sequence + 1) & sequenceMask;
        if (sequence == 0) {
            // 同一毫秒的序列数已经达到最大
            timestamp = tilNextMillis(lastTimestamp);
        }
    } else {
        // 不同毫秒内,序列号置为 1 - 3 随机数
        sequence = ThreadLocalRandom.current().nextLong(1, 3);
    }

    lastTimestamp = timestamp;

    // 时间戳部分 | 数据中心部分 | 机器标识部分 | 序列号部分
    return ((timestamp - twepoch) << timestampLeftShift)
        | (datacenterId << datacenterIdShift)
        | (workerId << workerIdShift)
        | sequence;
}
 

但是synchronized不能保证在分布式集群环境下方法的原子性,那么在集群环境下雪花算法的唯一性就需要用数据中心标识跟机器码标识来进行区分。

protected static long getDatacenterId(long maxDatacenterId) {
    long id = 0L;
    try {
        InetAddress ip = InetAddress.getLocalHost();
        NetworkInterface network = NetworkInterface.getByInetAddress(ip);
        if (network == null) {
            id = 1L;
        } else {
            byte[] mac = network.getHardwareAddress();
            if (null != mac) {
                id = ((0x000000FF & (long) mac[mac.length - 1]) | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
                id = id % (maxDatacenterId + 1);
            }
        }
    } catch (Exception e) {
        logger.warn(" getDatacenterId: " + e.getMessage());
    }
    return id;
}
 

数据标识其实就是取当前机器的Mac地址的后两位数 ,然后对这后两位数进行32取余数,因为数据标识最大只能为31,只占5bit。如果获取为空,那么数据标识为1.

雪花算法实现解析(附使用雪花算法主键冲突原因和解决方案)_第1张图片

机器码部分则是数据标识部分加上当前jvm进程的PID的哈希值对32进行取余数,如果获取不到,那么就直接取的数据标识的哈希值取余。

protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
    StringBuilder mpid = new StringBuilder();
    mpid.append(datacenterId);
    String name = ManagementFactory.getRuntimeMXBean().getName();
    if (StringUtils.isNotBlank(name)) {
        /*
         * GET jvmPid
         */
        mpid.append(name.split(StringPool.AT)[0]);
    }
    /*
     * MAC + PID 的 hashcode 获取16个低位
     */
    return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
}
 

线上主键冲突案例分析:

源码已经阅读完了,那么为什么线上会报主键冲突呢?

知晓了雪花算法的生成方式后那么可以知道,以我们的业务量来说单台机器肯定是不会重复生成ID的,出现问题的原因应该就是数据标识跟机器码,分析线上报错案例,报错IP为172.23.50.211,冲突主键为1506473193558306817。查询日志发现这个主键生成业务是在IP为172.23.52.253上。

解析冲突主键1506473193558306817为2进制后补齐64位 001010011101000000100010110001101001101101100001111000000000001

获取到数据标识位跟机器码位为:10110 01110

account服务总共有4个pod,查询日志分析,其中有三个pod的数据标识和机器码位为:10110 01110,那么在同一毫秒的时候生成的主键ID就冲突了。

那么现在的问题就是解决数据标识跟机器码相同的问题。线上的服务是使用的Docker容器,获取到的Mac地址和进程ID都是相同的。所以导致了生成的主键ID冲突

怎么样去解决这个问题呢?

问题解决方案:

现在的主键生成主要是依赖mybatisplus的雪花生成算法,在实体上配置主键生成类型

雪花算法实现解析(附使用雪花算法主键冲突原因和解决方案)_第2张图片

数据中心ID和机器码是在项目启动时加载:

雪花算法实现解析(附使用雪花算法主键冲突原因和解决方案)_第3张图片

查询资料得知mybatisplus提供了配置参数GlobalConfig可以配置机器码和数据标识,

优先会取配置里的参数,所以我们只需要在yml文件中配置即可

雪花算法实现解析(附使用雪花算法主键冲突原因和解决方案)_第4张图片

将workerId和datacenterId配置成随机生成,因为他们只能占5位 所以只能在0和31里进行随机。

如果是项目里写的雪花生成算法,那么可以直接重写workerId和datacenterId的获取逻辑即可。

当然,这种写法也有可能生成workerId和datacenterId相同的情况,但是这种情况几率很小,1024/1的概率。已经是可以接受的范围。

还有一种采用redis实现的获取不重复机器码实现。

采用redis给服务器自增workerId和datacenterId。

你可能感兴趣的:(算法)