警惕!MyBatis-Plus 主鍵生成策略的隱藏坑,踩過都哭了!
在 MyBatis-Plus 的使用過程中,我們經(jīng)常會享受到其便捷的 CRUD 操作,特別是內(nèi)置的主鍵生成策略,省去了手動管理 ID 的繁瑣。然而,當(dāng)項(xiàng)目進(jìn)入生產(chǎn)環(huán)境,特別是在 集群部署 或 K8S 容器化部署 后,你可能會遇到一個(gè)令人頭疼的問題——主鍵重復(fù)。

這并不是一個(gè)小概率事件,而是許多開發(fā)者在 高并發(fā)分布式環(huán)境 下都會踩中的坑。一旦主鍵重復(fù),數(shù)據(jù)庫插入操作將直接失敗,影響正常業(yè)務(wù)流程,甚至可能導(dǎo)致整個(gè)系統(tǒng)不可用。更糟糕的是,很多開發(fā)者在調(diào)試時(shí)可能并沒有意識到問題的根源,導(dǎo)致線上 Bug 難以復(fù)現(xiàn),排查困難。
本篇文章將深入剖析 MyBatis-Plus 主鍵生成策略的機(jī)制,探討其在 Docker、K8S 及集群環(huán)境下為何會導(dǎo)致主鍵沖突,并提供一個(gè)更加穩(wěn)定、高效的 分布式 ID 生成方案。如果你在 MyBatis-Plus 項(xiàng)目中使用了默認(rèn)的主鍵策略,強(qiáng)烈建議閱讀本文,否則你很可能在未來的某一天,因主鍵重復(fù)問題而陷入崩潰的境地!
以下是一個(gè)典型的錯誤日志:
Mybatis-Plus 啟動時(shí)會通過 com.baomidou.mybatisplus.core.toolkit.Sequence 類的
getMaxWorkerId() 和 getDatacenterId() 方法來初始化 workerId 和 dataCenterId。讓我們來看一下 MyBatis-Plus 生成 workerId 和 dataCenterId 的關(guān)鍵代碼:
- 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 進(jìn)程名稱生成,dataCenterId 由 MAC 地址計(jì)算。然而,在 Docker 環(huán)境下,容器的 JVM 進(jìn)程名稱可能重復(fù),MAC 地址也可能被橋接網(wǎng)絡(luò)共享,這導(dǎo)致 ID 生成可能發(fā)生沖突,進(jìn)而引發(fā)主鍵重復(fù)問題。
替代方案:更可靠的雪花算法
與其糾結(jié)于如何修復(fù) MyBatis-Plus 的 ID 生成邏輯,不如直接采用 更優(yōu)化的雪花算法,該算法不僅能解決 ID 沖突,還能 提升數(shù)據(jù)庫性能。
為什么需要優(yōu)化 ID 生成策略?
在數(shù)據(jù)庫設(shè)計(jì)中,分布式 ID 需要滿足以下特性:
- 全局唯一性:防止 ID 沖突
 - 遞增趨勢:減少 MySQL 數(shù)據(jù)頁分裂,提高性能
 - 高效生成:保證高并發(fā)環(huán)境下 ID 生成的速度
 
常見的分布式 ID 方案:
- 百度 UidGenerator
 - 滴滴 TinyID
 - 美團(tuán) Leaf
 - Twitter 雪花算法(SnowFlake)
 
雖然這些方案都能滿足分布式 ID 需求,但大部分需要依賴 數(shù)據(jù)庫或 Redis,對于中小型項(xiàng)目而言,額外的組件依賴可能帶來運(yùn)維成本。
因此,我們更推薦 Seata 改進(jìn)版雪花算法,它不僅優(yōu)化了 標(biāo)準(zhǔn)版雪花算法的“時(shí)鐘回?fù)堋眴栴},而且實(shí)現(xiàn)更加簡潔。
標(biāo)準(zhǔn)版雪花算法的缺陷
傳統(tǒng)雪花算法的 ID 格式如下:
| 時(shí)間戳(41位) | 機(jī)器 ID(10位) | 序列號(12位) |時(shí)間戳依賴系統(tǒng)時(shí)間,如果服務(wù)器時(shí)鐘回?fù)埽赡軙?dǎo)致 ID 生成沖突。
同一毫秒內(nèi)的序列號最多 4096(2^12)個(gè),超出后需要等待下一個(gè)毫秒。
Seata 優(yōu)化方案
Seata 對 雪花算法的 ID 結(jié)構(gòu)進(jìn)行了改造,使其不再依賴系統(tǒng)時(shí)間,而是使用 內(nèi)存中的時(shí)間戳遞增,避免了時(shí)鐘回?fù)軉栴}:
核心代碼
/**
 * timestamp 和 sequence 合并存儲在一個(gè) Long 類型中
 * 最高 11 位:未使用
 * 中間 41 位:時(shí)間戳
 * 最低 12 位:序列號
 */
private AtomicLong timestampAndSequence;
/**
 * 序列號占用的位數(shù)
 */
private final int sequenceBits = 12;
/**
 * 初始化時(shí)間戳和序列號
 */
private void initTimestampAndSequence() {
    long timestamp = getNewestTimestamp();
    long timestampWithSequence = timestamp << sequenceBits;
    this.timestampAndSequence = new AtomicLong(timestampWithSequence);
}代碼解析:
- 時(shí)間戳和序列號合并存儲,通過 AtomicLong 保證線程安全。
 - 時(shí)間戳不會直接綁定操作系統(tǒng),而是采用 內(nèi)部遞增機(jī)制 避免時(shí)鐘回?fù)軉栴}。
 
Worker ID 生成方式
Seata 還改進(jìn)了 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,優(yōu)先使用 MAC 地址,否則隨機(jī)生成
 */
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 地址");
}優(yōu)化點(diǎn):
- Worker ID 優(yōu)先使用 MAC 地址,無法獲取時(shí)則隨機(jī)生成,避免 Docker 容器 MAC 共享問題。
 - 時(shí)間戳與系統(tǒng)時(shí)間解耦,不再受 時(shí)鐘回?fù)苡绊憽?/li>
 - 序列號遞增機(jī)制優(yōu)化,保證 高并發(fā)環(huán)境下的唯一性。
 
總結(jié)
如果你在 MyBatis-Plus 的 集群環(huán)境 中遇到了 主鍵重復(fù) 的問題,不要只是修修補(bǔ)補(bǔ),而是 直接換用 Seata 的雪花算法,徹底解決 ID 生成沖突,避免線上事故!
Seata 方案的優(yōu)勢:
- 無外部依賴,適合中小型項(xiàng)目
 - 避免時(shí)鐘回?fù)軉栴},提高 ID 生成穩(wěn)定性
 - 高性能,支持高并發(fā)
 
如果你的項(xiàng)目仍然采用 MyBatis-Plus 默認(rèn)的 ID 生成策略,建議盡快引入 Seata 雪花算法,讓你的系統(tǒng)更健壯!















 
 
 














 
 
 
 