據(jù)庫(kù)連接的常見(jiàn)誤區(qū))
SpringBoot項(xiàng)目里數(shù)據(jù)庫(kù)連接出問(wèn)題報(bào)錯(cuò)信息往往千奇百怪但根子往往不在代碼邏輯而在一些被默認(rèn)值掩蓋的認(rèn)知死角。很多團(tuán)隊(duì)從SSH工程切換到SpringBoot配置變短了儀式感變輕了卻把數(shù)據(jù)庫(kù)驅(qū)動(dòng)的脾氣想象得過(guò)于溫順。當(dāng)你對(duì)著Communications link failure撓頭時(shí)真正需要面對(duì)的不是網(wǎng)絡(luò)抖動(dòng)而是你對(duì)連接生命周期的一無(wú)所知。沒(méi)有難連的數(shù)據(jù)庫(kù)只有不假思索的集成姿勢(shì)。配置文件的“瘦身”陷阱變量少了坑反而深了SpringBoot把數(shù)據(jù)庫(kù)配置壓縮成幾行以spring.datasource開(kāi)頭的鍵值對(duì)這本是便利??杀憷呱藨卸钁卸柙杏苏`解。最典型的坑是driver-class-name。你以為寫(xiě)了com.mysql.cj.jdbc.Driver就萬(wàn)事大吉但MySQL的驅(qū)動(dòng)類在不同版本下還會(huì)迭代一旦你依賴的mysql-connector-java是5.1.x而配置里寫(xiě)的是cj驅(qū)動(dòng)應(yīng)用啟動(dòng)時(shí)直接拋出ClassNotFound。配置精簡(jiǎn)不代表可以省略版本感知驅(qū)動(dòng)類名是連接協(xié)議的鑰匙差一個(gè)字符就是一個(gè)時(shí)代的鴻溝。另一個(gè)高頻誤區(qū)是連接URL的拼接。有人把serverTimezoneAsia/Shanghai丟棄然后在凌晨發(fā)現(xiàn)時(shí)間錯(cuò)亂八小時(shí)。更隱蔽的是useSSLfalse與allowPublicKeyRetrievaltrue的組合在MySQL 8.0以上版本中不配置這兩項(xiàng)連接會(huì)反復(fù)握手失敗且報(bào)錯(cuò)信息極其誤導(dǎo)讓你以為密碼錯(cuò)誤。絕大多數(shù)連接失敗的真相都藏在URL參數(shù)里而非數(shù)據(jù)庫(kù)端的權(quán)限表內(nèi)。配置文件不是越短越好而是需要你精確知道每一個(gè)被省略的默認(rèn)值到底代表著什么。連接池默認(rèn)值不是免死金牌更像是埋好的引線很多開(kāi)發(fā)者用SpringBoot默認(rèn)的HikariCP覺(jué)得性能已經(jīng)夠好于是不管不問(wèn)。但連接池的核心參數(shù)——最大連接數(shù)、最小空閑數(shù)、連接超時(shí)時(shí)間——全部被默認(rèn)值罩著。生產(chǎn)環(huán)境一旦出現(xiàn)慢SQL線程池排隊(duì)瞬間爆炸你以為加機(jī)器能解決其實(shí)只是讓更多連接去搶同一把鎖。連接池不是越大越好默認(rèn)最大10個(gè)連接在并發(fā)80的請(qǐng)求下等待隊(duì)列會(huì)以毫秒為單位膨脹直到你看見(jiàn)Connection is not available, request timed out。還有一類誤區(qū)是亂調(diào)maximum-pool-size。有人拍腦袋設(shè)成200數(shù)據(jù)庫(kù)的max_connections才150結(jié)果應(yīng)用還沒(méi)啟動(dòng)完數(shù)據(jù)庫(kù)就被自己打死。連接池的容量需要結(jié)合數(shù)據(jù)庫(kù)的會(huì)話上限、機(jī)器內(nèi)存、SQL的平均執(zhí)行耗時(shí)綜合推算。沒(méi)有最優(yōu)連接數(shù)只有最合適的上下文。另外connection-timeout設(shè)置得過(guò)短比如1000毫秒一次正常的schema校驗(yàn)都可能觸發(fā)超時(shí)設(shè)置得過(guò)長(zhǎng)比如60秒會(huì)讓前端用戶血怒。建議至少觀察一個(gè)業(yè)務(wù)高峰周期再定而不是照抄網(wǎng)上的“萬(wàn)能配置”。事務(wù)緩存與自調(diào)用你以為的Transactional根本沒(méi)生效SpringBoot通過(guò)注解管理事務(wù)比XML配置優(yōu)雅得多。但注解有個(gè)致命盲區(qū)——自調(diào)用。當(dāng)你一個(gè)類里的方法A調(diào)用本類方法B且B上標(biāo)注了Transactional事務(wù)管理器根本看不到B的代理對(duì)象它只會(huì)看到原始對(duì)象于是B里的SQL全部脫離事務(wù)。自調(diào)用是事務(wù)失效的第一個(gè)隱形殺手比漏寫(xiě)注解更防不勝防。破局之法是注入自己或者把事務(wù)方法拆到另一個(gè)Service里讓代理鏈完整。第二個(gè)殺手是異常被吞。Transactional默認(rèn)只在運(yùn)行時(shí)異常RuntimeException和Error時(shí)回滾如果方法捕獲了異常并打印日志后正常返回事務(wù)的邊界就是“成功”。很多慘案是數(shù)據(jù)庫(kù)寫(xiě)入成功后續(xù)業(yè)務(wù)拋了異常但異常在方法內(nèi)部被try-catch吃掉數(shù)據(jù)仿佛被施了半套魔法。事務(wù)不只是靠注解它靠的是異常的傳播紀(jì)律。如果你確實(shí)需要在checked exception下回滾必須顯式聲明rollbackFor Exception.class——這是SpringBoot文檔里寫(xiě)了上千遍但代碼里仍然每天都能翻到的錯(cuò)。連接泄漏每一個(gè)被遺忘的ResultSet都在緩慢謀殺你的系統(tǒng)JdbcTemplate的出現(xiàn)讓程序員以為不再需要手動(dòng)關(guān)連接可當(dāng)你混合使用JPA、MyBatis甚至是原生JDBC時(shí)連接泄漏就找到了藏身之處。比如在try塊里打開(kāi)了Connection卻忘了在finally或try-with-resources中關(guān)閉或者使用了DataSourceUtils.getConnection()但關(guān)閉時(shí)卻誤用connection.close()導(dǎo)致連接被真正關(guān)閉而不是歸還給池子。連接池的耗盡是漸進(jìn)式的你的服務(wù)不會(huì)立刻掛掉而是先出現(xiàn)偶發(fā)性的慢請(qǐng)求再發(fā)展為持續(xù)的超時(shí)最后在某個(gè)高并發(fā)瞬間徹底癱瘓。更隱蔽的泄漏來(lái)自懶加載的迭代器。用JPA的Streamable或者M(jìn)yBatis的Cursor如果整個(gè)流式讀取過(guò)程沒(méi)有包裹在事務(wù)里連接會(huì)在游標(biāo)用完后才釋放。你寫(xiě)了一個(gè)導(dǎo)出Excel的功能導(dǎo)到一半報(bào)錯(cuò)連接就永久留在池里。沒(méi)事時(shí)看不出異常但每天跑幾次定時(shí)任務(wù)三個(gè)月后連接池里的線程死了一半。排查連接泄漏別只盯著jstack先打開(kāi)連接池的監(jiān)控面板看看active和idle的曲線每一個(gè)不下降的峰值都是泄漏的腳印。多數(shù)據(jù)源一個(gè)事務(wù)管理器的幻覺(jué)兩個(gè)世界的心碎SpringBoot支持多數(shù)據(jù)源但很多人的實(shí)現(xiàn)方式是在配置里堆兩套spring.datasource然后以為事務(wù)能自動(dòng)分身?,F(xiàn)實(shí)是你在一個(gè)Service方法上標(biāo)注Transactional它默認(rèn)綁定第一個(gè)事務(wù)管理器第二個(gè)數(shù)據(jù)源的寫(xiě)入根本不陪你玩。多數(shù)據(jù)源下的事務(wù)默認(rèn)是各管各家的全局一致性只存在于你的想象里。需要分布式事務(wù)時(shí)有人馬上想到Seata、ShardingSphere但引入這些重量級(jí)組件之前你該先問(wèn)問(wèn)自己這兩個(gè)庫(kù)之間的數(shù)據(jù)一致性真的需要強(qiáng)一致嗎還是最終一致就夠用另外多數(shù)據(jù)源的另一個(gè)大坑是Mapper掃描路徑。MapperScan如果不分開(kāi)指定包兩個(gè)數(shù)據(jù)源的Mapper可能互相串門導(dǎo)致你用A數(shù)據(jù)源的事務(wù)管理器去操作B的Mapper運(yùn)行時(shí)報(bào)Invalid bound statement。數(shù)據(jù)源之間的隔離不只是寫(xiě)在配置里的還要落實(shí)到類的邊界上。一種相對(duì)穩(wěn)妥的做法是按業(yè)務(wù)模塊拆分包每個(gè)模塊獨(dú)立的DataSource、SqlSessionFactory、TransactionManager并且絕不在一個(gè)事務(wù)里同時(shí)寫(xiě)兩個(gè)庫(kù)。如果確實(shí)需要跨庫(kù)寫(xiě)放棄事務(wù)改用消息補(bǔ)償或本地消息表——這比任何分布式事務(wù)方案都更容易在一個(gè)復(fù)雜系統(tǒng)里活下來(lái)。ORM映射不是數(shù)據(jù)庫(kù)的鍋是你對(duì)對(duì)象的幻覺(jué)SpringBoot集成JPA或MyBatis實(shí)體類里的字段名總覺(jué)得跟數(shù)據(jù)庫(kù)列名天然對(duì)應(yīng)。實(shí)際上實(shí)體類和表之間隔著一道命名映射的鴻溝。JPA默認(rèn)的命名策略是蛇形轉(zhuǎn)駝峰但如果你表里的列名是USERNAME全大寫(xiě)或者用了特殊前綴t_uesr_name這種打字錯(cuò)誤——沒(méi)錯(cuò)表設(shè)計(jì)時(shí)拼錯(cuò)一個(gè)字母映射時(shí)你會(huì)在幾百個(gè)SQL里反復(fù)確認(rèn)“為什么查不到密碼”。數(shù)據(jù)庫(kù)的列名不是你Java字段的鏡像它是一個(gè)獨(dú)立的協(xié)議。與其抱怨ORM太傻不如一開(kāi)始就用Column(name...)顯式聲明別讓運(yùn)行時(shí)去猜。另一個(gè)OR M大坑是懶加載與N1查詢。JPA的ManyToOne(fetch FetchType.LAZY)看起來(lái)美好但當(dāng)你遍歷上百個(gè)實(shí)體去訪問(wèn)關(guān)聯(lián)對(duì)象時(shí)連接池會(huì)被幾百條查詢瞬間塞滿。你以為自己寫(xiě)了一條主查詢實(shí)際數(shù)據(jù)庫(kù)收到了101條SQL。懶加載不是性能救星它是延遲爆炸的溫床除非你明確知道自己在什么事務(wù)內(nèi)、訪問(wèn)哪些關(guān)聯(lián)路徑。更好的做法是寫(xiě)一個(gè)查詢方法指明要抓取的關(guān)聯(lián)字段或者用EntityGraph主動(dòng)加載。MyBatis用戶也別笑你的一次查詢里嵌套了collection搞不好也會(huì)發(fā)生同樣的循環(huán)查詢只是你看不見(jiàn)而已。時(shí)區(qū)與字符集兩個(gè)最容易被忽略的“政治正確”時(shí)區(qū)問(wèn)題在配置里出現(xiàn)過(guò)但這里值得單獨(dú)拉出來(lái)鞭尸。很多開(kāi)發(fā)機(jī)連的是本機(jī)MySQL時(shí)區(qū)跟服務(wù)器一致所以從來(lái)不報(bào)錯(cuò)??梢坏┎渴鸬桨⒗镌芃ySQL的時(shí)區(qū)是UTCJVM的時(shí)區(qū)是GMT8連接串里又不帶serverTimezone結(jié)果所有DateTime類型的數(shù)據(jù)就會(huì)像被施了魔法一樣晚八個(gè)小時(shí)。更崩潰的是你本地測(cè)試一切正常上到生產(chǎn)就出鬼。時(shí)區(qū)不是業(yè)務(wù)問(wèn)題而是配置紀(jì)律問(wèn)題本地通過(guò)恰恰是最大的陷阱。字符集的坑也類似。characterEncodingutf8寫(xiě)在URL里但數(shù)據(jù)庫(kù)表的collation卻是utf8mb4_general_ci這倆之間沒(méi)沖突但你要是存emoji就會(huì)變成問(wèn)號(hào)。連接層的編碼只決定傳輸字節(jié)真正決定存什么的是表的charset與collation。你以為在SpringBoot里加一個(gè)spring.datasource.sql-script-encoding就能解決那只是腳本執(zhí)行時(shí)的編碼跟運(yùn)行時(shí)查詢毫無(wú)關(guān)系。所以建庫(kù)時(shí)把DEFAULT CHARACTER SET utf8mb4寫(xiě)清楚連接URL再跟上characterEncodingutf8雙管齊下才敢存一個(gè)笑臉。監(jiān)控與排查的黑洞你連日志里在報(bào)什么都不知道最后一種常見(jiàn)的誤區(qū)是遇到數(shù)據(jù)庫(kù)問(wèn)題就悶頭改代碼從不看連接池監(jiān)控和SQL日志。SpringBoot有spring.datasource.hikari.connection-timeout、validation-timeout等參數(shù)但很多人從沒(méi)開(kāi)啟過(guò)Hikari的日志級(jí)別。當(dāng)你把logging.level.com.zaxxer.hikariDEBUG打開(kāi)就能看到連接獲取、歸還、超時(shí)的完整軌跡。能打印出連接池每次借出的耗時(shí)你就已經(jīng)解決了60%的問(wèn)題。然而更多人寧愿在Stack Overflow上搜三個(gè)小時(shí)也不愿花三分鐘開(kāi)啟這個(gè)日志。再看數(shù)據(jù)庫(kù)端的慢查詢?nèi)罩灸遣攀峭评碚嫦嗟脑牧?。SpringBoot的spring.jpa.show-sql只是控制臺(tái)打SQL它不告訴你這條SQL在執(zhí)行時(shí)走了多少行、掃描了多少索引。真正的瓶頸往往藏在索引缺失或隱式類型轉(zhuǎn)換里比如WHERE trade_no 12345而列是varcharMySQL會(huì)隱式轉(zhuǎn)換導(dǎo)致索引失效。每次數(shù)據(jù)庫(kù)出問(wèn)題第一反應(yīng)應(yīng)該去看數(shù)據(jù)庫(kù)的slow_log和explain而不是盯著應(yīng)用日志里的異常棧。當(dāng)你把連接池指標(biāo)、SQL執(zhí)行計(jì)劃、事務(wù)日志三塊拼在一起所謂的“神秘錯(cuò)誤”都會(huì)變成簡(jiǎn)單的因果鏈。SpringBoot集成數(shù)據(jù)庫(kù)真正的困難不在于寫(xiě)代碼而在于你愿不愿意去理解連接、事務(wù)、映射這三層底下的運(yùn)行機(jī)制。每一次配置的偷懶都會(huì)在某個(gè)不眠夜變成張牙舞爪的報(bào)錯(cuò)。優(yōu)雅地連接數(shù)據(jù)庫(kù)本質(zhì)上是用清晰的認(rèn)知去置換表面的簡(jiǎn)潔。與其記住各種報(bào)錯(cuò)的修復(fù)方案不如把上述那些誤區(qū)逐個(gè)在本地環(huán)境模擬一遍親手感受連接泄露的曲線、事務(wù)回滾的邊界、映射錯(cuò)亂的荒謬。當(dāng)你能一遍遍指認(rèn)這些陷阱時(shí)SpringBoot的“自動(dòng)配置”才真正為你所用而不是讓你成為它的奴隸。