1.1 鎖概述
鎖是計算機協調多個進程或線程并發訪問某一資源的機制(避免爭搶)。
在數據庫中,除傳統的計算資源(如 CPU、RAM、I/O 等)的爭用以外,數據也是一種供許多用戶共享的資源。如何保證數據并發訪問的一致性、有效性是所有數據庫必須解決的一個問題,鎖沖突也是影響數據庫并發訪問性能的一個重要因素。從這個角度來說,鎖對數據庫而言顯得尤其重要,也更加復雜。
1.2 鎖分類
從對數據操作的粒度分 :
1) 表鎖:操作時,會鎖定整個表。
2) 行鎖:操作時,會鎖定當前操作行。
從對數據操作的類型分:
1) 讀鎖(共享鎖):針對同一份數據,多個讀操作可以同時進行而不會互相影響。
2) 寫鎖(排它鎖):當前操作沒有完成之前,它會阻斷其他寫鎖和讀鎖。
1.3 MySQL 鎖
相對其他數據庫而言,MySQL的鎖機制比較簡單,其最顯著的特點是不同的存儲引擎支持不同的鎖機制。下表中羅列出了各存儲引擎對鎖的支持情況:
MySQL這3種鎖的特性可大致歸納如下 :
從上述特點可見,很難籠統地說哪種鎖更好,只能就具體應用的特點來說哪種鎖更合適!僅從鎖的角度來說:表級鎖更適合于以查詢為主,只有少量按索引條件更新數據的應用,如Web 應用;而行級鎖則更適合于有大量按索引條件并發更新少量不同數據,同時又有并查詢的應用,如一些在線事務處理(OLTP)系統。
1.2 MyISAM 表鎖
MyISAM 存儲引擎只支持表鎖,這也是MySQL開始幾個版本中唯一支持的鎖類型。
1.2.1 如何加表鎖
MyISAM 在執行查詢語句(SELECT)前,會自動給涉及的所有表加讀鎖,在執行更新操作(UPDATE、DELETE、INSERT 等)前,會自動給涉及的表加寫鎖,這個過程并不需要用戶干預,因此,用戶一般不需要直接用 LOCK TABLE 命令給 MyISAM 表顯式加鎖。
顯示加表鎖語法:
create database demo_03 default charset=utf8mb4;
?
use demo_03;
?
CREATE TABLE `tb_book` (
`id` INT(11) auto_increment,
`name` VARCHAR(50) DEFAULT NULL,
`publish_time` DATE DEFAULT NULL,
`status` CHAR(1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=myisam DEFAULT CHARSET=utf8 ;
?
INSERT INTO tb_book (id, name, publish_time, status) VALUES(NULL,'JAVA編程思想','2088-08-01','1');
INSERT INTO tb_book (id, name, publish_time, status) VALUES(NULL,'solr編程思想','2088-08-08','0');
?
?
?
CREATE TABLE `tb_user` (
`id` INT(11) auto_increment,
`name` VARCHAR(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=myisam DEFAULT CHARSET=utf8 ;
?
INSERT INTO tb_user (id, name) VALUES(NULL,'令狐沖');
INSERT INTO tb_user (id, name) VALUES(NULL,'田伯光');
?
1.2.2 讀鎖案例
準備環境
create database demo_03 default charset=utf8mb4;
?
use demo_03;
?
CREATE TABLE `tb_book` (
`id` INT(11) auto_increment,
`name` VARCHAR(50) DEFAULT NULL,
`publish_time` DATE DEFAULT NULL,
`status` CHAR(1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=myisam DEFAULT CHARSET=utf8 ;
?
INSERT INTO tb_book (id, name, publish_time, status) VALUES(NULL,'java編程思想','2088-08-01','1');
INSERT INTO tb_book (id, name, publish_time, status) VALUES(NULL,'solr編程思想','2088-08-08','0');
?
CREATE TABLE `tb_user` (
`id` INT(11) auto_increment,
`name` VARCHAR(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=myisam DEFAULT CHARSET=utf8 ;
?
INSERT INTO tb_user (id, name) VALUES(NULL,'令狐沖');
INSERT INTO tb_user (id, name) VALUES(NULL,'田伯光');
?
客戶端 一 :
1)獲得tb_book 表的讀鎖
insert into tb_book values(null,'Mysql高級','2088-01-01','1');
2) 執行查詢操作
insert into tb_book values(null,'Mysql高級','2088-01-01','1');
可以正常執行 , 查詢出數據。
客戶端 二 :
3) 執行查詢操作
insert into tb_book values(null,'Mysql高級','2088-01-01','1');
客戶端 一 :
4)查詢未鎖定的表
insert into tb_book values(null,'Mysql高級','2088-01-01','1');
客戶端 二 :
5)查詢未鎖定的表
insert into tb_book values(null,'Mysql高級','2088-01-01','1');
可以正常查詢出未鎖定的表;
客戶端 一 :
6) 執行插入操作
insert into tb_book values(null,'Mysql高級','2088-01-01','1');
執行插入, 直接報錯 , 由于當前tb_book 獲得的是 讀鎖, 不能執行更新操作。
客戶端 二 :
7) 執行插入操作
insert into tb_book values(null,'Mysql高級','2088-01-01','1');
當在客戶端一中釋放鎖指令 unlock tables 后 , 客戶端二中的 inesrt 語句 , 立即執行 ;
1.2.3 寫鎖案例
客戶端 一 :
1)獲得tb_book 表的寫鎖
lock table tb_book write ;
2)執行查詢操作
select * from tb_book ;
查詢操作執行成功;
3)執行更新操作
update tb_book set name = 'java編程思想(第二版)' where id = 1;
更新操作執行成功 ;
客戶端 二 :
4)執行查詢操作
select * from tb_book ;
當在客戶端一中釋放鎖指令 unlock tables 后 , 客戶端二中的 select 語句 , 立即執行 ;
1.2.4 結論
鎖模式的相互兼容性如表中所示:
由上表可見:
1) 對MyISAM 表的讀操作,不會阻塞其他用戶對同一表的讀請求,但會阻塞對同一表的寫請求;
2) 對MyISAM 表的寫操作,則會阻塞其他用戶對同一表的讀和寫操作;
簡而言之,就是讀鎖會阻塞寫,但是不會阻塞讀。而寫鎖,則既會阻塞讀,又會阻塞寫。
此外,MyISAM 的讀寫鎖調度是寫優先,這也是MyISAM不適合做寫為主的表的存儲引擎的原因。因為寫鎖后,其他線程不能做任何操作,大量的更新會使查詢很難得到鎖,從而造成永遠阻塞。
1.2.5 查看鎖的爭用情況
show open tables;
In_user : 表當前被查詢使用的次數。如果該數為零,則表是打開的,但是當前沒有被使用。
Name_locked:表名稱是否被鎖定。名稱鎖定用于取消表或對表進行重命名等操作。
show status like 'Table_locks%';
Table_locks_immediate : 指的是能夠立即獲得表級鎖的次數,每立即獲取鎖,值加1。
Table_locks_waited : 指的是不能立即獲取表級鎖而需要等待的次數,每等待一次,該值加1,此值高說明存在著較為嚴重的表級鎖爭用情況。
1.3 InnoDB 行鎖
1.3.1 行鎖介紹
行鎖特點 :偏向InnoDB 存儲引擎,開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖沖突的概率最低,并發度也最高。
InnoDB 與 MyISAM 的最大不同有兩點:一是支持事務;二是 采用了行級鎖。
1.3.2 背景知識
事務、ACID屬性、事務隔離級別,詳情請看:事務特性及隔離級別詳述
1.3.3 InnoDB 的行鎖模式
InnoDB 實現了以下兩種類型的行鎖。
- 共享鎖(S):又稱為讀鎖,簡稱S鎖,共享鎖就是多個事務對于同一數據可以共享一把鎖,都能訪問到數據,但是只能讀不能修改。
- 排他鎖(X):又稱為寫鎖,簡稱X鎖,排他鎖就是不能與其他鎖并存,如一個事務獲取了一個數據行的排他鎖,其他事務就不能再獲取該行的其他鎖,包括共享鎖和排他鎖,但是獲取排他鎖的事務是可以對數據就行讀取和修改。
對于UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及數據集加排他鎖(X);
對于普通SELECT語句,InnoDB不會加任何鎖;
可以通過以下語句顯示給記錄集加共享鎖或排他鎖 。
共享鎖(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
?
排他鎖(X) :SELECT * FROM table_name WHERE ... FOR UPDATE
1.3.4 案例準備工作
create table test_innodb_lock(
id int(11),
name varchar(16),
sex varchar(1)
)engine = innodb default charset=utf8;
?
insert into test_innodb_lock values(1,'100','1');
insert into test_innodb_lock values(3,'3','1');
insert into test_innodb_lock values(4,'400','0');
insert into test_innodb_lock values(5,'500','1');
insert into test_innodb_lock values(6,'600','0');
insert into test_innodb_lock values(7,'700','0');
insert into test_innodb_lock values(8,'800','1');
insert into test_innodb_lock values(9,'900','1');
insert into test_innodb_lock values(1,'200','0');
?
create index idx_test_innodb_lock_id on test_innodb_lock(id);
create index idx_test_innodb_lock_name on test_innodb_lock(name);
1.3.5 行鎖基本演示
1.3.6 無索引行鎖升級為表鎖
如果不通過索引條件檢索數據,那么InnoDB將對表中的所有記錄加鎖,實際效果跟表鎖一樣。
查看當前表的索引 : show index from test_innodb_lock ;
由于 執行更新時 , name字段本來為varchar類型, 我們是作為數組類型使用,存在類型轉換,索引失效,最終行鎖變為表鎖 ;
1.3.7 間隙鎖危害
當我們用范圍條件,而不是使用相等條件檢索數據,并請求共享或排他鎖時,InnoDB會給符合條件的已有數據進行加鎖; 對于鍵值在條件范圍內但并不存在的記錄,叫做 "間隙(GAP)" , InnoDB也會對這個 "間隙" 加鎖,這種鎖機制就是所謂的 間隙鎖(Next-Key鎖) 。
示例 :
1.3.8 InnoDB 行鎖爭用情況
show status like 'innodb_row_lock%';
Innodb_row_lock_current_waits: 當前正在等待鎖定的數量
?
Innodb_row_lock_time: 從系統啟動到現在鎖定總時間長度
?
Innodb_row_lock_time_avg:每次等待所花平均時長
?
Innodb_row_lock_time_max:從系統啟動到現在等待最長的一次所花的時間
?
Innodb_row_lock_waits: 系統啟動后到現在總共等待的次數
?
當等待的次數很高,而且每次等待的時長也不小的時候,我們就需要分析系統中為什么會有如此多的等待,然后根據分析結果著手制定優化計劃。
1.3.9 總結
InnoDB存儲引擎由于實現了行級鎖定,雖然在鎖定機制的實現方面帶來了性能損耗可能比表鎖會更高一些,但是在整體并發處理能力方面要遠遠由于MyISAM的表鎖的。當系統并發量較高的時候,InnoDB的整體性能和MyISAM相比就會有比較明顯的優勢。
但是,InnoDB的行級鎖同樣也有其脆弱的一面,當我們使用不當的時候,可能會讓InnoDB的整體性能表現不僅不能比MyISAM高,甚至可能會更差。
優化建議:
- 盡可能讓所有數據檢索都能通過索引來完成,避免無索引行鎖升級為表鎖。
- 合理設計索引,盡量縮小鎖的范圍
- 盡可能減少索引條件,及索引范圍,避免間隙鎖
- 盡量控制事務大小,減少鎖定資源量和時間長度
- 盡可使用低級別事務隔離(但是需要業務層面滿足需求)






