今天總結一下 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 的索引和鎖相關的知識點,基本上總結完畢,希望對大家有所幫助。