MySQL 原理篇html
MySQL 索引機制mysql
數據準備:
/* SQLyog Ultimate v12.09 (64 bit) MySQL - 5.6.17 : Database - test ********************************************************************* */ /*!40101 SET NAMES utf8 */; /*!40101 SET SQL_MODE=''*/; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; CREATE DATABASE /*!32312 IF NOT EXISTS*/`test` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `test`; /*Table structure for table `t2` */ DROP TABLE IF EXISTS `t2`; CREATE TABLE `t2` ( `id` int(11) NOT NULL, `name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*Data for the table `t2` */ insert into `t2`(`id`,`name`) values (1,'1'),(4,'4'),(7,'7'),(10,'10'); /*Table structure for table `teacher` */ DROP TABLE IF EXISTS `teacher`; CREATE TABLE `teacher` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(32) NOT NULL, `age` int(11) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4; /*Data for the table `teacher` */ insert into `teacher`(`id`,`name`,`age`) values (1,'seven11124',18),(2,'qingshan',18); /*Table structure for table `user_account` */ DROP TABLE IF EXISTS `user_account`; CREATE TABLE `user_account` ( `id` int(11) NOT NULL DEFAULT '0', `balance` int(11) NOT NULL, `lastUpdate` datetime NOT NULL, `userID` int(11) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; /*Data for the table `user_account` */ insert into `user_account`(`id`,`balance`,`lastUpdate`,`userID`) values (1,3200,'2018-12-06 13:27:57',1),(2,50,'2018-12-06 13:28:08',2),(3,1000,'2018-12-06 13:28:22',3); /*Table structure for table `users` */ DROP TABLE IF EXISTS `users`; CREATE TABLE `users` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(32) NOT NULL, `age` int(11) NOT NULL, `phoneNum` varchar(32) NOT NULL, `lastUpdate` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `idx_eq_name` (`name`) ) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4; /*Data for the table `users` */ insert into `users`(`id`,`name`,`age`,`phoneNum`,`lastUpdate`) values (1,'seven',26,'13666666666','2018-12-07 19:22:51'),(2,'qingshan',19,'13777777777','2018-12-08 21:01:12'),(3,'james',20,'13888888888','2018-12-08 20:59:39'),(4,'tom',99,'13444444444','2018-12-06 20:34:10'),(6,'jack',91,'13444444544','2018-12-06 20:35:07'),(11,'jack1',33,'13441444544','2018-12-06 20:36:19'),(15,'tom2',30,'1344444444','2018-12-08 15:08:24'),(19,'iiii',30,'1344444444','2018-12-08 21:21:47'); /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
在運行下面的演示案例以前,先把表和數據準備好。
鎖是用於管理不一樣事務對共享資源的併發訪問。
表鎖與行鎖的區別:
InnoDB 存儲引擎支持行鎖和表鎖(另類的行鎖),InnoDB 的表鎖是經過對全部行加行鎖實現的。
行鎖的算法
官網文檔:https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html
共享鎖:又稱爲讀鎖,簡稱 S 鎖,顧名思義,共享鎖就是多個事務對於同一數據能夠共享一把鎖,都能訪問到數據,可是隻能讀不能修改。
經過以下代碼,加鎖和釋放鎖:
-- 加鎖 select * from users WHERE id=1 LOCK IN SHARE MODE; -- 釋放鎖:提交事務 or 回滾事務 commit; rollback;
-- 共享鎖 -- 事務A執行 BEGIN; SELECT * FROM users WHERE id=1 LOCK IN SHARE MODE; ROLLBACK; COMMIT; -- 事務B執行 SELECT * FROM users WHERE id=1; UPDATE users SET age=19 WHERE id=1;
結論:UPDATE 語句被鎖住了,不能執行。在事務A得到共享鎖的狀況下,事務B能夠執行查詢操做,可是不能執行更新操做。
排它鎖:又稱爲寫鎖,簡稱 X 鎖,排他鎖不能與其餘鎖並存,如一個事務獲取了一個數據行的排他鎖,其餘事務就不能再獲取該行的鎖(共享鎖、排他鎖),只有該獲取了排他鎖的事務是能夠對數據行進行讀取和修改。(其餘事務要讀取數據可來自於快照)
經過以下代碼,加鎖和釋放鎖:
-- 加鎖 -- delete / update / insert 默認加上X鎖 -- SELECT * FROM table_name WHERE ... FOR UPDATE -- 釋放鎖:提交事務 or 回滾事務 commit; rollback;
-- 排它鎖 -- 事務A執行 BEGIN; UPDATE users SET age=23 WHERE id=1; COMMIT; ROLLBACK; -- 事務B執行 SELECT * FROM users WHERE id=1 LOCK IN SHARE MODE; SELECT * FROM users WHERE id=1 FOR UPDATE; -- SELECT 能夠執行,數據來自於快照 SELECT * FROM users WHERE id=1;
結論:事務B的第一條 SQL 和第二條 SQL 語句都不能執行,都已經被鎖住了,第三條 SQL 能夠執行,數據來自於快照,關於這點後面會講到。
InnoDB 的行鎖是經過給索引上的索引項加鎖來實現的。
只有經過索引條件進行數據檢索,InnoDB 才使用行級鎖,不然,InnoDB 將使用表鎖(鎖住索引的全部記錄)
經過普通索引進行數據檢索,好比經過下面例子中 UPDATE users SET lastUpdate=NOW() WHERE `name`='seven';
該 SQL 會在 name 字段的惟一索引上面加一把行鎖,同時會在該惟一索引對應的主鍵索引上面也會加上一把行鎖,總共會加兩把行鎖。
演示以前,先看一下 users 表的結構和數據內容。
-- 案例1 -- 事務A執行 BEGIN; UPDATE users SET lastUpdate=NOW() WHERE phoneNum='13666666666'; ROLLBACK; -- 事務B執行 UPDATE users SET lastUpdate=NOW() WHERE id=2; UPDATE users SET lastUpdate=NOW() WHERE id=1; -- 案例2 -- 事務A執行 BEGIN; UPDATE users SET lastUpdate=NOW() WHERE id=1; ROLLBACK; -- 事務B執行 UPDATE users SET lastUpdate=NOW() WHERE id=2; UPDATE users SET lastUpdate=NOW() WHERE id=1; -- 案例3 -- 事務A執行 BEGIN; UPDATE users SET lastUpdate=NOW() WHERE `name`='seven'; ROLLBACK; -- 事務B執行 UPDATE users SET lastUpdate=NOW() WHERE `name`='seven'; UPDATE users SET lastUpdate=NOW() WHERE id=1; UPDATE users SET lastUpdate=NOW() WHERE `name`='qingshan'; UPDATE users SET lastUpdate=NOW() WHERE id=2;
注意:這裏演示的案例都是在事務A沒有提交以前,執行事務B的語句。
案例1執行結果以下圖所示:
案例2執行結果以下圖所示:
案例3執行結果以下圖所示:
表示事務準備給數據行加入共享鎖,即一個數據行加共享鎖前必須先取得該表的 IS 鎖,意向共享鎖之間是能夠相互兼容的。
表示事務準備給數據行加入排他鎖,即一個數據行加排他鎖前必須先取得該表的 IX 鎖,意向排它鎖之間是能夠相互兼容的
意向鎖(IS 、IX)是 InnoDB 數據操做以前自動加的,不須要用戶干預。
意義:當事務想去進行鎖表時,能夠先判斷意向鎖是否存在,存在時則可快速返回該表不能啓用表鎖。
-- IS鎖的意義 -- 事務A執行 BEGIN; UPDATE users SET lastUpdate=NOW() WHERE id=1; ROLLBACK; -- 事務B執行 -- 由於沒有經過索引條件進行數據檢索,因此這裏加的是表鎖 UPDATE users SET lastUpdate=NOW() WHERE phoneNum='13777777777';
結論:事務B的 SQL 由於沒有經過索引條件進行數據檢索,因此這裏加的是表鎖,在對錶加鎖以前會查看該表是否已經存在了意向鎖,由於事務A已經得到了該表的意向鎖了,因此事務B不須要判斷每一行數據是否已經加鎖,能夠快速經過意向鎖阻塞當前 SQL 的更新操做。
針對自增列自增加的一個特殊的表級別鎖。
經過以下命令查看自增鎖的默認等級:
SHOW VARIABLES LIKE 'innodb_autoinc_lock_mode';
默認取值1,表明連續,事務未提交 ID 永久丟失。
-- 事務A執行 BEGIN; INSERT INTO users(NAME , age ,phoneNum ,lastUpdate ) VALUES ('tom2',30,'1344444444',NOW()); ROLLBACK; BEGIN; INSERT INTO users(NAME , age ,phoneNum ,lastUpdate ) VALUES ('xxx',30,'13444444444',NOW()); ROLLBACK; -- 事務B執行 INSERT INTO users(NAME , age ,phoneNum ,lastUpdate ) VALUES ('yyy',30,'13444444444',NOW());
事務A執行完後,在執行事務B的語句,發現插入的 ID 數據再也不連續,由於事務A獲取的 ID 數據在 ROLLBACK 以後被丟棄了。
當 SQL 執行按照索引進行數據的檢索時,查詢條件爲範圍查找(between and、<、>等)並有數據命中,則此時 SQL 語句加上的鎖爲 Next-key locks,鎖住索引的記錄 + 區間(左開右閉)。
演示以前,先看一下 t2 表的結構和數據內容。
臨鍵鎖(Next-key Locks):InnoDB 默認的行鎖算法。
t2 表中的數據行有4條數據:1,4,7,10,InnoDB 引擎會將表中的數據劃分爲:(-∞, 1] (1, 4] (4, 7] (7, 10] (10, +∞),執行以下 SQL 語句:
-- 臨鍵鎖 -- 事務A執行 BEGIN; SELECT * FROM t2 WHERE id>5 AND id<9 FOR UPDATE; ROLLBACK -- 事務B執行 BEGIN; SELECT * FROM t2 WHERE id=4 FOR UPDATE; -- 能夠執行 SELECT * FROM t2 WHERE id=7 FOR UPDATE; -- 鎖住 SELECT * FROM t2 WHERE id=10 FOR UPDATE; -- 鎖住 INSERT INTO `t2` (`id`, `name`) VALUES (9, '9'); -- 鎖住
SELECT * FROM t2 WHERE id>5 AND id<9 FOR UPDATE;
這條查詢語句命中了7這條數據,它會鎖住 (4, 7] 這個區間,同時還會鎖住下一個區間 (7, 10]。
防止幻讀。當咱們把下一個區間也鎖住的時候,這個時候咱們要新增數據,就會被鎖住,這樣就能夠防止幻讀。
當 SQL 執行按照索引進行數據的檢索時,查詢條件的數據不存在,這時 SQL 語句加上的鎖即爲 Gap locks,鎖住數據不存在的區間(左開右開)
Gap 只在 RR 事務隔離級別存在。由於幻讀問題是在 RR 事務經過臨鍵鎖和 MVCC 解決的,而臨鍵鎖=間隙鎖+記錄鎖,因此間隙鎖只在 RR 事務隔離級別存在。
-- 間隙鎖 -- 事務A執行 BEGIN; SELECT * FROM t2 WHERE id>4 AND id <6 FOR UPDATE; -- 或者 SELECT * FROM t2 WHERE id=6 FOR UPDATE; ROLLBACK; -- 事務B執行 INSERT INTO `t2` (`id`, `name`) VALUES (5, '5'); INSERT INTO `t2` (`id`, `name`) VALUES (6, '6');
SELECT * FROM t2 WHERE id>4 AND id <6 FOR UPDATE;
這條查詢語句不能命中數據,它會鎖住 (4, 7] 這個區間。
當 SQL 執行按照惟一性(Primary key、Unique key)索引進行數據的檢索時,查詢條件等值匹配且查詢的數據是存在,這時 SQL 語句加上的鎖即爲記錄鎖 Record Locks,鎖住具體的索引項。
-- 記錄鎖 -- 事務A執行 BEGIN; SELECT * FROM t2 WHERE id=4 FOR UPDATE; ROLLBACK; -- 事務B執行 SELECT * FROM t2 WHERE id=7 FOR UPDATE; SELECT * FROM t2 WHERE id=4 FOR UPDATE;
事務A執行 SELECT * FROM t2 WHERE id=4 FOR UPDATE;
把 id=4 的數據行鎖住。
當 SQL 執行按照普通索引進行數據的檢索時,查詢條件等值匹配且查詢的數據是存在,這時 SQL 語句鎖住數據存在區間(左開右開)
InnoDB 真正處理事務併發帶來的問題不只僅是依賴鎖,還有其餘的機制,下篇文章會講到,因此這裏只是演示利用鎖是如何解決事務併發帶來的問題,並非 InnoDB 真實的處理方式。
在事務B的更新語句上面加上一把 X 鎖,這樣就能夠有效的解決髒讀問題。
在事務A的查詢語句上面加上一把 S 鎖,事務B的更新操做將會被阻塞,這樣就能夠有效的解決不可重複讀的問題。
在事務A的查詢語句上面加上一把 Next-key 鎖,經過臨鍵鎖的定義,能夠知道這個時候,事務A會把 (-∞,+∞) 的區間數據都鎖住,事務B的新增操做將會被阻塞,這樣就能夠有效的解決幻讀的問題。
-- 事務A執行 BEGIN; UPDATE users SET lastUpdate = NOW() WHERE id =1; UPDATE t2 SET `name`='test' WHERE id =1; ROLLBACK; -- 事務B執行 BEGIN; UPDATE t2 SET `name`='test' WHERE id =1; UPDATE users SET lastUpdate = NOW() WHERE id =1; ROLLBACK;
事務A和事務B按照上面的執行步驟,最後由於存在相互等待的狀況,因此 MySQL 判斷出現死鎖了。