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