戰(zhàn)中的正確姿勢(shì):從概念到性能取舍)
1. 主鍵與外鍵的概念拆解先搞清楚兩個(gè)“鍵”到底管什么事聊主鍵和外鍵的區(qū)別之前我先說個(gè)真實(shí)經(jīng)歷。有一回我接手一套訂單系統(tǒng)前任開發(fā)者把所有表的外鍵約束全刪了理由是“外鍵影響寫入性能”。結(jié)果線上出現(xiàn)了不少孤兒數(shù)據(jù)——訂單表里有一條記錄指向的用戶ID根本不存在報(bào)表一跑就炸。后來我花了兩天補(bǔ)數(shù)據(jù)才意識(shí)到主鍵和外鍵的分工是完全不一樣的它們?cè)跀?shù)據(jù)庫里管的根本不是同一件事。主鍵核心任務(wù)只有一個(gè)在這張表里唯一標(biāo)識(shí)每一行。比如用戶表里的user_id不能為空不能重復(fù)一張表最多只有一個(gè)主鍵復(fù)合主鍵也算一個(gè)整體。你可以把它理解成每個(gè)人的身份證號(hào)碼不管叫張三還是李四只要身份證號(hào)唯一就能準(zhǔn)確找到這條記錄。在Java的實(shí)體類里你看到的Id注解標(biāo)注的字段映射到數(shù)據(jù)庫就是主鍵。外鍵的性質(zhì)完全不同。它不存在“唯一”的概念它的職責(zé)是聲明兩張表之間的關(guān)系。訂單表里的user_id是外鍵它指向用戶表的user_id主鍵含義是“這個(gè)訂單歸屬于哪個(gè)用戶”。外鍵字段允許重復(fù)甚至允許為NULL但它有一個(gè)硬性要求寫入的值必須能在被引用的主鍵里找到否則數(shù)據(jù)庫直接拒絕。這就像房產(chǎn)證上的“坐落地址”地址本身可以重復(fù)出現(xiàn)但必須指向一個(gè)真實(shí)存在的坐標(biāo)不能憑空寫一個(gè)不存在的路名。這里有個(gè)最重要的認(rèn)知差異主鍵約束的是“這一張表自身的數(shù)據(jù)質(zhì)量”外鍵約束的是“兩張表之間的數(shù)據(jù)一致性”。很多Java開發(fā)同學(xué)寫代碼時(shí)習(xí)慣把外鍵只當(dāng)作一個(gè)普通字段處理在Service層自己判斷關(guān)聯(lián)是否存在。這樣做不是不可以但一旦你理解了外鍵是數(shù)據(jù)庫層面的完整性契約就看得出兩種做法的代價(jià)完全不同——前者是每次寫入都要你親自記得校驗(yàn)后者是數(shù)據(jù)庫幫你兜底。說白了主鍵是“我是誰”的問題外鍵是“我和你什么關(guān)系”的問題。2. Java實(shí)戰(zhàn)中主外鍵的落地方式從建表SQL到ORM映射光講概念沒有用得落到代碼里。我按數(shù)據(jù)庫建表、JPA映射、MyBatis處理三條線展開你在實(shí)際項(xiàng)目里大概率都繞不開。2.1 數(shù)據(jù)庫DDL層主外鍵約束是怎么寫出來的先看一張最典型的用戶表和訂單表。用戶表的主鍵是user_id訂單表里有一個(gè)外鍵user_id指向用戶表的主鍵。CREATE TABLE t_user ( user_id BIGINT NOT NULL AUTO_INCREMENT COMMENT 用戶ID, username VARCHAR(64) NOT NULL COMMENT 用戶名, status TINYINT NOT NULL DEFAULT 1 COMMENT 狀態(tài), PRIMARY KEY (user_id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用戶表; CREATE TABLE t_order ( order_id BIGINT NOT NULL AUTO_INCREMENT COMMENT 訂單ID, user_id BIGINT NOT NULL COMMENT 下單用戶ID, order_amount DECIMAL(12,2) NOT NULL COMMENT 訂單金額, order_time DATETIME NOT NULL COMMENT 下單時(shí)間, PRIMARY KEY (order_id), KEY idx_user_id (user_id), CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES t_user(user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT訂單表;注意到幾個(gè)細(xì)節(jié)主鍵約束由PRIMARY KEY定義同時(shí)唯一且非空這是硬性規(guī)則。外鍵約束的寫法是CONSTRAINT fk_order_user FOREIGN KEYmysql扣住不放。我在訂單表的user_id上額外建了一個(gè)KEY idx_user_id這容易被新手忽略。InnoDB存儲(chǔ)引擎有個(gè)習(xí)慣如果外鍵列沒有索引它會(huì)自動(dòng)幫你建一個(gè)索引。但自動(dòng)建的性能和可維護(hù)性都不如自己顯式控制來得靠譜所以我會(huì)主動(dòng)建。這里插一句很多人會(huì)問的為什么外鍵列也要索引因?yàn)橥怄I約束要檢查“你插入的值是否在被引用表的主鍵里”本質(zhì)上是去被引用表的主鍵索引里查反過來如果將來要?jiǎng)h除用戶數(shù)據(jù)庫也得通過外鍵索引快速找到所有關(guān)聯(lián)訂單否則就要全表掃描。沒有索引的外鍵寫操作可能慢一個(gè)數(shù)量級(jí)。2.2 JPA/Hibernate中的主外鍵映射在Java項(xiàng)目里主外鍵關(guān)系最直接的體現(xiàn)就是注解映射。下面這段代碼是JPA風(fēng)格的經(jīng)典寫法Entity Table(name t_user) public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long userId; private String username; private Integer status; // 一對(duì)多方向用戶下關(guān)聯(lián)的訂單集合 OneToMany(mappedBy user) private ListOrder orders; } Entity Table(name t_order) public class Order { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long orderId; ManyToOne(fetch FetchType.LAZY) JoinColumn(name user_id, referencedColumnName userId) private User user; private BigDecimal orderAmount; private LocalDateTime orderTime; }關(guān)于主鍵就是Id配GeneratedValueIDENTITY表示數(shù)據(jù)庫自增。關(guān)于外鍵關(guān)鍵在于ManyToOne和JoinColumn。JoinColumn里的name寫的是數(shù)據(jù)庫里的字段名user_idreferencedColumnName寫的是被引用實(shí)體的屬性名userId。如果數(shù)據(jù)庫中訂單表的user_id直接叫userIdreferencedColumnName其實(shí)可以省略但我建議每次都寫出來防止以后字段改名時(shí)出現(xiàn)莫名其妙的映射錯(cuò)誤。一個(gè)真實(shí)的坑把fetch設(shè)成LAZY之后在事務(wù)外訪問訂單關(guān)聯(lián)的user對(duì)象會(huì)拋LazyInitializationException。這不是主外鍵設(shè)計(jì)的問題而是你對(duì)Hibernate懶加載機(jī)制不熟。解決方案要么在事務(wù)內(nèi)完成關(guān)聯(lián)數(shù)據(jù)的訪問要么用JOIN FETCH或EntityGraph顯式抓取。2.3 MyBatis中的主外鍵處理思路MyBatis沒有JPA那么重的實(shí)體關(guān)系管理主外鍵的處理基本靠自己寫SQL。主鍵這一塊MyBatis提供了useGeneratedKeys來把數(shù)據(jù)庫生成的主鍵回填到Java對(duì)象里。insert idinsertUser useGeneratedKeystrue keyPropertyuserId INSERT INTO t_user (username, status) VALUES (#{username}, #{status}) /insertkeyProperty對(duì)應(yīng)的是User實(shí)體里的userId屬性。如果不寫useGeneratedKeys插入后拿不到數(shù)據(jù)庫自增主鍵只能靠重新查詢這在業(yè)務(wù)上很麻煩。外鍵的處理就更有意思了。MyBatis中你可以有兩個(gè)選擇一是把外鍵當(dāng)作普通字段直接寫進(jìn)結(jié)果映射二是在關(guān)聯(lián)查詢中用association標(biāo)簽組裝對(duì)象。resultMap idOrderResultMap typecom.example.Order id propertyorderId columnorder_id/ result propertyorderAmount columnorder_amount/ result propertyorderTime columnorder_time/ association propertyuser javaTypecom.example.User id propertyuserId columnuser_id/ result propertyusername columnusername/ result propertystatus columnstatus/ /association /resultMap select idselectOrderWithUser resultMapOrderResultMap SELECT o.order_id, o.order_amount, o.order_time, u.user_id, u.username, u.status FROM t_order o LEFT JOIN t_user u ON o.user_id u.user_id WHERE o.order_id #{orderId} /select我特別想說一下LEFT JOIN和INNER JOIN的選擇。查訂單時(shí)如果你只需要“存在且合法的訂單”用INNER JOIN如果你要保留那些外鍵為NULL的孤兒訂單用于排查問題用LEFT JOIN。這個(gè)選擇背后就是對(duì)主外鍵本質(zhì)的理解——外鍵允許NULL意味著“這條訂單不屬于任何用戶”在數(shù)據(jù)語義上可能是被允許的。用錯(cuò)JOIN類型查詢結(jié)果會(huì)少一行或語義錯(cuò)亂這種bug在報(bào)表場(chǎng)景里排查起來特別頭痛。2.4 事務(wù)邊界與鎖順序主外鍵寫入順序的講究不管是JPA還是MyBatis只要涉及主外鍵相關(guān)的寫入操作事務(wù)的邊界、鎖的順序就要格外小心。我見過一個(gè)高頻問題先插入訂單再插入用戶或者更新用戶結(jié)果外鍵約束報(bào)錯(cuò)“外鍵檢查失敗”。原理很簡(jiǎn)單外鍵約束要求被引用的主鍵已經(jīng)存在訂單的外鍵user_id必須先能在用戶表里找到對(duì)應(yīng)記錄。所以寫入順序必須是先插入父表用戶再插入子表訂單。但鎖順序的問題比這個(gè)更深一層。InnoDB在執(zhí)行外鍵檢查時(shí)會(huì)先在子表外鍵索引上加共享鎖再去父表被引用行的主鍵索引上加共享鎖。如果你在同一事務(wù)里先更新父表主鍵、再插入子表外鍵記錄兩個(gè)操作的加鎖順序不一致高并發(fā)下就可能互相等待形成死鎖。我實(shí)際遇到過這種場(chǎng)景兩套業(yè)務(wù)邏輯一套先改用戶再下訂單另一套先下訂單再改用戶并發(fā)一上來死鎖日志能從早刷到晚。解決思路是統(tǒng)一加鎖順序讓所有涉及同一組主外鍵的操作都按“先父表后子表”的順序訪問。這跟Java并發(fā)編程里“按固定順序拿鎖”的道理一模一樣。3. 設(shè)計(jì)取舍什么時(shí)候堅(jiān)持外鍵約束什么時(shí)候改用邏輯外鍵你可能會(huì)覺得既然外鍵這么有用是不是所有關(guān)聯(lián)字段都要建外鍵我的回答是不一定甚至要看業(yè)務(wù)場(chǎng)景。這一節(jié)聊清楚數(shù)據(jù)庫外鍵和邏輯外鍵的區(qū)別以及主鍵生成策略的選擇。3.1 強(qiáng)一致與高并發(fā)的取舍數(shù)據(jù)庫外鍵的代價(jià)數(shù)據(jù)庫外鍵最大的價(jià)值是強(qiáng)一致性——任何繞過應(yīng)用層的SQL寫入都會(huì)被外鍵擋住。但代價(jià)也很明顯插入子表時(shí)數(shù)據(jù)庫必須去父表主鍵索引里做一次額外查詢少了Mapper層的判斷邏輯多了數(shù)據(jù)庫內(nèi)的隱式查詢。刪除父表記錄時(shí)如果有外鍵約束數(shù)據(jù)庫要掃描子表外鍵索引確認(rèn)沒有關(guān)聯(lián)記錄這就鎖住了父表行和子表索引。分庫分表場(chǎng)景下跨數(shù)據(jù)庫節(jié)點(diǎn)沒法建外鍵因?yàn)橥怄I只能約束同一個(gè)數(shù)據(jù)庫實(shí)例里的表關(guān)系?;ヂ?lián)網(wǎng)場(chǎng)景里尤其是用戶量過千萬的訂單系統(tǒng)、交易系統(tǒng)幾乎沒人敢在核心鏈路上建大量外鍵。原因不是外鍵本身慢而是它把“數(shù)據(jù)一致性校驗(yàn)”放在了一個(gè)數(shù)據(jù)庫實(shí)例上一旦拆分或擴(kuò)容約束就失效了。這時(shí)候團(tuán)隊(duì)通常選擇“邏輯外鍵”也就是在Java代碼層面維護(hù)這層關(guān)系。邏輯外鍵的核心設(shè)計(jì)是數(shù)據(jù)表里仍然有user_id這個(gè)字段但不建FOREIGN KEY約束只建普通索引。寫入前由Service層主動(dòng)去用戶表查一次確認(rèn)用戶存在后才允許插入訂單。如果用戶刪除了先處理訂單表退單、轉(zhuǎn)移、標(biāo)記無效再刪用戶整個(gè)過程包在同一個(gè)事務(wù)里由業(yè)務(wù)代碼保證一致性。3.2 邏輯外鍵在Java里的落地方式既然要舍棄數(shù)據(jù)庫外鍵就得在Java里找替代方案。最基礎(chǔ)的是在Service里做校驗(yàn)Service RequiredArgsConstructor public class OrderService { private final OrderRepository orderRepository; private final UserRepository userRepository; Transactional public void createOrder(OrderCreateRequest request) { Long userId request.getUserId(); User user userRepository.findById(userId) .orElseThrow(() - new BusinessException(用戶不存在不能下單)); Order order new Order(); order.setUser(user); order.setOrderAmount(request.getOrderAmount()); orderRepository.save(order); } }這樣寫有個(gè)好處報(bào)錯(cuò)信息是你自定義的BusinessException而不是數(shù)據(jù)庫拋出來的一長(zhǎng)串英文外鍵約束異常對(duì)接口返回更友好。但代價(jià)是每個(gè)寫入口都要記得校驗(yàn)。所以更穩(wěn)的做法是在刪除用戶的入口加一道檢查邏輯模擬外鍵的“防止父記錄被刪”能力Transactional public void deleteUser(Long userId) { long orderCount orderRepository.countByUserId(userId); if (orderCount 0) { throw new BusinessException(該用戶存在關(guān)聯(lián)訂單不能直接刪除); } userRepository.deleteById(userId); }這里我把它稱為“對(duì)刪除動(dòng)作的守衛(wèi)”。它相當(dāng)于把數(shù)據(jù)庫外鍵的RESTRICT行為用代碼實(shí)現(xiàn)了一遍。3.3 主鍵生成策略的選擇自增、UUID、還是雪花ID聊完外鍵必須回來看主鍵本身。因?yàn)橥怄I引用的對(duì)象畢竟是主鍵主鍵選得不好外鍵也好不到哪去。我把三種主流方案的實(shí)際表現(xiàn)列個(gè)表方案存儲(chǔ)空間寫入性能分布式適用性可讀性自增BIGINT8字節(jié)高B樹順序?qū)懭氩疃鄬?shí)例會(huì)重復(fù)高UUID字符串36字節(jié)左右較低隨機(jī)寫入頁分裂好差雪花算法BIGINT8字節(jié)較高趨勢(shì)遞增好中自增主鍵之所以寫入快是因?yàn)樾聰?shù)據(jù)永遠(yuǎn)追加到B樹的最右側(cè)無需頻繁移動(dòng)頁。UUID如果用無序版本每次插入都會(huì)在索引中間位置隨機(jī)落點(diǎn)頁分裂頻率大幅上升寫入性能會(huì)明顯下降。如果你必須用UUID強(qiáng)烈建議改用UUIDv7這種帶時(shí)間順序的版本能大幅緩解頁分裂問題。雪花算法的核心思路是64位long型ID高41位時(shí)間戳、中間10位機(jī)器ID、低12位序列號(hào)。Java里可以自己實(shí)現(xiàn)一個(gè)簡(jiǎn)單版本public class SnowflakeIdGenerator { private final long workerId; private final long datacenterId; private long sequence 0L; private long lastTimestamp -1L; private static final long WORKER_ID_BITS 5L; private static final long DATACENTER_ID_BITS 5L; private static final long SEQUENCE_BITS 12L; public synchronized long nextId() { long timestamp System.currentTimeMillis(); if (timestamp lastTimestamp) { throw new IllegalStateException(時(shí)鐘回?fù)芫芙^生成ID); } if (timestamp lastTimestamp) { sequence (sequence 1) 4095L; if (sequence 0L) { timestamp tilNextMillis(lastTimestamp); } } else { sequence 0L; } lastTimestamp timestamp; return ((timestamp - 1288834974657L) 22) | (datacenterId 17) | (workerId 12) | sequence; } }這段代碼的核心在于把時(shí)間戳、機(jī)器號(hào)、序列號(hào)壓進(jìn)一個(gè)long里既保證了全局趨勢(shì)遞增又讓不同節(jié)點(diǎn)生成的ID不會(huì)碰撞。Java后端做微服務(wù)拆分后如果你還在用數(shù)據(jù)庫自增當(dāng)主鍵一旦兩個(gè)服務(wù)都往同一張邏輯表寫數(shù)據(jù)主鍵沖突只是時(shí)間問題。這不是外鍵的問題但會(huì)影響外鍵引用的穩(wěn)定性。4. 典型坑位盤點(diǎn)主外鍵實(shí)踐里的高頻事故最后一部分聊實(shí)踐中容易踩的坑也算是我這幾年經(jīng)驗(yàn)的匯總。每條都是真實(shí)場(chǎng)景下會(huì)遇到的你對(duì)照一下自己項(xiàng)目說不定已經(jīng)踩過。4.1 級(jí)聯(lián)操作失控ON DELETE CASCADE的誤傷建外鍵的時(shí)候很多人圖省事直接寫ON DELETE CASCADE想著“刪父表時(shí)子表一起刪多方便”。但業(yè)務(wù)上往往不是這樣。我經(jīng)歷過一次線上事故管理員在后臺(tái)刪了一個(gè)測(cè)試用戶結(jié)果該用戶名下幾千條歷史訂單全部被物理刪除財(cái)務(wù)對(duì)賬數(shù)據(jù)直接缺了一大塊。雖然訂單表有備份但恢復(fù)數(shù)據(jù)花了一整個(gè)下午。ON DELETE CASCADE只適合“子表數(shù)據(jù)完全依附于父表”的場(chǎng)景比如刪除用戶時(shí)會(huì)話記錄、臨時(shí)表單項(xiàng)可以一起刪。訂單、合同這類有獨(dú)立業(yè)務(wù)價(jià)值的記錄絕對(duì)不能級(jí)聯(lián)刪除。我的建議是業(yè)務(wù)數(shù)據(jù)一律不用CASCADE改用邏輯刪除軟刪除或者用BEFORE DELETE觸發(fā)器做校驗(yàn)。如果你確實(shí)需要“刪用戶時(shí)同步處理訂單”也不要用級(jí)聯(lián)而是寫一段顯式業(yè)務(wù)代碼一條條處理訂單后再刪用戶過程清晰可審計(jì)。4.2 外鍵檢查導(dǎo)致的大事務(wù)鎖競(jìng)爭(zhēng)有些團(tuán)隊(duì)使用了數(shù)據(jù)庫外鍵但在高并發(fā)下事務(wù)很難扛住。原因是外鍵檢查要額外持有子表索引和父表主鍵索引的鎖這類鎖不像行鎖那樣粒度小有時(shí)會(huì)放大成間隙鎖或next-key鎖直接影響并發(fā)度。我遇到過一個(gè)具體現(xiàn)象同時(shí)開了多個(gè)線程批量導(dǎo)用戶和訂單偶發(fā)死鎖。排查后發(fā)現(xiàn)兩個(gè)線程分別按不同的順序操作user和order表而InnoDB做外鍵檢查時(shí)的鎖順序是固定的業(yè)務(wù)上的亂序?qū)е铝搜h(huán)等待。解決路徑有兩條一是約束所有代碼按“先父表后子表”訪問二是去掉數(shù)據(jù)庫外鍵改用邏輯外鍵和代碼校驗(yàn)把鎖的粒度從數(shù)據(jù)庫層面釋放出來用事務(wù)隔離級(jí)別和業(yè)務(wù)鎖來控制。4.3 主鍵和外鍵的數(shù)據(jù)類型不一致最隱蔽的坑這個(gè)問題相當(dāng)隱蔽。用戶表的主鍵是BIGINT訂單表的外鍵字段不小心設(shè)計(jì)成了INT數(shù)據(jù)量小的時(shí)候看不出問題一旦user_id超過INT上限約21億插入訂單時(shí)直接報(bào)錯(cuò)數(shù)據(jù)全卡在入口。還有一種情況更坑JPA里你定義的userId是Long但XML映射里resultMap的column類型寫成了IntegerMyBatis會(huì)用Integer去接Long的值同樣會(huì)出現(xiàn)異常值或轉(zhuǎn)換錯(cuò)誤。我建議你在建表時(shí)統(tǒng)一主外鍵字段類型為BIGINTJava實(shí)體主鍵統(tǒng)一用Long不要因?yàn)椤皥D省事”混著用。4.4 快速排查清單速查表這里整理了一張實(shí)用性很高的速查表按照癥狀、可能原因、解決方案來對(duì)照適合排查現(xiàn)場(chǎng)直接翻癥狀可能原因檢查方向外鍵檢查失敗插入被拒絕父表主鍵不存在或外鍵字段寫了不存在的ID先查父表記錄是否在再看外鍵字段是否有臟數(shù)據(jù)刪除父表記錄超時(shí)或等待鎖子表外鍵索引缺失刪父表需要全表掃子表查看子表執(zhí)行計(jì)劃確認(rèn)外鍵列有索引并發(fā)寫入偶發(fā)死鎖主外鍵相關(guān)表的加鎖順序不一致統(tǒng)一先父表后子表的訪問順序分庫分表后關(guān)聯(lián)查詢變慢關(guān)聯(lián)查詢跨庫外鍵無法跨庫約束考慮冗余字段、寬表設(shè)計(jì)或應(yīng)用層聚合插入大字段后主鍵ID跳躍自增主鍵的AUTO_INCREMENT回滾機(jī)制事務(wù)回滾不會(huì)回收自增ID屬正?,F(xiàn)象不要依賴連續(xù)主鍵ID跳躍這個(gè)問題很多初級(jí)開發(fā)會(huì)當(dāng)成bug上報(bào)其實(shí)InnoDB的自增主鍵在事務(wù)回滾之后不會(huì)回退這是設(shè)計(jì)如此。如果你業(yè)務(wù)上需要連續(xù)編號(hào)自增主鍵本身就不適合你要用專門的編號(hào)表來生成。還有一個(gè)隱藏坑很多人刪除父表數(shù)據(jù)時(shí)用DELETE但外鍵是限制關(guān)系刪除順序錯(cuò)了會(huì)報(bào)錯(cuò)。正確順序是先刪子表關(guān)聯(lián)記錄再刪父表記錄或者把父表記錄標(biāo)記為無效。這跟前文說的事務(wù)鎖順序一脈相承。5. 我的一點(diǎn)實(shí)操體會(huì)回到開頭那套訂單系統(tǒng)。后來我在復(fù)盤時(shí)發(fā)現(xiàn)最初的問題不是“外鍵該不該刪”而是團(tuán)隊(duì)對(duì)外鍵的定位不統(tǒng)一——有人把它當(dāng)性能殺手有人把它當(dāng)擺設(shè)。當(dāng)你理解了主鍵是身份標(biāo)識(shí)、外鍵是關(guān)系約束之后設(shè)計(jì)思路會(huì)清晰很多第一主鍵選擇要盡早定。自增、雪花、UUID各有適用場(chǎng)景一旦數(shù)據(jù)量起來再換主鍵策略代價(jià)是重建所有索引和關(guān)聯(lián)關(guān)系。第二外鍵不是不能用而是要區(qū)分場(chǎng)景。核心交易鏈路建議用邏輯外鍵加顯式校驗(yàn)低頻后臺(tái)管理、內(nèi)部系統(tǒng)可以放心用數(shù)據(jù)庫外鍵換省心。第三不管哪種方案關(guān)聯(lián)字段的索引一定不要省這是最容易忽略的性能隱患。如果讓我給一個(gè)最直接的建議團(tuán)隊(duì)水平高、業(yè)務(wù)一致性強(qiáng)要求高保留數(shù)據(jù)庫外鍵業(yè)務(wù)需要高并發(fā)、頻繁分庫分表就用代碼校驗(yàn)替代。選之前先問自己一句這條關(guān)系斷裂之后你拿什么邏輯來兜底能答出來選哪個(gè)都沒問題。最后再分享一個(gè)小經(jīng)驗(yàn)每次建表我都會(huì)把字段注釋和關(guān)系說明寫在DDL里。主鍵字段注明“主鍵唯一標(biāo)識(shí)用戶”外鍵字段注明“外鍵引用t_user.user_id”。這個(gè)習(xí)慣在三個(gè)月后查表結(jié)構(gòu)時(shí)非常省事你也一定會(huì)感謝當(dāng)時(shí)認(rèn)真寫注釋的自己。