MySQL 索引和鎖機制總結


今天總結一下 MySQL 的索引和鎖機制相關的知識點。之所以總結索引,是因為索引非常重要,對於任何一個網站數據庫來說,查詢占 80% 以上,優化數據庫性能,最主要是優化查詢效率,使用索引是提高查詢效率的最有效途徑之一。之所以總結 MySQL 的鎖機制,一方面是因為網上資料太少,平時大家也很少關注,另一方面是了解 MySQL 的鎖機制,有利於數據庫的優化設計,在一些重要場景中合理使用鎖機制,能夠有效保障數據的安全性。


一、MySQL 索引

MySQL 索引的主要用途就是提高數據的查詢性能。索引本質上就是一種數據結構,在表數據之外,數據庫系統還維護着滿足特定查找算法的數據結構,這些數據結構以某種方式指向數據, 並且利用這些數據結構上實現高級查找算法,這種數據結構就是索引。

MySQL 索引按照功能分類,主要包含以下索引:

  • 普通索引:最基本的索引,它沒有任何限制。
  • 唯一索引:索引列的值必須唯一,但允許有空值。如果是組合索引,則列值組合必須唯一。
  • 主鍵索引:一種特殊的唯一索引,不允許有空值。一般在建表時同時創建主鍵索引。
  • 聯合索引:就是將多個列組合在一起創建索引。
  • 外鍵索引:只有 InnoDB 引擎支持外鍵索引,用來保證數據的一致性、完整性,可以實現級聯操作。
  • 全文索引:快速匹配全部文檔的方式。InnoDB 引擎 5.6 版本以后才支持全文索引。MEMORY 引擎不支持全文索引。

按照結構分類,主要包含兩種索引:

  • B+Tree 索引 :MySQL 中使用最頻繁的一個索引數據結構,是 InnoDB 和 MyISAM 存儲引擎默認的索引類型。
  • Hash 索引 : MySQL 中 Memory 存儲引擎默認支持的索引類型。

(1)索引的創建和刪除

-- MySQL 創建索引的語法格式如下:
CREATE [UNIQUE|FULLTEXT] INDEX 索引名稱
[USING 索引類型]  -- 默認是 B+TREE 索引
ON 表名(列名...);

----------------------------

-- 在創建一張表時,可以直接創建主鍵索引
-- 因此主鍵列不需要單獨創建索引
CREATE TABLE test(
	id INT PRIMARY KEY AUTO_INCREMENT,
	data1 VARCHAR(100),
	data2 INT
);

-- 在登錄用戶表中,為【用戶注冊時間】創建普通索引
CREATE INDEX idx_register_time ON login_user(register_time);

-- 在登錄用戶表中,為【用戶名】創建唯一索引
CREATE UNIQUE INDEX udx_user_name ON login_user(user_name);

----------------------------

-- 查看一個表中的索引,語法格式為:
-- SHOW INDEX FROM 表名;
SHOW INDEX FROM login_user;
-- 使用 ALTER 為表添加索引,語法格式如下:

-- 普通索引
ALTER TABLE 表名 ADD INDEX 索引名稱(列名);

-- 唯一索引
ALTER TABLE 表名 ADD UNIQUE 索引名稱(列名);

-- 主鍵索引
ALTER TABLE 表名 ADD PRIMARY KEY(主鍵列名); 

-- 聯合索引
ALTER TABLE 表名 ADD INDEX 索引名稱(列名1,列名2,...);

-- 外鍵索引(添加外鍵約束,就是外鍵索引)
ALTER TABLE 表名 ADD CONSTRAINT 外鍵名 FOREIGN KEY (本表外鍵列名) REFERENCES 主表名(主鍵列名);

-- 全文索引(mysql只支持文本類型)
ALTER TABLE 表名 ADD FULLTEXT 索引名稱(列名);

----------------------------

-- 為 login_user 表中家庭地址 address 列添加全文索引
ALTER TABLE login_user ADD FULLTEXT fdx_address(address);

----------------------------

-- 刪除索引的語法格式為:
-- DROP INDEX 索引名稱 ON 表名;
DROP INDEX fdx_address ON login_user;

(2)聯合索引的特點

MySQL 在一張表中建立聯合索引時,會遵循最左匹配的原則:即在查詢數據時從聯合索引的最左邊的字段開始匹配。
假設我們在 login_user 表中,針對 user_name 、mobile、email 這三個字段創建聯合索引:

ALTER TABLE login_user ADD INDEX idx_login(user_name, mobile, email);

聯合索引 idx_login 實際建立了 (user_name)、 (user_name, mobile)、 (user_name, mobile, email) 三個索引。在查詢 SQL 語句中,只要包含最左邊的字段 user_name 即可,不需要考慮字段順序,因為 MySQL 的優化器會自動幫助我們調整 where 條件中的字段順序,匹配我們建立的索引。

-- 聯合索引不需要考慮字段順序,MySQL優化器會自動調整 where 條件后面的字段順序,
-- 只要包含最左邊的字段即可,所以下面的 SQL語句都可以命中索引:

-- 使用了  (user_name, mobile, email) 這個索引
SELECT * FROM login_user WHERE email='jobs@test.com' AND mobile='158xxxx2108' AND user_name='喬豆豆';

-- 使用了  (user_name, mobile) 這個索引
SELECT * FROM login_user WHERE user_name='候胖胖' AND mobile='134xxxx4820';

-- 使用了  (user_name) 這個索引
SELECT * FROM login_user WHERE user_name='藺贊贊'

-- 使用了  (user_name) 這個索引, email 這個字段的查詢條件沒有索引
SELECT * FROM login_user WHERE email='wolfer@funny.com' AND user_name='任肥肥';

----------------------------

-- 下面的 SQL 查詢語句,不會使用索引,因為不包含聯合索引最左邊的字段 user_name
SELECT * FROM login_user WHERE mobile='134xxxx6559' AND email='wolfer@funny.com';

(3)使用索引注意事項

MySQL 數據表中創建索引時請盡量考慮一些原則,便於提升索引的使用效率,更高效的使用索引:

  • 對查詢頻次較高,且數據量比較大的表,一定要創建索引。
  • 對於值不重復的字段,盡量使用唯一索引,區分度越高,使用索引的效率越高。
  • 索引字段的選擇,應當從 where 子句的條件中提取,如果 where 子句中的組合比較多,那么應當挑選最常用、過濾效果最好的字段。
  • 盡量使用存儲值比較短的字段創建索引,索引創建之后也是使用硬盤來存儲的,因此提升索引訪問的 I/O 效率,也可以提升總體的訪問效率。假如構成索引的字段總長度比較短,那么在給定大小的存儲塊內可以存儲更多的索引值,相應的可以有效的提升 MySQL 訪問索引的 I/O 效率。
  • 索引可以有效的提升查詢數據的效率,但索引數量並不是越多越好。索引的存儲也是占用硬盤空間的,當對一張表進行增、刪、改操作后,其索引也是需要進行維護的,索引的存儲量會隨着數據量的增大而增大,過多的索引會增加數據庫對該表索引的維護負擔,降低該表增刪改查的整體性能。

二、MySQL 鎖機制

常用的數據庫(Oracle,SQL Server,MySQL)的事務,都有四種隔離級別,如下所示:

隔離級別 中文名稱 出現臟讀 出現不可重復讀 出現幻讀 數據庫默認隔離級別
read uncommitted 讀未提交
read committed 讀已提交 Oracle / SQL Server
repeatable read 可重復讀 MySQL
serializable 串行化

以上排序,按照隔離級別從小到大,安全性越來越高,但是效率越來越低。
一般情況下,不會使用 read uncommitted 和 serializable ,因為 read uncommitted 安全級別最差,在並行處理時,各種問題都可能出現; serializable 雖然不會出現問題,但是所有對數據庫的操作無法並行處理,只能單線程排隊處理,性能效率比較低。
一般情況下,我們不要修改數據庫事務的默認隔離級別。要想修改 MySQL 數據庫隔離級別,使用以下 SQL 語句:

-- 將 MySQL 的數據庫隔離級別,修改為串行化
-- 注意:每次修改完數據庫隔離級別時,客戶端需要重新連接 MySQL 才能生效
set global transaction isolation level serializable;

下面簡單介紹一下臟讀、不可重復讀、幻讀這三個問題:

問題 現象
臟讀 在一個事務處理過程中讀取了另一個未提交的事務中的數據,導致兩次查詢結果不一致
不可重復讀 在一個事務處理過程中讀取了另一個事務中修改並已提交的數據,導致兩次查詢結果不一致
幻讀 查詢某些記錄時不存在,但 insert 時發現此記錄已存在,造成添加失敗。
查詢某些記錄時不存在,但 delete 時卻發現刪除成功(受影響行數大於 0),導致誤刪了數據

從上面介紹的情況可以發現,MySQL 默認使用 repeatable read 隔離級別,只會有可能出現幻讀的問題,怎么解決呢?
答案就是:采用 MySQL 的鎖機制來解決。可以考慮在一些比較重要的場景中使用,比如跟錢有關的業務,以及動態數據遷移等等。


(1)鎖的介紹

我們首先對 MySQL 的鎖進行一下分類,這樣能夠從全局的角度去理解鎖。

按操作分類:

  • 讀鎖:也叫共享鎖。針對同一份數據,多個事務的讀取操作,可以同時加鎖而不會互相影響 ,但不能修改數據記錄。
  • 寫鎖:也叫排他鎖。一個事務對加鎖數據的讀寫操作沒有完成之前,會阻止其他事務對該數據的讀寫操作。

按粒度分類:

  • 表鎖:加鎖時會鎖定整張表。開銷小,加鎖快;不會出現死鎖;鎖定力度大,發生鎖沖突概率高,並發度最低。
  • 行鎖:加鎖時會鎖定當前操作行。開銷大,加鎖慢;會出現死鎖;鎖定粒度小,發生鎖沖突的概率低,並發度高。
存儲引擎 表鎖 行鎖
MyISAM 支持 不支持
InnoDB 支持 支持
MEMORY 支持 不支持

按使用方式分類:

  • 悲觀鎖:每次查詢數據時都認為別人會修改,很悲觀,所以查詢時加鎖。
  • 樂觀鎖:每次查詢數據時都認為別人不會修改,很樂觀,但是更新數據時,需要先判斷一下在此期間別人有沒有去更新這個數據,然后決定自己要如何處理。常見的 3 種數據庫(Oracle,SQL Server,MySQL)不提供樂觀鎖,需要自己去實現,實現起來也很容易。

InnoDB 同時支持表鎖和行鎖,MyISAM 和 MEMORY 只支持表鎖。
下面我們針對 InnoDB 和 MyISAM 兩種存儲引擎進行鎖操作介紹,有關 MEMORY 的鎖操作,可以參考 MyISAM 的鎖操作。
在演示相關鎖操作的代碼之前,我們先介紹一下要操作的示例表結構,我們有一張示例表 employee ,該表只有 3 個字段:

字段名稱 說明
id 員工id,只針對該字段創建了主鍵索引,其它字段沒有索引
name 員工姓名
money 員工薪水

(2)InnoDB 鎖操作

InnoDB 同時支持表鎖和行鎖,默認使用行鎖,當編寫的 SQL 語句的 Where 條件無法使用索引時,則自動提升為表鎖。

-- InnoDB 讀鎖(共享鎖)語法格式如下:
SELECT語句 LOCK IN SHARE MODE;

/*
讀鎖(共享鎖)的特點:數據可以被多個事務查詢,但是不能修改
*/

----------------------------

/*
打開【第一個 MySQL 客戶端工具】,先運行下面前 3 條 SQL,別運行 COMMIT 語句。
*/

-- 開啟事務
START TRANSACTION;

-- 查詢id為 1 的數據記錄。加入共享鎖
SELECT * FROM employee WHERE id=1 LOCK IN SHARE MODE;

-- 查詢名稱為其它某個人的數據記錄。加入共享鎖
SELECT * FROM employee WHERE name='侯胖胖' LOCK IN SHARE MODE;

-- 提交事務
COMMIT;

----------------------------

/*
打開【第二個 MySQL 客戶端工具】,手動逐條選中以下 SQL 語句並運行。
*/

-- 開啟事務
START TRANSACTION;

-- 查詢 id 為 1 的數據記錄
-- 結果:可以查詢
SELECT * FROM employee WHERE id=1;

-- 查詢 id 為 1 的數據記錄,並加入讀鎖(共享鎖)
-- 結果:可以查詢,這說明【共享鎖】和【共享鎖】是兼容的
SELECT * FROM employee WHERE id=1 LOCK IN SHARE MODE;

-- 修改 id 為 2 的姓名為藺贊贊
-- 結果:修改成功,因為 id 是 employee 表的索引,而 InnoDB 引擎默認是行鎖
-- 而且【第一個客戶端工具】並沒有對 id 為 2 的記錄加鎖
UPDATE employee SET name='藺贊贊' WHERE id=2;

-- 修改id為 1 的姓名為任肥肥
-- 結果:不能修改,會出現阻塞。
-- 因為【第一個客戶端工具】給 id 為 1 的記錄加了讀鎖(共享鎖),其它線程只能讀,不能寫
-- 只有當【第一個客戶端工具】運行 COMMIT 后,才能修改成功
UPDATE employee SET name='任肥肥' WHERE id=1;

-- 通過 name 字段,修改 monkey 字段的值
-- 結果:不能修改。InnoDB引擎如果不采用帶索引的列。則會提升為表鎖
-- 所以由於 name 不是 employee 表的索引,無法使用行鎖,自動升級為表鎖
-- 但是此處的表鎖,跟【第一個客戶端工具】的 id 為 1 的行鎖有沖突,所以這里會阻塞
-- 只有當【第一個客戶端工具】運行 COMMIT 后,才能修改成功
UPDATE employee SET money=30000 WHERE name='喬豆豆';

-- 提交事務
COMMIT;
-- InnoDB 寫鎖(排他鎖)語法格式如下:
SELECT語句 FOR UPDATE;

/*
寫鎖(排他鎖)的特點:加鎖的數據,不能被其他事務【加鎖查詢】或【修改】
*/

----------------------------

/*
打開【第一個 MySQL 客戶端工具】,先運行下面前 2 條 SQL,別運行 COMMIT 語句。
*/
-- 開啟事務
START TRANSACTION;

-- 查詢id為1的數據記錄,並加入排他鎖
SELECT * FROM employee WHERE id=1 FOR UPDATE;

-- 提交事務
COMMIT;

----------------------------

/*
打開【第二個 MySQL 客戶端工具】,手動逐條選中以下 SQL 語句並運行。
*/

-- 開啟事務
START TRANSACTION;

-- 查詢 id 為 1 的數據記錄
-- 結果:普通查詢沒問題
SELECT * FROM employee WHERE id=1;

-- 查詢 id 為 1 的數據記錄,並加入共享鎖
-- 結果:不能查詢,會出現阻塞。因為寫鎖(排他鎖)不能和其他鎖共存
-- 只有當【第一個客戶端工具】運行 COMMIT 后,才能查詢
SELECT * FROM employee WHERE id=1 LOCK IN SHARE MODE;

-- 查詢 id 為 1 的數據記錄,並加入寫鎖(排他鎖)
-- 結果:不能查詢,會出現阻塞。因為寫鎖(排他鎖)不能和其他鎖共存
-- 只有當【第一個客戶端工具】運行 COMMIT 后,才能查詢
SELECT * FROM employee WHERE id=1 FOR UPDATE;

-- 修改 id 為 1 的姓名為天蓬
-- 結果:不能修改,會出現阻塞。因為寫鎖(排他鎖)不能和其他鎖共存
-- 只有當【第一個客戶端工具】運行 COMMIT 后,才能修改
UPDATE employee SET name='天蓬' WHERE id=1;

-- 提交事務
COMMIT;

最后得出的結論就是:

  • (讀鎖)共享鎖和(讀鎖)共享鎖:兼容
  • (讀鎖)共享鎖和(寫鎖)排他鎖:沖突
  • (寫鎖)排他鎖和(寫鎖)排他鎖:沖突
  • (寫鎖)排他鎖和(讀鎖)共享鎖:沖突

(3)MyISAM 鎖操作

MyISAM 和 MEMORY 僅支持表鎖,而且不支持事務,只有 InnoDB 才支持事務,下面僅對 MyISAM 進行鎖操作演示。

-- MyISAM 讀鎖,進行加鎖的語法格式如下:
LOCK TABLE 表名 READ;

/*
讀鎖的特點:所有連接只能讀取數據,不能修改
*/

-- 解鎖(將當前會話所有的表進行解鎖)
UNLOCK TABLES;

----------------------------

/*
打開【第一個 MySQL 客戶端工具】,先運行下面前 3 條 SQL,別運行解鎖語句。
*/

-- 為 employee 表加入讀鎖
LOCK TABLE employee READ;

-- 查詢 employee 表
-- 結果:查詢成功
SELECT * FROM employee;

-- 修改 employee 表的某條記錄
-- 結果:修改失敗,不阻塞,直接報錯
UPDATE employee SET money=20000 WHERE id=1;

-- 解鎖
UNLOCK TABLES;

----------------------------

/*
打開【第二個 MySQL 客戶端工具】,手動逐條選中以下 SQL 語句並運行。
*/

-- 查詢 employee 表
-- 結果:查詢成功
SELECT * FROM employee;

-- 修改 employee 表的任意一條記錄
-- 結果:修改失敗,會出現阻塞。
-- 只有當【第一個客戶端工具】運行 UNLOCK TABLES 之后,才能修改成功
UPDATE employee SET money=25000 WHERE id=2;
-- MyISAM 寫鎖,進行加鎖的語法格式如下:
LOCK TABLE 表名 WRITE;

/*
寫鎖的特點:一個連接加鎖后,只能該連接進行查詢和修改操作,其他連接【不能查詢】和【不能修改】
*/

-- 解鎖(將當前會話所有的表進行解鎖)
UNLOCK TABLES;

----------------------------

/*
打開【第一個 MySQL 客戶端工具】,先運行下面前 3 條 SQL,別運行解鎖語句。
*/

-- 為 employee 表添加寫鎖
LOCK TABLE employee WRITE;

-- 查詢 employee 表
-- 結果:查詢成功。當前連接可以查詢
SELECT * FROM employee;

-- 修改 employee 表某條記錄
-- 結果:修改成功。當前連接可以修改
UPDATE employee SET money=3999 WHERE id=2;

-- 解鎖
UNLOCK TABLES;

----------------------------

/*
打開【第二個 MySQL 客戶端工具】,手動逐條選中以下 SQL 語句並運行。
*/

-- 查詢 employee 表
-- 不能查詢。
-- 只有當【第一個客戶端工具】運行 UNLOCK TABLES 之后,才能查詢成功
SELECT * FROM employee;

-- 修改 employee 表某條記錄
-- 結果:不能修改。
-- 只有當【第一個客戶端工具】運行 UNLOCK TABLES 之后,才能修改成功
UPDATE employee SET money=2999 WHERE id=3;

(4)悲觀鎖和樂觀鎖

悲觀鎖的特點:

  • 顧名思義就是很悲觀,它對於數據被外界修改的操作持保守態度,認為數據隨時會修改。
  • 整個數據處理中需要將數據加鎖。悲觀鎖一般都是依靠關系型數據庫提供的鎖機制。
  • 上面介紹的行鎖、表鎖、讀鎖、寫鎖等等所有的鎖都是悲觀鎖。

樂觀鎖的特點:

  • 顧名思義就是很樂觀,每次操作數據的時候認為沒有人會來修改它,所以不去加鎖。
  • 但是在更新的時候會去判斷在此期間數據有沒有被修改。
  • 需要用戶自己去實現,不會發生並發搶占資源,只有在提交操作的時候檢查驗證數據時效性。

悲觀鎖和樂觀鎖的使用場景:

  • 對於讀的操作遠多於寫的操作的時候,一個更新操作加鎖會阻塞所有的讀取操作,降低了並發處理性能,最后還得解鎖,具有一定的開銷。這時候可以選擇樂觀鎖。
  • 如果是讀寫操作的比例差距不是非常大,或者系統沒有出現響應不及時、吞吐量瓶頸的問題,那就不要去使用樂觀鎖。因為樂觀鎖增加了實現的復雜度,也給業務帶來了額外的風險。這時候可以選擇悲觀鎖。

樂觀鎖的實現方式:

  • 通過增加【版本號】來實現

    • 給數據表中添加一個 version 列,每次更新后都將這個列的值加 1。
    • 讀取數據時,將版本號讀取出來,在執行更新的時候,比較版本號。
    • 如果相同則執行更新,如果不相同,說明此條數據已經發生了變化。
    • 用戶自行根據這個特點,自行來決定怎么處理,比如重新開始一遍,或者放棄本次更新。
  • 通過增加【標識符】來實現

    • 和版本號方式基本一樣,給數據表中添加一個列 dataflag,數據類型可以是 varchar(50)。
    • 每次更新后都將最新時間更新到此列,或者隨機生成一個 GUID 或者 UUID 更新到此列。
    • 讀取數據時,將 dataflag 讀取出來,在執行更新的時候,比較 dataflag 的值。
    • 如果相同則執行更新,如果不相同,說明此條數據已經發生了變化。
    • 用戶自行根據這個特點,自行來決定怎么處理,比如重新開始一遍,或者放棄本次更新。
-- 獲取數據的 version 值,假如獲取出來的值是 100
SELECT version FROM employee WHERE id=1;

-- 同時使用 id 和 version 作為 where 條件,修改該數據的值 
UPDATE employee SET name='弼馬溫',version=version+1 WHERE id=1 AND version=100;

----------------------------

-- 獲取數據的 dataflag 值,假如獲取出來的值是 bedcb0aa-85ba-11ec-b014-902e16a6f8db
SELECT dataflag FROM employee WHERE id=2;

-- 同時使用 id 和 dataflag 作為 where 條件,修改該數據的值 
UPDATE employee SET name='齊天大聖',version=uuid()
WHERE id=2 AND version='bedcb0aa-85ba-11ec-b014-902e16a6f8db';

到此為止,MySQL 的索引和鎖相關的知識點,基本上總結完畢,希望對大家有所幫助。



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM