mybatis-plus内置雪花算法主键重复问题

Mybatis-Plus 使用ID_WORKER生成主键id重复

问题描述

目前项目使用的id是mybatis-plus 内置的主键生成策略 ID_WORKER ,最近测试在做性能压测,部署架构是单服务集群的部署方式,然后就发现了id重复的异常,异常如下

在这里插入图片描述

问题分析

首先分析的是id生成是不是就是重复了,先关掉其中一台机器,单机跑,这个时候发现压到1000的并发都没有出现过id重复,这个说明单机情况下不存在id重复问题,说明只有集群的情况下才会出现。

再分析一下id生成的几个要素,雪花算法的核心能影响到id生成的几个因素:1.服务器时间2.workId(机器 ID 部分)3.datacenterId(数据标识 ID 部分)。先检查了一下服务器时间,都是一样的,然后再看一下workId的生成,我们先看一下源码。

public Sequence() {
    this.datacenterId = getDatacenterId(maxDatacenterId);
    this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
}
//获取workerId
protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
        StringBuilder mpid = new StringBuilder();
        mpid.append(datacenterId);
        //代表正在运行的Java虚拟机的名称。
        String name = ManagementFactory.getRuntimeMXBean().getName();
        if (StringUtils.isNotEmpty(name)) {
            /*
             * GET jvmPid
             */
            mpid.append(name.split(StringPool.AT)[0]);
        }
        /*
         * MAC + PID 的 hashcode 获取16个低位
         */
        return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
    }

这里生成的workerId主要核心影响就是获取运行的虚拟机名称,现在猜测就是有可能是由于两台机的虚拟机名称可能一样,导致拿到的workerId一样。这个没有具体去验证,猜测是这样的。那我们尝试修改一下这个workerId的值。

问题解决

1.先设置workerId为随机数,这样保证每个机器部署的时候拿到的都是随机数

# 设置随机
mybatis-plus.global-config.worker-id: ${random.int}

这里你启动项目的时候回发现一个异常

在这里插入图片描述

异常很明显,这里的worer id 必选小于31,那我们就需要修改一下随机数

# 设置随机
mybatis-plus.global-config.worker-id: ${random.int(1,31)}

这个时候我们先看一下我们设置参数有没有生效,为了比较明显看到效果,我们直接设置worker-id为一个固定值20,再断点看一下,我们找到com.baomidou.mybatisplus.core.toolkit.IdWorker这个核心类,获取id的核心方法是com.baomidou.mybatisplus.core.toolkit.IdWorker#getId,那我们就在这里加一个断点看下

public class IdWorker {

    /**
     * 主机和进程的机器码
     */
    private static Sequence WORKER = new Sequence();
	//获取id
    public static long getId() {
        return WORKER.nextId();
    }

    public static String getIdStr() {
        return String.valueOf(WORKER.nextId());
    }

    /**
     * <p>
     * 有参构造器
     * </p>
     *
     * @param workerId     工作机器 ID
     * @param datacenterId 序列号
     */
    public static void initSequence(long workerId, long datacenterId) {
        WORKER = new Sequence(workerId, datacenterId);
    }

    /**
     * <p>
     * 使用ThreadLocalRandom获取UUID获取更优的效果 去掉"-"
     * </p>
     */
    public static String get32UUID() {
        ThreadLocalRandom random = ThreadLocalRandom.current();
        return new UUID(random.nextLong(), random.nextLong()).toString().replace(StringPool.DASH, StringPool.EMPTY);
    }

}

断点后的结果是:

在这里插入图片描述

这个时候看到workerId没有生效,我们继续分析下源码。

项目启动的时候,mybatis-plus 会调用一个com.baomidou.mybatisplus.core.MybatisConfiguration#init方法来初始化配置信息,我们看下代码

public void init(GlobalConfig globalConfig) {
        // 初始化 Sequence
    //这里需要同时设置workerId和datacenterId
        if (null != globalConfig.getWorkerId()
            && null != globalConfig.getDatacenterId()) {
            IdWorker.initSequence(globalConfig.getWorkerId(), globalConfig.getDatacenterId());
        }
        // 打印 Banner
        if (globalConfig.isBanner()) {
            System.out.println(" _ _   |_  _ _|_. ___ _ |    _ ");
            System.out.println("| | |\\/|_)(_| | |_\\  |_)||_|_\\ ");
            System.out.println("     /               |         ");
            System.out.println("                        "+MybatisPlusVersion.getVersion()+" ");
        }
    }

我们发现workerId和datacenterId必须同时设置才会获取我们设置的值。

那我们就修改配置一下

# 设置随机
mybatis-plus.global-config.worker-id: ${random.int(1,31)}
mybatis-plus.global-config.datacenter-id: ${random.int(1,31)}

ties

设置随机

mybatis-plus.global-config.worker-id: ${random.int(1,31)}
mybatis-plus.global-config.datacenter-id: ${random.int(1,31)}

这样设置以后发现终于生效了,然后部署一下,问题终于解决了。这里问题虽然解决了,但是workerId重复其实没有实际验证过,如果有验证过的同学欢迎留言。

  • 13
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 29
    评论
高并发分布式系统中生成全局唯一Id汇总 数据在分片时,典型的是分库分表,就有一个全局ID生成的问题。 单纯的生成全局ID并不是什么难题,但是生成的ID通常要满足分片的一些要求: 1 不能有单点故障。 2 以时间为序,或者ID里包含时间。这样一是可以少一个索引,二是冷热数据容易分离。 3 可以控制ShardingId。比如某一个用户的文章要放在同一个分片内,这样查询效率高,修改也容易。 4 不要太长,最好64bit。使用long比较好操作,如果是96bit,那就要各种移位相当的不方便,还有可能有些组件不能支持这么大的ID。 一 twitter twitter在把存储系统从MySQL迁移到Cassandra的过程中由于Cassandra没有顺序ID生成机制,于是自己开发了一套全局唯一ID生成服务:Snowflake。 1 41位的时间序列(精确到毫秒,41位的长度可以使用69年) 2 10位的机器标识(10位的长度最多支持部署1024个节点) 3 12位的计数顺序号(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号) 最高位是符号位,始终为0。 优点:高性能,低延迟;独立的应用;按时间有序。 缺点:需要独立的开发和部署。 原理 java 实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 public class IdWorker { private final long workerId; private final static long twepoch = 1288834974657L; private long sequence = 0L; private final static long workerIdBits = 4L; public final static long maxWorkerId = -1L ^ -1L << workerIdBits; private final static long sequenceBits = 10L; private final static long workerIdShift = sequenceBits; private final static long timestampLeftShift = sequenceBits + workerIdBits; public final static long sequenceMask = -1L ^ -1L < this.maxWorkerId || workerId < 0) { throw new IllegalArgumentException(String.format( "worker Id can't be greater than %d or less than 0", this.maxWorkerId)); } this.workerId = workerId; } public synchronized long nextId() { long timestamp = this.timeGen(); if (this.lastTimestamp == timestamp) { this.sequence = (this.sequence + 1) & this.sequenceMask; if (this.sequence == 0) { System.out.println("###########" + sequenceMask); timestamp = this.tilNextMillis(this.lastTimestamp); } } else { this.sequence = 0; } if (timestamp < this.lastTimestamp) { try { throw new Exception( String.format( "Clock moved backwards. Refusing to generate id for %d milliseconds", this.lastTimestamp - timestamp)); } catch (Exception e) { e.printStackTrace(); } } this.lastTimestamp = timestamp; long nextId = ((timestamp - twepoch << timestampLeftShift)) | (this.workerId << this.workerIdShift) | (this.sequence); System.out.println("timestamp:" + timestamp + ",timestampLeftShift:" + timestampLeftShift + ",nextId:" + nextId + ",workerId:" + workerId + ",sequence:" + sequence); return nextId; } private long tilNextMillis(final long lastTimestamp) { long timestamp = this.timeGen(); while (timestamp <= lastTimestamp) { timestamp = this.timeGen(); } return timestamp; } private long timeGen() { return System.currentTimeMillis(); } public static void main(String[] args){ IdWorker worker2 = new IdWorker(2); System.out.println(worker2.nextId()); } }
评论 29
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

简单简单小白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值