前言:
關於什麼是鎖,爲什麼需要鎖等概念這裏不做解釋了,可以看下我的另一諞關於jdk中併發的欄目,其中介紹了併發的各種只是框架,包括鎖以及jdk的鎖實現等等。併發編程、鎖等知識框架
一、mysql中的鎖
本章節講述表鎖和行鎖,gap鎖在本文後邊事務中會講到
因爲MyISAM支持表鎖,InnoDb支持表、行鎖。所以按照引擎來講述下這兩個鎖
1、表鎖
特性
開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖衝突的概率最高,併發 度最低。
適用場景
表級鎖更適合於以查詢爲主,只有少量按索引條件更新數據的應用,如 OLAP 系統
2、行鎖
特性
開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖衝突的概率最低,併發 度也最高
適用場景
行級鎖則更適合於有大量按索引條件併發更新少量不同數據,同時又有併發查詢的應用,如 一些在線事務處理(OLTP)系統。
3、MyISAM鎖
MySQL 的表級鎖有兩種模式: 表共享讀鎖(Table Read Lock) 表獨佔寫鎖(Table Write Lock)
3.1共享讀鎖
# session1 鎖testmysam表
lock table testmysam READ
# session2
select * from testmysam # 可以查詢
#session1
insert into testmysam value(2); # 報錯
update testmysam set id=2 where id=1; # 報錯
# session2
insert into testmysam value(2); # 等待
#session1
insert into account value(4,'aa',123); #報錯
select * from account ; #報錯
# session2 中
insert into account value(4,'aa',123); #成功
# session1
select s.* from lock table 表名 as 別名 read;
總結
- 對錶1加讀鎖之後,只能對1進行讀操作,不能寫表1,也不能操作其他表
- 對MyISAM表的讀操作,不會阻塞其他用戶對同一表的讀請求,但會阻塞對同一表的寫請求
3.2獨佔寫鎖
# session1 對testmysam加寫鎖
lock table testmysam WRITE
# session1對testmysam增刪改查都可以
insert testmysam value(3);
delete from testmysam where id = 3
select * from testmysam
# session1對其他表操作報錯
select s.* from testmysam s
insert into account value(4,'aa',123);
#session 2 會等待,因爲寫鎖會阻塞讀操作
select * from testmysam
總結:
- 寫鎖,對 MyISAM 表的寫操作,則會阻塞其他用戶對同一表的讀和寫操作
- 對 MyISAM 表的寫操作,當前 session 可以對本表做 CRUD,但對其他表進行操作會報錯
4、InnoDB鎖
共享鎖又稱:讀鎖:當一個事務對某幾行上讀鎖時,允許其他事務對這幾行進行讀操作,但 不允許其進行寫操作,也不允許其他事務給這幾行上排它鎖,但允許上讀鎖。
排它鎖又稱:寫鎖:當一個事務對某幾個上寫鎖時,不允許其他事務寫,但允許讀。更不允 許其他事務給這幾行上任何鎖。包括寫鎖。
注:
- 兩個事務不能鎖同一個索引。
- insert ,delete , update 在事務中都會自動默認加上排它鎖。
- 行鎖必須有索引才能實現,否則會自動鎖全表,那麼就不是行鎖了。
# 建表
CREATE TABLE testdemo (
`id` int(255) NOT NULL ,
`c1` varchar(300) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
`c2` int(50) NULL DEFAULT NULL ,
PRIMARY KEY (`id`),
INDEX `idx_c2` (`c2`) USING BTREE )
ENGINE=InnoDB;
# 插入數據
insert into testdemo VALUES(1,'1',1),(2,'2',2);
# 開啓事務1
BEGIN
select * from testdemo where id =1 for update
# 另一個session
update testdemo set c1 = '1' where id = 2 #成功
update testdemo set c1 = '1' where id = 1 #等待
BEGIN
update testdemo set c1 = '1' where id = 1
# 在另外一個 session 中
update testdemo set c1 = '1' where id = 1 等待
BEGIN
update testdemo set c1 = '1' where c1 = '1'
# 在另外一個 session 中
update testdemo set c1 = '2' where c1 = '2' #等待,因爲c1行沒有索引,直接會進行鎖表操作
# 第一個 session 中
select * from testdemo where id =1 for update
# 第二個 session
select * from testdemo where id =1 lock in share mode #阻塞,寫鎖會阻塞讀鎖
5、鎖的等待問題
工作中可能會遇到這種問題。
#程序員甲,正直調試代碼
BEGIN
SELECT * FROM testdemo WHERE id = 1 FOR UPDATE
# 你正直完成的功能也要經過那部分的代碼,你得上個讀鎖,然後你就只能等待他釋放鎖。
BEGIN
SELECT * FROM testdemo WHERE id = 1 lock in share mode
解決方案
# 查看mysql鎖詳情
select * from information_schema.INNODB_LOCKS;
# 查看mysql正在等待鎖的query
select * from sys.innodb_lock_waits
我現在執行的這個 sql 語句有了,另外看下最下面,kill 命令
,你在工作中完全可以通過 kill 吧阻塞了的 sql 語句給幹掉,你就可以繼續運行了,不過這命令也要注意使用過,如果某同 事正在做比較重要的調試,你 kill,被他發現可能會被暴打一頓。
二、事務
1、事務的概念
現在的很多軟件都是多用戶,多程序,多線程的,對同一個表可能同時有很多人在用,爲保 持數據的一致性,所以提出了事務的概念。
舉例:A 給 B 要劃錢,A 的賬戶-1000 元, B 的賬戶就要+1000 元,這兩個 update 語句必須作爲 一個整體來執行,不然 A 扣錢了,B 沒有加錢這種情況很難處理。
2、事務的特性
事務應該具有 4 個屬性:原子性、一致性、隔離性、持久性。這四個屬性通常稱爲 ACID 特性。
- 原子性(atomicity)
一個事務必須被視爲一個不可分割的最小單元,整個事務中的所有操作要麼全部提交成功,要麼全部失敗,對於一個事務來說,不可能只執行其中的一部分操作 - 一致性(consistency)
一致性是指事務將數據庫從一種一致性轉換到另外一種一致性狀態,在事務開始之前和事務結束之後數據庫中數據的完整性沒有被破壞 - 隔離性(isolation)
一個事務的執行不能被其他事務干擾。即一個事務內部的操作及使用的數據對併發的其他事 務是隔離的,併發執行的各個事務之間不能互相干擾。 (對數據庫的並行執行,應該像串行執行一樣) - 持久性(durability)
一旦事務提交,則其所做的修改就會永久保存到數據庫中。此時即使系統崩潰,已經提交的修改數據也不會丟失
3、事務併發存在的問題
3.1 髒讀
事務A讀取了事務B更新的數據,然後B回滾操作,那麼A讀取到的數據是髒數據
3.2不可重複度
事務 A 多次讀取同一數據,事務 B 在事務 A 多次讀取的過程中,對數據作了更新並提交,導致事務 A 多次讀取同一數據時,結果 不一致。
3.3幻讀
和不可重複讀唯一的卻別就是通過插入或者刪除數據導致的,比如事務A插入主鍵id=1的數據,未提交。事務B去插入id=1的數據,報錯:主鍵衝突。但是事務B查詢的時候卻查詢不到id=1的數據,事務A一臉懵逼。幻讀無疑發生了
4、解決事務併發問題-事務隔離級別
4.1未提交讀(READ UNCOMMITED)
未提交讀我們可以看到3.1、3.2、3.3三個問題都是存在的。因爲事務幾乎沒有起到作用。
4.2已提交讀 (READ COMMITED)
4.2.1解決了髒讀問題
# 查看事務的隔離級別
show variables like '%tx_isolation%';
# 設置事務隔離級別
set SESSION TRANSACTION ISOLATION LEVEL read committed;
# session1 中
start TRANSACTION
update account set balance = balance -50 where id = 1
# session2 中查詢 (數據並沒改變)
select * from account
# 回到第一個 session2 中
commit
# session2中數據已經改變
select * from account # (數據已經改變)
4.2.2存在不可重複度問題
# 設置隔離級別
set SESSION TRANSACTION ISOLATION LEVEL read committed;
# 事務A
start TRANSACTION
update account set balance = balance -50 where id = 1
# 事務B,查詢數據未變化,因爲A還未提交
select * from account
# 事務 A
commit
# 事務B中,發現事務已經改變,事務B兩次讀取的數據不一致,則存在不可重複讀問題
select * from account
4.3可重複讀(REPEATABLE READ)
4.3.1解決了不可重複讀
例子和4.2.2中一樣,只不過修改了隔離級別爲可重複讀
set SESSION TRANSACTION ISOLATION LEVEL repeatable read;
發現事務B兩次讀取到的數據是一致的,即解決了不可重複讀問題。
但是,B讀取不到A已經提交的數據了,這難道不會存在問題嗎?
- 假設數據本來100
- A事務減去50變爲50
- B事務讀取是100
- A事務commit
- B事務讀取還是100,再進行減50
- B事務commit
那數據庫中數據豈不是是50 ,但是我們明明是減了兩次。這就違反了數據庫完整性約束了呀。別慌,我們去數據庫查看數據的時候,發現數據是0,並沒有違反數據庫完整性約束。爲什麼呢
因爲可重複讀的隔離級別下使用了MVCC機制,select操作不會更新版本號,是快照讀(歷史版本);insert、update和delete會更新版本號,是當前讀(當前版本)。
4.3.2存在幻讀問題
# 事務A
begin
insert into account (id) values(1)
# 事務B
begin
insert into account (id) values(1) # 報錯,主鍵衝突
select * from account where id=1; # 查詢不到數據,一臉懵逼,你告訴我主鍵衝突,但是我卻查詢不到數據,幻讀發生了
mysql在可重複度的隔離級別下其實已經解決的幻讀的問題,使用的是間隙鎖(gap鎖)
# 事務A
begin
# 插入數據並且對ID=1進行加鎖(對索引加鎖)
insert into account (id) values(1) for update
# 事務B
begin
# 發現id=1已經被鎖住了,所以會阻塞等待
insert into account (id) values(1) # 報錯,主鍵衝突
# 等到事務Acommit後就會報主鍵衝突,這時候再去查找id=1的數據就可以查找到了
4.4可串行化(SERIALIZABLE)
相當於鎖表,性能太差,所以一般不建議使用
show variables like '%tx_isolation%';
set SESSION TRANSACTION ISOLATION LEVEL serializable;
# 事務A
begin
select * from account
# 事務B
begin
select * from account
# 發現根本就不讓插入,因爲事務A對整張表進行了加鎖,我們必須等待事務Acommit釋放鎖之後才能進行寫操作
insert into account VALUES(4,'deer',500)
gap鎖小甜點
select @@tx_isolation;
create table t_lock_1 (a int primary key);
insert into t_lock_1 values(10),(11),(13),(20),(40);
# 事務A
begin
select * from t_lock_1 where a <= 13 for update;
# 事務B
insert into t_lock_1 values(21)
insert into t_lock_1 values(19) #阻塞
# 因爲在 rr 隔離級別中者個會掃描到當前值(13)的下一個值(20),並把這些數據全部加鎖,所以20之前的額數據都被加鎖了
create table t_lock_2 (
a int primary key,
b int, key (b));
insert into t_lock_2 values(1,1),(3,1),(5,3),(8,6),(10,8);
# 事務A
BEGIN
select * from t_lock_2 where b=3 for update;
# 事務B
// 不可執行,因爲 a=5 上有一把記錄鎖
select * from t_lock_2 where a = 5 lock in share mode;
// 不可以執行,因爲 b=2 在(1, 3]內
insert into t_lock_2 values(4, 2);
// 不可以執行,因爲 b=5 在(3, 6)內
insert into t_lock_2 values(6, 5);
// 可以執行,(2, 0)均不在鎖住的範圍內
insert into t_lock_2 values(2, 0);
// 可以執行,(6, 7)均不在鎖住的範圍內
insert into t_lock_2 values(6, 7);
二級索引鎖住的範圍是 (1, 3],(3, 6) 主鍵索引只鎖住了 a=5 的這條記錄 [5]