警惕!MyBatis-Plus 主键生成策略的隐藏坑,踩过都哭了!

开发
如果你在 MyBatis-Plus 的 集群环境 中遇到了 主键重复 的问题,不要只是修修补补,而是 直接换用 Seata 的雪花算法,彻底解决 ID 生成冲突,避免线上事故!

在 MyBatis-Plus 的使用过程中,我们经常会享受到其便捷的 CRUD 操作,特别是内置的主键生成策略,省去了手动管理 ID 的繁琐。然而,当项目进入生产环境,特别是在 集群部署 或 K8S 容器化部署 后,你可能会遇到一个令人头疼的问题——主键重复。

这并不是一个小概率事件,而是许多开发者在 高并发分布式环境 下都会踩中的坑。一旦主键重复,数据库插入操作将直接失败,影响正常业务流程,甚至可能导致整个系统不可用。更糟糕的是,很多开发者在调试时可能并没有意识到问题的根源,导致线上 Bug 难以复现,排查困难。

本篇文章将深入剖析 MyBatis-Plus 主键生成策略的机制,探讨其在 Docker、K8S 及集群环境下为何会导致主键冲突,并提供一个更加稳定、高效的 分布式 ID 生成方案。如果你在 MyBatis-Plus 项目中使用了默认的主键策略,强烈建议阅读本文,否则你很可能在未来的某一天,因主键重复问题而陷入崩溃的境地!

以下是一个典型的错误日志:

Mybatis-Plus 启动时会通过 com.baomidou.mybatisplus.core.toolkit.Sequence 类的
getMaxWorkerId() 和 getDatacenterId() 方法来初始化 workerId 和 dataCenterId。

让我们来看一下 MyBatis-Plus 生成 workerId 和 dataCenterId 的关键代码:

  • Worker ID 生成逻辑
protected long getMaxWorkerId(long datacenterId, long maxWorkerId) {
    StringBuilder mpid = new StringBuilder();
    mpid.append(datacenterId);
    String name = ManagementFactory.getRuntimeMXBean().getName();
    if (StringUtils.isNotBlank(name)) {
        mpid.append(name.split("@")[0]);
    }
    return (long)(mpid.toString().hashCode() & '\uffff') % (maxWorkerId + 1L);
}
  • Data Center ID 生成逻辑
protected long getDatacenterId(long maxDatacenterId) {
    byte[] mac = network.getHardwareAddress();
    if (null != mac) {
        id = (255L & (long)mac[mac.length - 2] | 65280L & (long)mac[mac.length - 1] << 8) >> 6;
        id %= maxDatacenterId + 1L;
    }
    return id;
}

从代码可以看出,workerId 由 JVM 进程名称生成,dataCenterId 由 MAC 地址计算。然而,在 Docker 环境下,容器的 JVM 进程名称可能重复,MAC 地址也可能被桥接网络共享,这导致 ID 生成可能发生冲突,进而引发主键重复问题。

替代方案:更可靠的雪花算法

与其纠结于如何修复 MyBatis-Plus 的 ID 生成逻辑,不如直接采用 更优化的雪花算法,该算法不仅能解决 ID 冲突,还能 提升数据库性能。

为什么需要优化 ID 生成策略?

在数据库设计中,分布式 ID 需要满足以下特性:

  • 全局唯一性:防止 ID 冲突
  • 递增趋势:减少 MySQL 数据页分裂,提高性能
  • 高效生成:保证高并发环境下 ID 生成的速度

常见的分布式 ID 方案:

  • 百度 UidGenerator
  • 滴滴 TinyID
  • 美团 Leaf
  • Twitter 雪花算法(SnowFlake)

虽然这些方案都能满足分布式 ID 需求,但大部分需要依赖 数据库或 Redis,对于中小型项目而言,额外的组件依赖可能带来运维成本。

因此,我们更推荐 Seata 改进版雪花算法,它不仅优化了 标准版雪花算法的“时钟回拨”问题,而且实现更加简洁。

标准版雪花算法的缺陷

传统雪花算法的 ID 格式如下:

| 时间戳(41位) | 机器 ID(10位) | 序列号(12位) |

时间戳依赖系统时间,如果服务器时钟回拨,可能会导致 ID 生成冲突。

同一毫秒内的序列号最多 4096(2^12)个,超出后需要等待下一个毫秒。

Seata 优化方案

Seata 对 雪花算法的 ID 结构进行了改造,使其不再依赖系统时间,而是使用 内存中的时间戳递增,避免了时钟回拨问题:

核心代码

/**
 * timestamp 和 sequence 合并存储在一个 Long 类型中
 * 最高 11 位:未使用
 * 中间 41 位:时间戳
 * 最低 12 位:序列号
 */
private AtomicLong timestampAndSequence;

/**
 * 序列号占用的位数
 */
private final int sequenceBits = 12;

/**
 * 初始化时间戳和序列号
 */
private void initTimestampAndSequence() {
    long timestamp = getNewestTimestamp();
    long timestampWithSequence = timestamp << sequenceBits;
    this.timestampAndSequence = new AtomicLong(timestampWithSequence);
}

代码解析:

  • 时间戳和序列号合并存储,通过 AtomicLong 保证线程安全。
  • 时间戳不会直接绑定操作系统,而是采用 内部递增机制 避免时钟回拨问题。

Worker ID 生成方式

Seata 还改进了 Worker ID 生成逻辑,避免 MyBatis-Plus 依赖 MAC 地址的问题:


/**
 * 初始化 WorkerId
 * @param workerId 如果为空,则自动生成
 */
private void initWorkerId(Long workerId) {
    if (workerId == null) {
        workerId = generateWorkerId();
    }
    if (workerId > maxWorkerId || workerId < 0) {
        throw new IllegalArgumentException("WorkerId 超出范围:" + maxWorkerId);
    }
    this.workerId = workerId << (timestampBits + sequenceBits);
}

/**
 * 生成 Worker ID,优先使用 MAC 地址,否则随机生成
 */
private long generateWorkerId() {
    try {
        return generateWorkerIdBaseOnMac();
    } catch (Exception e) {
        return generateRandomWorkerId();
    }
}

/**
 * 获取 MAC 地址生成 Worker ID
 */
private long generateWorkerIdBaseOnMac() throws Exception {
    Enumeration<NetworkInterface> all = NetworkInterface.getNetworkInterfaces();
    while (all.hasMoreElements()) {
        NetworkInterface networkInterface = all.nextElement();
        if (networkInterface.isLoopback() || networkInterface.isVirtual()) {
            continue;
        }
        byte[] mac = networkInterface.getHardwareAddress();
        return ((mac[4] & 0B11) << 8) | (mac[5] & 0xFF);
    }
    throw new RuntimeException("没有可用的 MAC 地址");
}

优化点:

  • Worker ID 优先使用 MAC 地址,无法获取时则随机生成,避免 Docker 容器 MAC 共享问题。
  • 时间戳与系统时间解耦,不再受 时钟回拨影响。
  • 序列号递增机制优化,保证 高并发环境下的唯一性。

总结

如果你在 MyBatis-Plus 的 集群环境 中遇到了 主键重复 的问题,不要只是修修补补,而是 直接换用 Seata 的雪花算法,彻底解决 ID 生成冲突,避免线上事故!

Seata 方案的优势:

  • 无外部依赖,适合中小型项目
  • 避免时钟回拨问题,提高 ID 生成稳定性
  • 高性能,支持高并发

如果你的项目仍然采用 MyBatis-Plus 默认的 ID 生成策略,建议尽快引入 Seata 雪花算法,让你的系统更健壮!

责任编辑:赵宁宁 来源: 路条编程
相关推荐

2023-10-31 08:01:48

Mybatis参数jdbcurl​

2022-04-26 21:49:55

Spring事务数据库

2019-10-30 14:44:41

Prometheus开源监控系统

2024-04-01 08:05:27

Go开发Java

2024-12-20 16:49:15

MyBatis开发代码

2017-07-17 15:46:20

Oracle并行机制

2018-01-10 13:40:03

数据库MySQL表设计

2024-05-06 00:00:00

缓存高并发数据

2015-03-24 16:29:55

默认线程池java

2023-06-07 08:00:00

MySQL批量插入

2020-11-03 06:57:10

MyBatis数据库

2019-09-18 15:20:16

MyBatisSQL数据库

2024-12-04 09:36:37

2023-06-07 08:08:37

MybatisSpringBoot

2023-03-13 13:36:00

Go扩容切片

2018-09-11 09:14:52

面试公司缺点

2023-07-29 22:02:06

MyBatis数据库配置

2009-09-25 13:33:43

Hibernate主键

2023-06-14 08:34:18

Mybatis死锁框架

2018-01-10 06:17:24

点赞
收藏

51CTO技术栈公众号