前言
有些小伙伴在工作中,可能經(jīng)常遇到這樣的場景:系統(tǒng)上線初期運(yùn)行良好,隨著數(shù)據(jù)量增長,突然某天接口超時、CPU飆升、甚至整個系統(tǒng)癱瘓。
排查半天,發(fā)現(xiàn)是某個SQL語句寫的有問題,或者是數(shù)據(jù)庫配置不當(dāng)導(dǎo)致的。
今天這篇文章我就從淺入深,帶你徹底避開MySQL的6大常見雷區(qū),希望的對你會有所幫助。
為什么MySQL雷區(qū)如此之多?
在深入具體雷區(qū)之前,我們先聊聊為什么MySQL這么容易踩坑。
這背后有幾個深層次原因:
- 看似簡單:MySQL語法簡單,入門容易,讓很多人低估了它的復(fù)雜性 
- 默認(rèn)配置坑多:MySQL的默認(rèn)配置往往不是最優(yōu)的,需要根據(jù)業(yè)務(wù)場景調(diào)整 
- 漸進(jìn)式問題:很多問題在數(shù)據(jù)量小的時候不會暴露,等到暴露時已經(jīng)為時已晚 
- 知識更新快:從5.6到5.7再到8.0,每個版本都有重要變化,需要持續(xù)學(xué)習(xí) 
有些小伙伴在工作中,可能直接用默認(rèn)配置部署MySQL,或者在寫SQL時只關(guān)注功能實(shí)現(xiàn),忽略了性能問題。
這就是為什么我們需要系統(tǒng)性地了解這些雷區(qū)。
好了,讓我們開始今天的主菜。我將從最常見的索引失效,逐步深入到復(fù)雜的死鎖問題,確保每個雷區(qū)都講透、講懂。
雷區(qū)一:索引失效的常見場景
索引是MySQL性能的基石,但錯誤的使用方式會讓索引失效,導(dǎo)致全表掃描。
這是最常見的性能雷區(qū)。
為什么索引會失效?
索引失效的本質(zhì)是MySQL優(yōu)化器認(rèn)為使用索引的成本高于全表掃描。
了解這些場景,可以幫助我們寫出更高效的SQL。
示例場景:
-- 創(chuàng)建測試表
CREATE TABLE user (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50),
    age INT,
    email VARCHAR(100),
    created_time DATETIME,
    INDEX idx_name (name),
    INDEX idx_age (age),
    INDEX idx_created_time (created_time)
);
-- 雷區(qū)1.1:對索引列進(jìn)行函數(shù)操作
-- 錯誤寫法:索引失效
EXPLAIN SELECT * FROM user WHERE DATE(created_time) = '2023-01-01';
-- 正確寫法:使用范圍查詢
EXPLAIN SELECT * FROM user 
WHERE created_time >= '2023-01-01 00:00:00' 
AND created_time < '2023-01-02 00:00:00';
-- 雷區(qū)1.2:隱式類型轉(zhuǎn)換
-- 錯誤寫法:name是字符串,但用了數(shù)字,導(dǎo)致索引失效
EXPLAIN SELECT * FROM user WHERE name = 123;
-- 正確寫法:類型匹配
EXPLAIN SELECT * FROM user WHERE name = '123';
-- 雷區(qū)1.3:前導(dǎo)模糊查詢
-- 錯誤寫法:LIKE以%開頭,索引失效
EXPLAIN SELECT * FROM user WHERE name LIKE '%三%';
-- 正確寫法:非前導(dǎo)模糊查詢,可以使用索引
EXPLAIN SELECT * FROM user WHERE name LIKE '蘇%';
-- 雷區(qū)1.4:OR條件使用不當(dāng)
-- 錯誤寫法:age有索引,email無索引,導(dǎo)致整個查詢無法使用索引
EXPLAIN SELECT * FROM user WHERE age = 25 OR email = 'test@example.com';
-- 正確寫法:使用UNION優(yōu)化OR查詢
EXPLAIN 
SELECT * FROM user WHERE age = 25
UNION
SELECT * FROM user WHERE email = 'test@example.com';
深度剖析
有些小伙伴在工作中可能會疑惑:為什么這些寫法會導(dǎo)致索引失效?
- 函數(shù)操作破壞索引有序性 
- 隱式類型轉(zhuǎn)換的本質(zhì) 
- 當(dāng)類型不匹配時,MySQL會進(jìn)行隱式轉(zhuǎn)換 
- 實(shí)際上相當(dāng)于:- CAST(name AS SIGNED) = 123
 
- 對索引列進(jìn)行了函數(shù)操作,導(dǎo)致失效 
- 前導(dǎo)模糊查詢的B+樹遍歷 
- B+樹索引按照前綴排序 
- LIKE '蘇%'可以利用前綴匹配
 
- LIKE '%三'無法確定前綴,必須全表掃描
 
避坑指南
雷區(qū)二:事務(wù)隔離級別與幻讀
事務(wù)隔離級別是MySQL中比較復(fù)雜的概念,理解不當(dāng)會導(dǎo)致數(shù)據(jù)不一致和性能問題。
為什么事務(wù)隔離級別重要?
不同的隔離級別在數(shù)據(jù)一致性、性能、并發(fā)性之間做出不同權(quán)衡。
選擇不當(dāng)會出現(xiàn)臟讀、不可重復(fù)讀、幻讀等問題。
示例場景:
-- 查看當(dāng)前事務(wù)隔離級別
SELECT @@transaction_isolation;
-- 設(shè)置隔離級別為REPEATABLE-READ(默認(rèn))
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 場景:轉(zhuǎn)賬業(yè)務(wù)中的幻讀問題
-- 會話1:事務(wù)A
START TRANSACTION;
SELECT COUNT(*) FROM account WHERE user_id = 1001; -- 返回2
-- 會話2:事務(wù)B  
START TRANSACTION;
INSERT INTO account (user_id, balance) VALUES (1001, 500);
COMMIT;
-- 會話1:事務(wù)A繼續(xù)
SELECT COUNT(*) FROM account WHERE user_id = 1001; -- 仍然返回2(可重復(fù)讀)
UPDATE account SET balance = balance + 100 WHERE user_id = 1001; -- 影響3行!
SELECT COUNT(*) FROM account WHERE user_id = 1001; -- 返回3,出現(xiàn)幻讀!
COMMIT;
深度剖析
有些小伙伴在工作中可能遇到過:明明查詢時不存在的數(shù)據(jù),更新時卻影響到了。這就是典型的幻讀問題。
幻讀的本質(zhì):
- 在可重復(fù)讀隔離級別下,普通SELECT看不到其他事務(wù)的插入 
- 但UPDATE/DELETE會看到所有已提交的數(shù)據(jù) 
- 這導(dǎo)致同一個事務(wù)內(nèi),查詢和更新看到的數(shù)據(jù)不一致 
MySQL的解決方案:
為了理解間隙鎖的工作原理,我畫了一個鎖范圍示意圖:

這個圖展示了當(dāng)查詢id > 8時,MySQL會鎖定[5,10]的間隙、ID=10的記錄,以及[10,∞]的間隙,防止其他事務(wù)插入ID>8的數(shù)據(jù)。
避坑指南
- 理解不同隔離級別的特性 
- 在REPEATABLE-READ下,注意UPDATE可能產(chǎn)生幻讀 
- 對于需要絕對一致性的場景,使用SERIALIZABLE隔離級別 
- 合理設(shè)計(jì)事務(wù)邊界,避免長事務(wù) 
雷區(qū)三:大數(shù)據(jù)量下的分頁優(yōu)化
分頁查詢是Web應(yīng)用中最常見的操作,但在大數(shù)據(jù)量下性能急劇下降。
為什么分頁會變慢?
LIMIT offset, size在offset很大時,需要掃描并跳過大量記錄,造成性能瓶頸。
示例場景:
-- 創(chuàng)建測試表,假設(shè)有1000萬數(shù)據(jù)
CREATE TABLE order (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    amount DECIMAL(10,2),
    status TINYINT,
    created_time DATETIME,
    INDEX idx_created_time (created_time)
);
-- 雷區(qū):傳統(tǒng)的分頁寫法
-- 當(dāng)offset達(dá)到500萬時,性能急劇下降
EXPLAIN SELECT * FROM order 
ORDER BY created_time DESC 
LIMIT 5000000, 20;
-- 優(yōu)化方案1:游標(biāo)分頁(推薦)
-- 第一頁
SELECT * FROM order 
ORDER BY created_time DESC, id DESC 
LIMIT 20;
-- 第二頁:記住上一頁最后一條記錄的created_time和id
SELECT * FROM order 
WHERE created_time < '2023-06-01 10:00:00' 
   OR (created_time = '2023-06-01 10:00:00' AND id < 1000000)
ORDER BY created_time DESC, id DESC 
LIMIT 20;
-- 優(yōu)化方案2:子查詢優(yōu)化(適用于非游標(biāo)場景)
SELECT * FROM order 
WHERE id >= (
    SELECT id FROM order 
    ORDER BY created_time DESC 
    LIMIT 5000000, 1
)
ORDER BY created_time DESC 
LIMIT 20;
深度剖析
有些小伙伴在工作中可能發(fā)現(xiàn),為什么offset越大查詢越慢?
傳統(tǒng)分頁的性能瓶頸:
- 大量無效IO:需要讀取并跳過offset條記錄
- 回表成本:對于非覆蓋索引,需要回表查詢完整數(shù)據(jù)
- 排序開銷:大數(shù)據(jù)量的排序可能在磁盤進(jìn)行
游標(biāo)分頁的優(yōu)勢:
- 直接定位到起始位置,無需跳過大量記錄
- 利用索引的有序性,避免排序操作
- 性能穩(wěn)定,不隨數(shù)據(jù)量增長而下降
為了理解傳統(tǒng)分頁與游標(biāo)分頁的區(qū)別,我畫了一個對比圖:

避坑指南
- 優(yōu)先使用游標(biāo)分頁(基于游標(biāo)或時間戳)
- 如果必須使用傳統(tǒng)分頁,使用子查詢優(yōu)化
- 確保排序字段有索引
- 前端配合使用無限滾動或游標(biāo)分頁UI 
雷區(qū)四:字符集與排序規(guī)則陷阱
字符集問題經(jīng)常在系統(tǒng)國際化或多語言支持時暴露,處理不當(dāng)會導(dǎo)致亂碼、排序錯誤、索引失效。
為什么字符集如此重要?
不同的字符集支持不同的字符范圍,排序規(guī)則影響字符串比較和排序結(jié)果。
示例場景:
SHOW VARIABLES LIKE 'character_set%';
SHOW VARIABLES LIKE 'collation%';
-- 雷區(qū):UTF8不是真正的UTF-8
-- MySQL的utf8最多支持3字節(jié),無法存儲emoji等4字節(jié)字符
CREATE TABLE user_utf8 (
    id INT PRIMARY KEY,
    name VARCHAR(50) CHARACTER SET utf8
);
-- 插入emoji表情失敗
INSERT INTO user_utf8 VALUES (1, '張三??'); -- 錯誤!
-- 正確:使用utf8mb4
CREATE TABLE user_utf8mb4 (
    id INT PRIMARY KEY,
    name VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
);
-- 插入emoji成功
INSERT INTO user_utf8mb4 VALUES (1, '張三??'); -- 成功!
-- 雷區(qū):排序規(guī)則影響查詢結(jié)果
CREATE TABLE product (
    id INT PRIMARY KEY,
    name VARCHAR(100)
) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
-- 大小寫不敏感查詢
SELECT * FROM product WHERE name = 'apple'; -- 會匹配'Apple', 'APPLE'
-- 如果需要大小寫敏感,使用binary或特定collation
SELECT * FROM product WHERE name = BINARY 'apple'; -- 只匹配'apple'
 
  
深度剖析
有些小伙伴在工作中可能遇到過存儲emoji失敗,或者查詢時大小寫匹配異常,這都是字符集配置不當(dāng)導(dǎo)致的。
UTF8 vs UTF8MB4:
- utf8:MySQL歷史上的"假UTF-8",最多3字節(jié),不支持emoji、部分中文生僻字
- utf8mb4:真正的UTF-8實(shí)現(xiàn),支持4字節(jié),推薦使用
排序規(guī)則的影響:
- _ci結(jié)尾:大小寫不敏感(Case Insensitive)
- _cs結(jié)尾:大小寫敏感(Case Sensitive)
- _bin結(jié)尾:二進(jìn)制比較,完全匹配
為了理解不同字符集的存儲范圍,我畫了一個對比圖:

避坑指南
雷區(qū)五:外鍵與級聯(lián)操作的隱患
外鍵約束可以保證數(shù)據(jù)完整性,但使用不當(dāng)會帶來性能問題和復(fù)雜的維護(hù)成本。
為什么外鍵是雙刃劍?
外鍵在保證數(shù)據(jù)一致性的同時,會帶來鎖競爭、維護(hù)復(fù)雜、遷移困難等問題。
示例場景:
-- 創(chuàng)建帶外鍵的表結(jié)構(gòu)
CREATE TABLE department (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL
);
CREATE TABLE employee (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    department_id INT,
    FOREIGN KEY (department_id) 
        REFERENCES department(id)
        ON DELETE CASCADE
        ON UPDATE CASCADE
);
-- 雷區(qū)1:級聯(lián)刪除導(dǎo)致意外數(shù)據(jù)丟失
-- 刪除部門時,所有相關(guān)員工也被刪除,可能不是期望的行為
DELETE FROM department WHERE id = 1; -- 部門1的所有員工都被刪除!
-- 雷區(qū)2:外鍵鎖競爭
-- 會話1:刪除部門
START TRANSACTION;
DELETE FROM department WHERE id = 1; -- 持有部門1的鎖
-- 會話2:在同一個部門插入員工(被阻塞)
START TRANSACTION;
INSERT INTO employee (name, department_id) VALUES ('新員工', 1); -- 等待鎖
-- 雷區(qū)3:數(shù)據(jù)遷移困難
-- 導(dǎo)入數(shù)據(jù)時必須按正確順序,否則外鍵約束失敗
深度剖析
有些小伙伴在工作中可能發(fā)現(xiàn),系統(tǒng)并發(fā)量上來后,經(jīng)常出現(xiàn)鎖等待超時,外鍵約束是常見原因之一。
外鍵的性能影響:
- 鎖范圍擴(kuò)大:操作父表時需要檢查子表,可能鎖定更多數(shù)據(jù)
- 死鎖風(fēng)險:多表之間的外鍵關(guān)系容易形成死鎖環(huán)路
- 并發(fā)下降:外鍵檢查需要額外加鎖,降低系統(tǒng)并發(fā)能力
級聯(lián)操作的風(fēng)險:
- ON DELETE CASCADE:誤刪父表記錄會導(dǎo)致大量子表數(shù)據(jù)丟失
- ON UPDATE CASCADE:更新主鍵時傳播到所有子表,性能影響大
為了理解外鍵鎖的競爭關(guān)系,我畫了一個鎖等待示意圖:

避坑指南
- 高并發(fā)場景慎用外鍵,可在應(yīng)用層保證數(shù)據(jù)一致性
- 如果使用外鍵,避免ON DELETE/UPDATE CASCADE
- 使用軟刪除替代物理刪除
- 批量操作時暫時禁用外鍵檢查 
 雷區(qū)六:連接池配置不當(dāng)
連接池配置看似簡單,實(shí)則影響整個系統(tǒng)的穩(wěn)定性和性能。
配置不當(dāng)會導(dǎo)致連接泄露、池化失效等問題。
為什么連接池如此關(guān)鍵?
數(shù)據(jù)庫連接是寶貴的資源,創(chuàng)建和銷毀成本很高。
連接池管理不當(dāng)會直接導(dǎo)致系統(tǒng)崩潰。
示例場景:
// Spring Boot中的Druid連接池配置
@Configuration
public class DruidConfig {
    
    @Bean
    @ConfigurationProperties("spring.datasource.druid")
    public DataSource dataSource() {
        return DruidDataSourceBuilder.create().build();
    }
}
// application.yml配置
spring:
  datasource:
    druid:
      # 雷區(qū)1:初始連接數(shù)過大,浪費(fèi)資源
      initial-size: 50
      # 雷區(qū)2:最大連接數(shù)過小,并發(fā)時等待
      max-active: 20
      # 雷區(qū)3:最小空閑連接數(shù)不合理
      min-idle: 5
      # 雷區(qū)4:獲取連接超時時間過短
      max-wait: 3000
      # 雷區(qū)5:沒有配置連接有效性檢查
      validation-query: SELECT 1
      test-on-borrow: true
      test-on-return: false
      test-while-idle: true
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
深度剖析
有些小伙伴在工作中可能遇到過連接池耗盡、連接泄露等問題,這都是配置不當(dāng)導(dǎo)致的。
連接池的核心參數(shù):
連接泄露的檢測與預(yù)防:
// 常見的連接泄露模式
public class UserService {
    
    // 錯誤寫法:連接未關(guān)閉
    public User getUser(int id) {
        Connection conn = dataSource.getConnection();
        // 執(zhí)行查詢...
        // 忘記調(diào)用conn.close()
        return user;
    }
    
    // 正確寫法:使用try-with-resources
    public User getUserCorrect(int id) {
        try (Connection conn = dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement("SELECT * FROM user WHERE id = ?")) {
            
            stmt.setInt(1, id);
            ResultSet rs = stmt.executeQuery();
            // 處理結(jié)果...
            return user;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}
為了理解連接池的工作機(jī)制,我畫了一個連接池狀態(tài)轉(zhuǎn)換圖:

避坑指南
- 根據(jù)業(yè)務(wù)壓力合理配置連接池參數(shù)
- 使用try-with-resources確保連接關(guān)閉
- 開啟連接泄露檢測功能
- 監(jiān)控連接池狀態(tài),設(shè)置合理的告警閾值
總結(jié)
經(jīng)過以上6大雷區(qū)的分析,相信你對MySQL的常見坑點(diǎn)有了更深入的理解。
雷區(qū)對比總結(jié):

參考文章:原文鏈接?
該文章在 2025/10/31 16:11:35 編輯過