對于DB來說,經常會面對并發問題,但是開發的時候DB總是能很好的解決并發的問題。那么面對并發DB是怎么進行控制的呢?之前一段時間總是對Mysql的鎖機制概念十分模糊,什么時候加鎖?加什么鎖?鎖住之后會是怎么樣?

  需要明確的點

  首先,鎖是為了解決數據庫事務并發問題引入的特性,在Mysql中鎖的行為是和mysql隔離機制有關的,畢竟鎖是用來解決DB的隔離性和一致性的。并不是任何操作都是需要加鎖的,讀操作是不加鎖的,當然也可以顯式的加鎖(lock in share mode或for update)。

  Mysql鎖的類型

  Mysql因為有很多種存儲引擎,導致它的實現也是五花八門,但是最常用的就應該是MyISAM和InnoDB了。對于兩者的區別之前也寫過,其中有一點是MyISAM鎖級別是表級而InnoDB的鎖級別是行級(當然InnoDB也有表級鎖)。mysql鎖的類別如下:

  表級鎖:開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖沖突的概率最高,并發度最低。

  行級鎖:開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖沖突的概率最低,并發度也最高。

  頁面鎖:開銷和加鎖時間界于表鎖和行鎖之間;會出現死鎖;鎖定粒度界于表鎖和行鎖之間,并發度一般。

  不同的鎖粒度決定了不同引擎的應用場景,我們最常用的表級鎖的引擎是MyISAM和InnoDB,行級引擎是InnoDB。至于頁級鎖的引擎常用的是Berkeley DB。

  Mysql的鎖

  Mysql的鎖主要為兩種:共享鎖(S Lock)和排他鎖(X Lock)。從字面上我們可以理解,共享鎖就是多個事務可以共享,互相兼容。而排他鎖則是多個事務不兼容互相排斥。

  如果一個事務T1獲得了r行的共享鎖,那么另外一個事務T2可以立即獲得r的共享鎖,這種情況稱為“鎖兼容”。如果有T3想獲得r行的排他鎖必須等到T1、T2釋放r行的共享鎖,這種稱為“鎖不兼容”,下表對應的是鎖兼容性:

Mysql InnoDB引擎的鎖和隔離機制那些事兒

  可以看到只有共享鎖是兼容的,也就是說讀請求和讀請求之間是沒有影響的。

  InnoDB為了支持在不同粒度上加鎖操作,InnoDB支持另一種加鎖機制——意向鎖。意向鎖的意思很簡單,就是有意愿進行加鎖。

  意向共享鎖(IS Lock):事務想要獲取一張表中的某幾行共享鎖。

  意向排他鎖(IX Lock):事務想要獲取一張表中的某幾行的排它鎖。

  由于InnoDB支持的行級別的鎖,因此意向鎖其實不會阻塞除全表掃描意外的任何請求。意向鎖的兼容性如下所示:

Mysql InnoDB引擎的鎖和隔離機制那些事兒

  意向鎖和意向鎖之間是完全兼容的,但是意向鎖和共享鎖以及排它鎖可能是有互斥性的。因為意向鎖的鎖粒度是表級鎖,所以在全表掃描是往往會對表加鎖,那么此時就會發生鎖沖突。

  之前一直不明白意向鎖到底是干什么的,相信很多人和我一樣,后來查了很多資料才知道,有一個很形象的例子:

  如果你家小區有一個保安,那么就能避免經常有人去按你家的門鎖...

  保安就是意向鎖,它能避免經常有請求去請求行級鎖,因為訪問行級鎖也是有一定開銷的。

  上面說的東西概念性都比較強,但是千萬別被誤導,因為上面的概念在實際的查詢中不一定全都會使用,例如mysql的讀操作,通常是不會加鎖的(和隔離機制有關),也就是說通常的讀操作是不加鎖的,而是通過mvcc去解決的,對于通常的寫請求,insert、update、delete通常會加行鎖、間隙鎖或表鎖(這和索引是有關系的),這些鎖通常是排他的,會阻塞其他的事務寫事務。具體的情況需要結合隔離機制。

  Mysql的隔離性

  隔離性是指一個事務所做的修改在最終提交之前,對其他的事務是不可見的。

  mysql的隔離性分為四個隔離級別,不同的隔離級別有不同的特點和實現:

  1.Read Uncommitted(臟讀):從隔離級別的名稱可知,事務可以讀取到其他沒有commit的事務的修改,所以稱為臟讀,因為讀取到了本來不應該讀到的記錄,此事務隔離級別一般是不會用的,因為如果后面另一個事務rollback掉了,豈不是悲劇了?

  2.Read Committed(提交讀,也叫不可重復讀):只能讀取到已經提交的數據。Oracle等多數數據庫默認都是該級別 (不重復讀)。對于此級別的隔離,比較上面的臟讀是會嚴格一些的,例如事務1開始查詢了一條記錄,但是隨后另一個事務2修改了本條記錄,此時事務1再次進行讀取,此時是讀取不到的因為事務2沒有進行commit,隨后事務2commit,事務1再次讀取,可以讀到最新修改后的記錄。這比臟讀更加嚴格了一些,因為讀取不到未提交的數據,但是此種隔離級別在同一個事務(事務1中)兩次讀取,讀取到了不同的結果,這也就是不可重復讀。

  在RC級別中,數據的讀取都是不加鎖的,但是數據的寫入、修改和刪除是需要加鎖的。

  一個例子:

SQL代碼
  1. CREATE TABLE `student` (  
  2.   `id` int(11) NOT NULL AUTO_INCREMENT,  
  3.   `namevarchar(100) NOT NULL,  
  4.   `stu_id` int(11) NOT NULL,  
  5.   PRIMARY KEY (`id`),  
  6.   KEY `idx_student_id` (`stu_id`)  
  7. ) ENGINE=InnoDB AUTO_INCREMENT=5  
SQL代碼
  1. +----+------+--------+  
  2. | id | name | stu_id |  
  3. +----+------+--------+  
  4. |  1 | 語文 |      1 |  
  5. |  2 | 數學 |      2 |  
  6. |  3 | 英語 |      1 |  
  7. +----+------+--------+  
  8. rows in set  

  上面是student表內的數據,接下來設置事務隔離級別為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例子說明,在更新記錄的時候會對此記錄加行鎖,在事務沒有commit之前不會釋放鎖,所以事務2的更新會阻塞等待事務1的排它鎖,當事務1Commit后,行鎖釋放事務2獲得行鎖,更新成功。

  其實mysql的鎖機制是通過對索引加鎖,但是一旦更新不走索引會怎么樣,答案是會全表掃描,鎖表。所以在更新的時候盡量走索引,避免不必要的麻煩。

  接下來實驗一下RC基本寫的不可重復讀:

  事務1:

SQL代碼
  1. mysql> begin;  
  2. Query OK, 0 rows affected  
  3.   
  4. mysql> select * from student where stu_id = 2;  
  5. +----+------+--------+  
  6. | id | name | stu_id |  
  7. +----+------+--------+  
  8. |  2 | 生物 |      2 |  
  9. +----+------+--------+  
  10. 1 row in set  

  事務2:

SQL代碼
  1. mysql> begin;  
  2. Query OK, 0 rows affected  
  3.   
  4. mysql> update student set name = '地理' where stu_id = 2;  
  5. Query OK, 1 row affected  
  6. Rows matched: 1  Changed: 1  Warnings: 0  
  7.   
  8. mysql> commit;  
  9. Query OK, 0 rows affected  

  接下來事務1再次查詢:

SQL代碼
  1. mysql> select * from student where stu_id = 2;  
  2. +----+------+--------+  
  3. | id | name | stu_id |  
  4. +----+------+--------+  
  5. |  2 | 地理 |      2 |  
  6. +----+------+--------+  
  7. 1 row in set  

  上述過程可見,帶事務1的一個事務中,兩次請求得到了不同的結果,就導致了不可重復讀的現象。

  3.Repeatable Read(可重讀或者叫幻讀):RR解決了臟讀的問題,該級別保證了在同一個事務中多次讀取同樣記錄的結果是一致的。

  例子和上面RC中的例子一樣,只不過在事務2提交時,事務1再次查詢是看不到事務1更新的記錄的,所以叫可重復讀,但是理論上這種方式只能解決更新問題,但是解決不了新增的問題,因為無論RC還是RR,mysql都是通過Mvcc(Multi-Version Concurrency Control )機制去實現的。

  Mvcc是多版本的并發控制協議,它和基于鎖的并發控制最大的區別和優點是:讀不加鎖,讀寫不沖突。它將每一個更新的數據標記一個版本號,在更新時進行版本號的遞增,插入時新建一個版本號,同時舊版本數據存儲在undo日志中。

  而對于讀操作,因為多版本的引入,就分為快照讀和當前讀。快照讀只是針對于目標數據的版本小于等于當前事務的版本號,也就是說讀數據的時候可能讀到舊的數據,但是這種快照讀不需要加鎖,性能很高。當前讀是讀取當前數據的最新版本,但是更新等操作會對數據進行加鎖,所以當前讀需要獲取記錄的行鎖,存在鎖爭用的問題。

  RC和RR都是基于Mvcc實現,但是讀取的快照數據是不同的。RC級別下,對于快照讀,讀取的總是最新的數據,也就出現了上面的例子,一個事務中兩次讀到了不同的結果。而RR級別總是讀到小于等于此事務的數據,也就實現了可重讀。

  下面是快照讀和當前讀的常見操作:

  1. 快照讀:就是select

  select * from table ....;

  2. 當前讀:特殊的讀操作(加共享鎖或排他鎖),插入/更新/刪除操作,需要加鎖。

  select from table where ? lock in share mode;

  select from table where ? for update;

  insert;

  update ;

  delete;

  其實Mysql實現的Mvcc并不純粹,因為在當前讀的時候需要對記錄進行加鎖,而不是多版本競爭。下面是具體操作時的Mvcc機制:

  1. SELECT時,讀取創建版本號<=當前事務版本號,刪除版本號為空或>當前事務版本號。

  2. INSERT時,保存當前事務版本號為行的創建版本號

  3. DELETE時,保存當前事務版本號為行的刪除版本號

  4. UPDATE時,插入一條新紀錄,保存當前事務版本號為行創建版本號,同時保存當前事務版本號到原來刪除的行

  上面說明了RR是如何解決重讀問題,但是眾所周知,RR有一個致命的問題就是幻讀,即只能解決另一個事務2更新對事務1不可見的問題,但是當事務2新插入一行數據的時候,事務1還是可見,這就是幻讀問題。但是在實際使用中,我們發現并沒有發生“幻讀”問題。那么,Mysql是如何解決幻讀問題的呢?

  我們分兩個方面說:

  1.快照讀:對于快照讀,其實是不會出現幻讀問題的,通過上面我們得知,select時只會讀取小于等于當前事務版本的行,但是新行的版本號是高于讀事務的,那么新插入的行對之前的讀事務是不可見的。

  2.當前讀:因為當前讀,讀到的往往是最新的行數據,但是對于事務1更新了一行,同時事務2插入了一個新行(利用一個非唯一索引進行更新),那么會利用gap鎖去控制新行的插入來避免這個問題。一個例子看一下:

  首先開啟事務A:

SQL代碼
  1. mysql> begin;  
  2. Query OK, 0 rows affected  
  3.   
  4. mysql> select * from student where stu_id =3;  
  5. +----+------+--------+  
  6. | id | name | stu_id |  
  7. +----+------+--------+  
  8. |  2 | 化學 |      3 |  
  9. +----+------+--------+  
  10. 1 row in set  
  11. mysql> update student set name = "物理" where stu_id = 3;  
  12. Query OK, 1 row affected  
  13. Rows matched: 1  Changed: 1  Warnings: 0  

  接下來開啟事務B:

SQL代碼
  1. mysql> begin;  
  2. Query OK, 0 rows affected  
  3.   
  4. mysql> insert into student(id,name,stu_id) values (5,"歷史",3);  
  5. Query OK, 1 row affected  

  我們可以看到,事務A在更新之后,事務B進行插入操作的時候會阻塞,但是這里使用的不是行鎖,這就是因為rr隔離模式下,mysql使用的是next-keylocking機制防止“當前讀”的幻讀問題。如果不阻塞新插入的數據,那么就會導致更新之后,再次查詢時會發現部分數據沒有更新,本意是按照索引更新所有的行,但是新插入的行沒有更新,這就會令我們很奇怪。

  那需要先說說Mysql里面特殊的鎖——Next-Key鎖:

  Next-Key鎖是行鎖和Gap鎖(間隙鎖)的合體(可以理解為二者相加,因為gap鎖是開區間的,加上行鎖正好是閉區間)。間隙鎖,顧名思義,是對一個間隙進行加鎖,間隙是索引的間隙,也就是說,更新的時候必須走索引,否則會將全表鎖住。導致其他所有的寫操作全部阻塞。next-key鎖主要是針對非唯一索引,因為唯一索引和主鍵索引每次只會定位到單條記錄,所以不需要next-key鎖,下面盜一張圖來理解下:

Mysql InnoDB引擎的鎖和隔離機制那些事兒

  當按照id(非唯一索引,不是主鍵,主鍵是name)進行更新或刪除的時候會先對id索引進行加鎖,但加的是next_key鎖。因為在RR隔離級別下,需要防止“當前讀”的幻讀問題,加上next-keylock之后,在[6-10]區間和[10-11]區間進行插入時會阻塞,因為已經加了next-key鎖,為什么用next-key鎖?因為新增加的記錄只能在10的左邊和10的右邊或者就是10。那么鎖住范圍后就能保證防止“幻讀”。

  4.Serializable(可串行化):這個隔離級別,在并發效果上最差的,因為讀加共享鎖,寫加排他鎖,讀寫互斥。也就是說此級別下select是需要加鎖的。此模式下可以保證數據安全,適用于并發比較低,同時數據安全性要求比較高的場景。

  總結:mysql的鎖機制和事務隔離級別有關。并不是說所有的讀操作都不加鎖,寫操作加鎖,加什么鎖也和索引類型、有無索引有關。

除非特別注明,雞啄米文章均為原創
轉載請標明本文地址:http://www.cpbsu.com/software/645.html
2016年10月14日
作者:雞啄米 分類:軟件開發 瀏覽: 評論:0