MySql(三)——事务和锁
前言
前兩篇已經針對索引和常見的查詢執行步驟做了一個簡單的總結,這一篇博客開始總結鎖和事務,這個是MySQL最重要的部分。主要針對InnoDB存儲引擎進行詳細的實例操作。
事務
事務的概念
提到事務,都會想到經典的轉賬實例,這里依舊還是以轉賬實例來說明事務的ACID屬性,其實有些屬性有時候解釋的異常官方,理解起來似乎并沒有太深刻,這次盡量用通俗的語句來解釋ACID。
-- id為3的用戶,需要向用戶id為1的用戶轉賬1000update user_account set balance = balance - 1000 where userID = 3; update user_account set balance = balance +1000 where userID = 1;A(Atomicity)?原子性
這個還比較好理解,就是上述的兩個語句,要么一起提交,要么一起不提交。可以理解為只是保證上述兩個語句一起提交個mysql去執行;
C(Consistency)一致性
有時候這個會和原子性混淆。原子性只是保證語句會一起提交給mysql,并不保證數據最終的正確性。一致性就是保證的數據正確性。
I(Isolation)隔離性
這個是針對并發場景下的解釋,一個事務在數據提交之前,對其他事務的可見性設定(這個有點抽象,后續再解釋一下)。真是隔離性的不同,mysql才會有了各種鎖來保證數據的一致性。
D(Durability)持久性
事務所做的修改,將會永久的保存,不會因為系統的意外導致數據的丟失,這個是最好理解的,這里不再贅述。
事務的開啟
事務在mysql是默認開啟的,可以通過show variables like 'autocommit' 查看是否開啟自動提交。
SQL語句開啟事務
mysql默認是自動開啟了autocommit的,所以如果autocommit沒有開啟,可以通過set session autocommit=true開啟會話級別的自動提交開關(一般不建議開啟自動提交開關)?。
開啟了自動提交的開關之后,update/delete/insert操作,mysql會自動commit這些操作。但是要開啟自定義的事務,需要用到begin或者start transaction。
一個簡單的實例,有一個表,記錄如下:
就兩條記錄,比較簡單 。然后執行以下事務,但是并不執行完成,只執行到第二句;并不提交或者回滾事務。
BEGIN; UPDATE teacher SET NAME = 'limantest' WHERE id = 1; COMMIT; ROLLBACK;之后可以看到如下現象:
左邊可以看到,update操作已經成功,但是通過另一個客戶端看到的記錄卻是更新之前的數據。(在未提交或者回滾這個更新操作時,同一個會話中能看到操作的最新數據),根本原因就是該事務沒有提交或者回滾。等到正式提交之后,這個數據才會更新
至此可以做一個小結,在SQL語句中對事務的操作如下:
| begin/start transaction | 手動開啟事務 |
| commit/rollback | 提交/回滾事務 |
| set session autocommit=on/off | 開啟/關閉會話級別的自動提交 |
JDBC中開啟事務
這個就比較簡單了,在connection對象級別,設置自動提交參數即可——connection.setAutoCommit(true/false)
Spring中開啟事務
利用AOP,這個可以參看相關大佬的博客即可,這里不再贅述。
老生常談的問題
前面說過,并發場景下,為了保證事務的隔離性,mysql引入了各種鎖來解決,但是在真正總結鎖之前,需要明確,并發場景下會有那些問題(這些問題已經被很多大牛總結過N多次了)。
臟讀
如下圖所示,事務B在更新數據的過程中,事務A讀取了數據,之后事務B回滾了這個數據,導致事務A并沒有讀取到正確的數據,因此這個稱之為臟讀。
不可重復讀
事務A在事務B更新數據某一條數據之前和之后分別讀取了數據庫中的數據,導致事務A兩次讀取的數據結果不一致。如下圖所示,不可重復讀和幻讀最大的差別就是不可重復讀是針對某條具體的數據而言的。
幻讀
幻讀與不可重復讀有點像,但是不同點是幻讀是針對批量數據而言,不可重復讀是針對一條具體的數據而言。同樣是事務A在事務B更新數據的前后讀取了數據表中的數據,導致批量數據前后讀取的不一致。
?事務的四種隔離級別
因為存在上述的三種問題,因此為了解決這些問題,ISO定義了四種事務的隔離級別,這個也是一個老的概念了很多大牛都有過總結。分別為如下四種隔離級別
1、Read Uncommitted(讀未提交)
這種是最low的一種隔離級別,似乎什么都沒做,允許出現臟讀,事務未提交的數據對其他事務也是可見的(即事務B沒有提交的數據對事務A也是可見的)
2、Read Commit(讀已提交)
一個事務開啟之后,這個事務就只能看到自己提交的事務所做的修改。——解決了臟讀,但是沒有解決不可重復讀這個問題
3、Repeatable Read (可重復讀)
同一個事務,針對某一條具體的記錄,多次讀取其他事務操作的數據,結果都是一樣的?!鉀Q了不可重復讀的問題,但是沒有解決幻讀的問題
4、Serializable(串行化)
每個事務排隊讀取或操作數據?!粫嬖跀祿灰恢碌膯栴}了,世界至此和平。
針對InnoDB引擎,厲害之處就在于,在MySQL的事務隔離級別為Repeatable Read(可重復讀)的時候,通過鎖或者MVCC機制解決了幻讀的問題。這也就是本篇博客要重點總結的地方。
下面就開始重點總結InnoDB的鎖和MVCC機制。
鎖
鎖是個什么東西,這里就不介紹了,接觸過并發概念的,都會明白。這里就不說鎖的概念了,直接總結InnoDB中的幾種鎖。
表鎖與行鎖簡介
表鎖——即鎖住整張數據表,行鎖——即鎖住某一行具體的數據記錄。兩者之間的差異簡單用下表所示
| 鎖定粒度 | 表鎖>行鎖(行鎖粒度更細) |
| 加鎖效率 | 表鎖>行鎖(表鎖無需定位到具體的數據行,自然加鎖操作更快) |
| 沖突概率 | 表鎖>行鎖(數據行的條數大于表的個數,自然表鎖沖突概率較大) |
| 并發性能 | 表鎖<行鎖 |
InnoDB鎖的類型
從mysql的官網來看,mysql的鎖有8種之多,但是其實有些鎖本質只是一個思想而已,個人認為并不能算作真正意義上的鎖。下面開始詳細介紹每一個鎖。
共享鎖(Shared Lock——S鎖)
共享鎖也叫S鎖。這個和我們在Java并發變成中接觸的讀鎖很類似,事務A給某條數據加上了讀鎖,其他事務就只能讀取該數據了(可以獲取該數據的S鎖),在事務A釋放鎖之前,都不能修改這個數據。通過LOCK IN SHARE MODE的方式可以加鎖,如下圖所示,以動圖的形式展現了共享鎖的實例。
排他鎖(Exclusive Lock——X鎖)
排他鎖也叫X鎖。不能與其他鎖并存,如果事務A獲取了某一行數據的排他鎖,則事務B無法獲取該行數據的任何鎖,只有獲取該行數據的排他鎖的事務A是可以對這行數據進行讀取和操作的。(但是可以進行查詢,只是這個時候查詢的結果來自快照——后面再介紹快照的概念)。
delete/update/insert默認就會加上排他鎖。select 語句后面加上 FOR UPDATE也會獲取排他鎖。
下面還是用一個動圖來表示。在事務A執行了update語句之后,沒有提交,事務B嘗試去獲取共享鎖失敗,在事務A釋放之后,事務B才正確獲取到了數據。
InnoDB所謂的行鎖和表鎖
在熟悉了共享鎖和排他鎖(這兩者都是行鎖)之后,Innodb在此基礎上實現了表鎖,但是其實核心并沒有多復雜,可以通過以下幾個實例來進行總結,先完成準備工作:
準備一張數據表——users,并建立一個主鍵索引,在name數據列上建立唯一索引。
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=20 DEFAULT CHARSET=utf8mb4?在表中插入如下數據:
insert into `users` (`id`, `name`, `age`, `phoneNum`, `lastUpdate`) values('1','test01','26','13666666666','2018-12-07 19:22:51'); insert into `users` (`id`, `name`, `age`, `phoneNum`, `lastUpdate`) values('2','test02','19','13777777777','2018-12-08 21:01:12'); insert into `users` (`id`, `name`, `age`, `phoneNum`, `lastUpdate`) values('3','test03','20','13888888888','2018-12-08 20:59:39'); insert into `users` (`id`, `name`, `age`, `phoneNum`, `lastUpdate`) values('4','test04','99','13444444444','2018-12-06 20:34:10'); insert into `users` (`id`, `name`, `age`, `phoneNum`, `lastUpdate`) values('6','test06','91','13444444544','2018-12-06 20:35:07'); insert into `users` (`id`, `name`, `age`, `phoneNum`, `lastUpdate`) values('11','test11','33','13441444544','2018-12-06 20:36:19'); insert into `users` (`id`, `name`, `age`, `phoneNum`, `lastUpdate`) values('15','test15','30','1344444444','2018-12-08 15:08:24'); insert into `users` (`id`, `name`, `age`, `phoneNum`, `lastUpdate`) values('19','test19','30','1344444444','2018-12-08 21:21:47');1、沒有索引的列
事務A執行update users set lastUpdate=now() where phoneNum = '13666666666'的操作,會發現,整行表的數據都被鎖住了,無法獲取任何數據行的共享鎖或者排他鎖,只有事務A的鎖釋放之后,其他事務才能進行數據操作。如下動圖所示。
2、主鍵索引上更新數據
事務A需要修改id為1的數據,事務B在其修改過程中,試圖也去修改數據,會發現阻塞,但是事務B修改id為2的數據卻能正常修改,說明這個時候并沒有鎖表,而是簡單的鎖住了數據行。
3、唯一索引的操作
事務A嘗試修改name='test01'的數據,事務B嘗試通過對應的id去更新這條數據,但是被阻塞。同時事務B可以正常更新其他記錄的數據。
?其實從上面的三個例子中我們似乎能看出,Innodb的行鎖和表鎖的區別。這兩者似乎都和索引有關,所以我們這里似乎可以總結一下,Innodb的行鎖究竟鎖了什么。
1、通過實例1我們發現只有通過索引的數據列操作數據,InnoDB才會使用行級鎖,否則Innodb將會使用表鎖。因為在實例1中事務通過一個沒有加索引的列操作數據,導致整表被鎖。
2、通過實例2和實例3我們進一步可以確認,InnoDB的行鎖是通過給索引加鎖來實現的。在實例2和實例3中,通過索引項操作數據的時候,發現只是鎖住的數據行,并沒有鎖住整個表數據。同時在實例三種,事務A通過唯一索引操作操作數據的時候,發現事務B通過主鍵索引也無法操作數據。所以這里可以確認,通過唯一索引項操作數據,會在唯一索引的索引關鍵字和主鍵索引的對應的關鍵字上加鎖(這個可能要結合上一篇博客總結的輔助索引來理解),相當于鎖住了兩個索引關鍵字。
理解了上述三個實例之后,就開始正式梳理InnoDB的表鎖。
意向共享鎖(IS)
意向共享鎖和意向排他鎖,這個就好比我們學習并發的概念時候,類比的一個火車上衛生間的指示燈。這里兩個鎖都找不到準確的概念來描述,就好比一個指示燈吧。
意向共享鎖——事務準備給數據行加入共享鎖之前必須先取得該表的意向共享鎖。意向共享鎖之間是可以相互兼容的
意向排他鎖(IX)
意向排他鎖——事務準備給數據行加入排他鎖之前必須先取得該表的意向排他鎖。意向排他鎖之間是可以相互兼容的
意向鎖是InnoDB數據操作之前自動加上的,不需要我們做任何操作。當某個事務想去進行鎖表的時候,可以先判斷意向鎖是否存在,如果存在說明其它事務正在對該表進行操作,該事務可以快速返回即可。(就可以理解為火車上衛生間的指示燈)
?
自增鎖
這個是一個特殊的表級別的鎖,我們在之前在建表的時候,將Id設置為自增,事務未提交的id,將會永久丟失??梢酝ㄟ^設置系統變量innodb_autoinc_lock_mode改變自增的步長。
臨鍵鎖(Next-key locks)
總結到這里,嚴格意義上說對mysql的行鎖和表鎖已經總結的差不多了,但是針對行鎖還有一些其他的操作。這里簡單總結一下,先準備一張數據表,SQL如下:
CREATE TABLE `t2` (`id` int(11) NOT NULL,`name` varchar(255) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8;insert into `t2` (`id`, `name`) values('1','1'); insert into `t2` (`id`, `name`) values('4','4'); insert into `t2` (`id`, `name`) values('7','7'); insert into `t2` (`id`, `name`) values('10','10');?表結構異常的簡單。
之后開始我們的總結,先從臨鍵鎖開始
臨鍵鎖是InnoDB默認的行鎖算法,以上表的數據為例(上表的數據比較簡單,只有四條數據,1,4,7,10),InnoDB會將這些記錄按照左開右閉的原則,劃分為如下區間
當查找條件為范圍時,且有數據命中則此時的SQL會加上臨鍵鎖,且鎖住索引的記錄+區間(有點繞,看下面的實例就知道了)?
實例:
事務1執行以下SQL,select * from t2 where id > 5 and id < 9 for update; 這個事務的查找條件是一個范圍,根據Innodb會對其采用臨鍵鎖,根據上述原則,id>5&& id<9 則id=7 被命中,所以id=7左邊和右邊的區間被鎖住,即(4,7]和(7,10]被鎖住。這個時候我們開啟事務2,利用事務2獲取id=10這條記錄的行鎖,會發現被阻塞。同時事務2執行insert into t2 (id,name) values(12,'12');能順利執行,說明其他區間并沒有被鎖住。
間隙鎖(Gap鎖)
當查找條件為范圍查找的時候,沒有記錄能精確匹配到該范圍,則臨鍵鎖會退化為gap鎖。例如:如果執行select * from t2 where id >4 and id <6 for update;則這個時候會鎖住區間(4,7),實例如下:
事務A在執行id范圍為4到6查詢的時候,發現表中并沒有確定的記錄進行匹配,這個時候會鎖住區間(4,7)。事務2嘗試插入id為5的數據會阻塞,直到事務A釋放了gap鎖之后,事務B才成功提交(這,不就是解決了幻讀的問題么)。這里可以看到間隙鎖正好解決了幻讀的問題,因此對應本篇文章的開頭老生常談的問題,間隙鎖必然是只會在RR(可重復讀)的隔離級別存在。
記錄鎖
?這個與開頭介紹的索引的行鎖的區別就是,這里是針對普通列而言,如果針對我們建立的t2表,select * from t2 where id = 4;區間(1,7)會被鎖定,如果id變為主鍵,則不會鎖定區間。如下所示,第一次執行的時候,區間被鎖定,事務B無法插入id為3的數據,第二次執行的時候,已經將id改為主鍵,但是能正常插入id為3的數據。
至此大部分常見的8種鎖已經總結完成,可以通過一個表來簡單總結一下
| 共享鎖 | 行級鎖,其他事務能正常讀取,但是無法操作修改數據 |
| 排他鎖 | 行級鎖,其它事務不能做任何操作 |
| 意向共享鎖 | 表級鎖,共享鎖的指示燈 |
| 意向排他鎖 | 表級鎖,排他鎖的指示燈 |
| 自增鎖 | 一種特殊的表級鎖,事務未提交的id會丟失 |
| 記錄鎖 | 非索引列鎖住區間 |
| 間隙鎖 | 未匹配的記錄,鎖住區間 |
| 臨鍵鎖 | 某種程度上,臨鍵鎖=間隙鎖+記錄鎖 |
回到老生常談的問題
針對臟讀
在事務B修改數據的時候,給數據加上X鎖(排他鎖)即可解決臟讀的問題
針對不可重復讀
事務A在第一次讀取數據的時候,給數據加上S鎖(共享鎖)。事務B就無法修改數據了。
?針對幻讀
事務A在進行范圍查找的時候,這個范圍的數據都會被鎖住,因此并不存在幻讀的問題了。這就是InnoDB的厲害之處,利用臨鍵鎖,間隙鎖,記錄鎖解決了幻讀的問題。
?一個遺留問題
回到本文開頭的X鎖的實例:我們說過,update會自動的加上X鎖,而加上了X鎖,則無法獲取共享鎖,也就是無法讀取記錄了。
但是看如下實例:
事務B讀取的操作似乎并沒有受到影響,這里就涉及快照和MVCC的概念,會在下一篇博客中進行總結?
總結
本篇博客從老生常談的數據庫事務問題出發,介紹了InnoDB的幾種鎖,然后通過實例總結了這幾種鎖是如何解決事務帶來的問題的。
總結
以上是生活随笔為你收集整理的MySql(三)——事务和锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: loadrunner 注册破解
- 下一篇: 给C盘减减肥,让你电脑飞一般速度