對于DB來說,經(jīng)常會面對并發(fā)問題逻锐,但是開發(fā)的時候DB總是能很好的解決并發(fā)的問題。那么面對并發(fā)DB是怎么進(jìn)行控制的呢?之前一段時間總是對Mysql的鎖機(jī)制概念十分模糊,什么時候加鎖馍刮?加什么鎖?鎖住之后會是怎么樣窃蹋?
需要明確的點(diǎn)####
首先卡啰,鎖是為了解決數(shù)據(jù)庫事務(wù)并發(fā)問題引入的特性静稻,在Mysql中鎖的行為是和mysql隔離機(jī)制有關(guān)的,畢竟鎖是用來解決DB的隔離性和一致性的匈辱。并不是任何操作都是需要加鎖的振湾,讀操作是不加鎖的,當(dāng)然也可以顯式的加鎖(lock in share mode或for update)亡脸。
Mysql鎖的類型####
Mysql因?yàn)橛泻芏喾N存儲引擎押搪,導(dǎo)致它的實(shí)現(xiàn)也是五花八門,但是最常用的就應(yīng)該是MyISAM和InnoDB了浅碾。對于兩者的區(qū)別之前也寫過大州,其中有一點(diǎn)是MyISAM鎖級別是表級而InnoDB的鎖級別是行級(當(dāng)然InnoDB也有表級鎖)。mysql鎖的類別如下:
表級鎖:開銷小垂谢,加鎖快摧茴;不會出現(xiàn)死鎖;鎖定粒度大埂陆,發(fā)生鎖沖突的概率最高,并發(fā)度最低。
行級鎖:開銷大娃豹,加鎖慢焚虱;會出現(xiàn)死鎖;鎖定粒度最小懂版,發(fā)生鎖沖突的概率最低,并發(fā)度也最高鹃栽。
頁面鎖:開銷和加鎖時間界于表鎖和行鎖之間;會出現(xiàn)死鎖躯畴;鎖定粒度界于表鎖和行鎖之間民鼓,并發(fā)度一般。
不同的鎖粒度決定了不同引擎的應(yīng)用場景蓬抄,我們最常用的表級鎖的引擎是MyISAM和InnoDB丰嘉,行級引擎是InnoDB。至于頁級鎖的引擎常用的是Berkeley DB嚷缭。
Mysql的鎖####
Mysql的鎖主要為兩種:共享鎖(S Lock)和排他鎖(X Lock)饮亏。從字面上我們可以理解,共享鎖就是多個事務(wù)可以共享阅爽,互相兼容路幸。而排他鎖則是多個事務(wù)不兼容互相排斥。
如果一個事務(wù)T1獲得了r行的共享鎖付翁,那么另外一個事務(wù)T2可以立即獲得r的共享鎖简肴,這種情況稱為“鎖兼容”。如果有T3想獲得r行的排他鎖必須等到T1百侧、T2釋放r行的共享鎖砰识,這種稱為“鎖不兼容”能扒,下表對應(yīng)的是鎖兼容性:
可以看到只有共享鎖是兼容的,也就是說讀請求和讀請求之間是沒有影響的仍翰。
InnoDB為了支持在不同粒度上加鎖操作赫粥,InnoDB支持另一種加鎖機(jī)制——意向鎖。意向鎖的意思很簡單予借,就是有意愿進(jìn)行加鎖越平。
意向共享鎖(IS Lock):事務(wù)想要獲取一張表中的某幾行共享鎖。
意向排他鎖(IX Lock):事務(wù)想要獲取一張表中的某幾行的排它鎖灵迫。
由于InnoDB支持的行級別的鎖秦叛,因此意向鎖其實(shí)不會阻塞除全表掃描意外的任何請求。意向鎖的兼容性如下所示:
意向鎖和意向鎖之間是完全兼容的瀑粥,但是意向鎖和共享鎖以及排它鎖可能是有互斥性的挣跋。因?yàn)橐庀蜴i的鎖粒度是表級鎖,所以在全表掃描是往往會對表加鎖狞换,那么此時就會發(fā)生鎖沖突避咆。
之前一直不明白意向鎖到底是干什么的,相信很多人和我一樣修噪,后來查了很多資料才知道查库,有一個很形象的例子:
如果你家小區(qū)有一個保安,那么就能避免經(jīng)常有人去按你家的門鎖...
保安就是意向鎖黄琼,它能避免經(jīng)常有請求去請求行級鎖樊销,因?yàn)樵L問行級鎖也是有一定開銷的。
上面說的東西概念性都比較強(qiáng)脏款,但是千萬別被誤導(dǎo)围苫,因?yàn)樯厦娴母拍钤趯?shí)際的查詢中不一定全都會使用,例如mysql的讀操作撤师,通常是不會加鎖的(和隔離機(jī)制有關(guān))剂府,也就是說通常的讀操作是不加鎖的,而是通過mvcc去解決的丈氓,對于通常的寫請求周循,insert、update万俗、delete通常會加行鎖湾笛、間隙鎖或表鎖(這和索引是有關(guān)系的),這些鎖通常是排他的闰歪,會阻塞其他的事務(wù)寫事務(wù)嚎研。具體的情況需要結(jié)合隔離機(jī)制。
Mysql的隔離性####
隔離性是指一個事務(wù)所做的修改在最終提交之前,對其他的事務(wù)是不可見的临扮。
mysql的隔離性分為四個隔離級別论矾,不同的隔離級別有不同的特點(diǎn)和實(shí)現(xiàn):
1.Read Uncommitted(臟讀):從隔離級別的名稱可知,事務(wù)可以讀取到其他沒有commit的事務(wù)的修改杆勇,所以稱為臟讀贪壳,因?yàn)樽x取到了本來不應(yīng)該讀到的記錄,此事務(wù)隔離級別一般是不會用的蚜退,因?yàn)槿绻竺媪硪粋€事務(wù)rollback掉了闰靴,豈不是悲劇了?
2.Read Committed(提交讀钻注,也叫不可重復(fù)讀):只能讀取到已經(jīng)提交的數(shù)據(jù)蚂且。Oracle等多數(shù)數(shù)據(jù)庫默認(rèn)都是該級別 (不重復(fù)讀)。對于此級別的隔離幅恋,比較上面的臟讀是會嚴(yán)格一些的杏死,例如事務(wù)1開始查詢了一條記錄,但是隨后另一個事務(wù)2修改了本條記錄捆交,此時事務(wù)1再次進(jìn)行讀取淑翼,此時是讀取不到的因?yàn)槭聞?wù)2沒有進(jìn)行commit,隨后事務(wù)2commit品追,事務(wù)1再次讀取窒舟,可以讀到最新修改后的記錄。這比臟讀更加嚴(yán)格了一些诵盼,因?yàn)樽x取不到未提交的數(shù)據(jù),但是此種隔離級別在同一個事務(wù)(事務(wù)1中)兩次讀取银还,讀取到了不同的結(jié)果风宁,這也就是不可重復(fù)讀。
在RC級別中蛹疯,數(shù)據(jù)的讀取都是不加鎖的戒财,但是數(shù)據(jù)的寫入、修改和刪除是需要加鎖的捺弦。
一個例子:
CREATE TABLE `student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`stu_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_student_id` (`stu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5
+----+------+--------+
| id | name | stu_id |
+----+------+--------+
| 1 | 語文 | 1 |
| 2 | 數(shù)學(xué) | 2 |
| 3 | 英語 | 1 |
+----+------+--------+
3 rows in set
上面是student表內(nèi)的數(shù)據(jù)饮寞,接下來設(shè)置事務(wù)隔離級別為RC
SET session transaction isolation level read committed;
SET SESSION binlog_format = 'ROW';
接下來測試一下update的行鎖:
T1 | T2 |
---|---|
update student set name = '生物' where stu_id = 2; | |
update student set name = '生物' where stu_id = 2; | |
更新成功 | 阻塞 |
commit | |
更新成功 |
上面的update例子說明,在更新記錄的時候會對此記錄加行鎖列吼,在事務(wù)沒有commit之前不會釋放鎖幽崩,所以事務(wù)2的更新會阻塞等待事務(wù)1的排它鎖,當(dāng)事務(wù)1Commit后寞钥,行鎖釋放事務(wù)2獲得行鎖慌申,更新成功。
其實(shí)mysql的鎖機(jī)制是通過對索引加鎖理郑,但是一旦更新不走索引會怎么樣蹄溉,答案是會全表掃描咨油,鎖表。所以在更新的時候盡量走索引柒爵,避免不必要的麻煩役电,具體這種索引和鎖的問題推薦一篇博客:http://hedengcheng.com/?p=771#_Toc374698322
接下來實(shí)驗(yàn)一下RC基本寫的不可重復(fù)讀:
事務(wù)1:
mysql> begin;
Query OK, 0 rows affected
mysql> select * from student where stu_id = 2;
+----+------+--------+
| id | name | stu_id |
+----+------+--------+
| 2 | 生物 | 2 |
+----+------+--------+
1 row in set
事務(wù)2:
mysql> begin;
Query OK, 0 rows affected
mysql> update student set name = '地理' where stu_id = 2;
Query OK, 1 row affected
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit;
Query OK, 0 rows affected
接下來事務(wù)1再次查詢:
mysql> select * from student where stu_id = 2;
+----+------+--------+
| id | name | stu_id |
+----+------+--------+
| 2 | 地理 | 2 |
+----+------+--------+
1 row in set
上述過程可見,帶事務(wù)1的一個事務(wù)中棉胀,兩次請求得到了不同的結(jié)果法瑟,就導(dǎo)致了不可重復(fù)讀的現(xiàn)象。
3.Repeatable Read(可重讀或者叫幻讀):RR解決了臟讀的問題膏蚓,該級別保證了在同一個事務(wù)中多次讀取同樣記錄的結(jié)果是一致的瓢谢。
例子和上面RC中的例子一樣,只不過在事務(wù)2提交時驮瞧,事務(wù)1再次查詢是看不到事務(wù)1更新的記錄的氓扛,所以叫可重復(fù)讀,但是理論上這種方式只能解決更新問題论笔,但是解決不了新增的問題采郎,因?yàn)闊o論RC還是RR,mysql都是通過Mvcc(Multi-Version Concurrency Control )機(jī)制去實(shí)現(xiàn)的狂魔。
Mvcc是多版本的并發(fā)控制協(xié)議酌摇,它和基于鎖的并發(fā)控制最大的區(qū)別和優(yōu)點(diǎn)是:讀不加鎖,讀寫不沖突暑刃。它將每一個更新的數(shù)據(jù)標(biāo)記一個版本號铲敛,在更新時進(jìn)行版本號的遞增,插入時新建一個版本號籽孙,同時舊版本數(shù)據(jù)存儲在undo日志中烈评。
而對于讀操作,因?yàn)槎喟姹镜囊敕附ǎ头譃榭煺兆x和當(dāng)前讀讲冠。快照讀只是針對于目標(biāo)數(shù)據(jù)的版本小于等于當(dāng)前事務(wù)的版本號适瓦,也就是說讀數(shù)據(jù)的時候可能讀到舊的數(shù)據(jù)竿开,但是這種快照讀不需要加鎖,性能很高玻熙。當(dāng)前讀是讀取當(dāng)前數(shù)據(jù)的最新版本否彩,但是更新等操作會對數(shù)據(jù)進(jìn)行加鎖,所以當(dāng)前讀需要獲取記錄的行鎖嗦随,存在鎖爭用的問題胳搞。
RC和RR都是基于Mvcc實(shí)現(xiàn),但是讀取的快照數(shù)據(jù)是不同的。RC級別下肌毅,對于快照讀筷转,讀取的總是最新的數(shù)據(jù),也就出現(xiàn)了上面的例子悬而,一個事務(wù)中兩次讀到了不同的結(jié)果呜舒。而RR級別總是讀到小于等于此事務(wù)的數(shù)據(jù),也就實(shí)現(xiàn)了可重讀笨奠。
下面是快照讀和當(dāng)前讀的常見操作:
- 快照讀:就是select
select * from table ....; - 當(dāng)前讀:特殊的讀操作(加共享鎖或排他鎖)袭蝗,插入/更新/刪除操作,需要加鎖般婆。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert;
update ;
delete;
其實(shí)Mysql實(shí)現(xiàn)的Mvcc并不純粹到腥,因?yàn)樵诋?dāng)前讀的時候需要對記錄進(jìn)行加鎖,而不是多版本競爭蔚袍。下面是具體操作時的Mvcc機(jī)制:
- SELECT時乡范,讀取創(chuàng)建版本號<=當(dāng)前事務(wù)版本號,刪除版本號為空或>當(dāng)前事務(wù)版本號啤咽。
- INSERT時晋辆,保存當(dāng)前事務(wù)版本號為行的創(chuàng)建版本號
- DELETE時,保存當(dāng)前事務(wù)版本號為行的刪除版本號
- UPDATE時宇整,插入一條新紀(jì)錄瓶佳,保存當(dāng)前事務(wù)版本號為行創(chuàng)建版本號,同時保存當(dāng)前事務(wù)版本號到原來刪除的行
上面說明了RR是如何解決重讀問題鳞青,但是眾所周知霸饲,RR有一個致命的問題就是幻讀,即只能解決另一個事務(wù)2更新對事務(wù)1不可見的問題臂拓,但是當(dāng)事務(wù)2新插入一行數(shù)據(jù)的時候贴彼,事務(wù)1還是可見,這就是幻讀問題埃儿。但是在實(shí)際使用中,我們發(fā)現(xiàn)并沒有發(fā)生“幻讀”問題融涣。那么童番,Mysql是如何解決幻讀問題的呢?
我們分兩個方面說:
1.快照讀:對于快照讀威鹿,其實(shí)是不會出現(xiàn)幻讀問題的剃斧,通過上面我們得知,select時只會讀取小于等于當(dāng)前事務(wù)版本的行忽你,但是新行的版本號是高于讀事務(wù)的幼东,那么新插入的行對之前的讀事務(wù)是不可見的。
2.當(dāng)前讀:因?yàn)楫?dāng)前讀,讀到的往往是最新的行數(shù)據(jù)根蟹,但是對于事務(wù)1更新了一行脓杉,同時事務(wù)2插入了一個新行(利用一個非唯一索引進(jìn)行更新),那么會利用gap鎖去控制新行的插入來避免這個問題简逮。一個例子看一下:
首先開啟事務(wù)A:
mysql> begin;
Query OK, 0 rows affected
mysql> select * from student where stu_id =3;
+----+------+--------+
| id | name | stu_id |
+----+------+--------+
| 2 | 化學(xué) | 3 |
+----+------+--------+
1 row in set
mysql> update student set name = "物理" where stu_id = 3;
Query OK, 1 row affected
Rows matched: 1 Changed: 1 Warnings: 0
接下來開啟事務(wù)B:
mysql> begin;
Query OK, 0 rows affected
mysql> insert into student(id,name,stu_id) values (5,"歷史",3);
Query OK, 1 row affected
我們可以看到球散,事務(wù)A在更新之后,事務(wù)B進(jìn)行插入操作的時候會阻塞散庶,但是這里使用的不是行鎖蕉堰,這就是因?yàn)閞r隔離模式下,mysql使用的是next-keylocking機(jī)制防止“當(dāng)前讀”的幻讀問題悲龟。如果不阻塞新插入的數(shù)據(jù)屋讶,那么就會導(dǎo)致更新之后,再次查詢時會發(fā)現(xiàn)部分?jǐn)?shù)據(jù)沒有更新须教,本意是按照索引更新所有的行皿渗,但是新插入的行沒有更新,這就會令我們很奇怪没卸。
那需要先說說Mysql里面特殊的鎖——Next-Key鎖:
Next-Key鎖是行鎖和Gap鎖(間隙鎖)的合體(可以理解為二者相加羹奉,因?yàn)間ap鎖是開區(qū)間的,加上行鎖正好是閉區(qū)間)约计。間隙鎖诀拭,顧名思義,是對一個間隙進(jìn)行加鎖煤蚌,間隙是索引的間隙耕挨,也就是說,更新的時候必須走索引尉桩,否則會將全表鎖住筒占。導(dǎo)致其他所有的寫操作全部阻塞。next-key鎖主要是針對非唯一索引蜘犁,因?yàn)槲ㄒ凰饕椭麈I索引每次只會定位到單條記錄翰苫,所以不需要next-key鎖,下面盜一張圖來理解下:
當(dāng)按照id(非唯一索引这橙,不是主鍵奏窑,主鍵是name)進(jìn)行更新或刪除的時候會先對id索引進(jìn)行加鎖,但加的是next_key鎖屈扎。因?yàn)樵赗R隔離級別下埃唯,需要防止“當(dāng)前讀”的幻讀問題,加上next-keylock之后鹰晨,在[6-10]區(qū)間和[10-11]區(qū)間進(jìn)行插入時會阻塞墨叛,因?yàn)橐呀?jīng)加了next-key鎖止毕,為什么用next-key鎖?因?yàn)樾略黾拥挠涗浿荒茉?0的左邊和10的右邊或者就是10漠趁。那么鎖住范圍后就能保證防止“幻讀”扁凛。
4.Serializable(可串行化):這個隔離級別,在并發(fā)效果上最差的棚潦,因?yàn)樽x加共享鎖令漂,寫加排他鎖,讀寫互斥丸边。也就是說此級別下select是需要加鎖的叠必。此模式下可以保證數(shù)據(jù)安全,適用于并發(fā)比較低妹窖,同時數(shù)據(jù)安全性要求比較高的場景纬朝。
總結(jié):mysql的鎖機(jī)制和事務(wù)隔離級別有關(guān)。并不是說所有的讀操作都不加鎖骄呼,寫操作加鎖共苛,加什么鎖也和索引類型、有無索引有關(guān)蜓萄。
國慶糾結(jié)了幾天隅茎,總結(jié)了一下,如果有什么錯誤還請指出嫉沽。還有明天得上班了=_=,I am angry~
參考:
https://book.douban.com/subject/23008813/
https://book.douban.com/subject/5373022/
http://tech.meituan.com/innodb-lock.html
http://hedengcheng.com/?p=771