從 select ... for update來分析mysql的鎖

一 mysql的悲觀鎖 - 以行鎖作示例

每次拿數據的時候都認爲別的線程會修改數據,因此每次拿數據的時候都會給數據上鎖。上鎖以後,當別的線程想要拿數據時,就會阻塞。直到給數據上鎖的線程將事務提交或者回滾。傳統的關係數據庫裏面不少用了這種鎖機制,好比行鎖,表鎖,共享鎖,排他鎖等,都是在作操做以前先上鎖。mysql

下面的圖從網上粘的,用mysql的兩個視窗演示一下行鎖(左邊先執行)sql

 

(1) 左邊的線程,在事務中經過select for update語句給sid=1的數據行上了鎖,右邊的線程此時可使用select語句讀取數據,可是若是也使用select for update語句就會阻塞,使用update, add, delete也會阻塞。 而當左邊的線程將事務提交(或者回滾),右邊的線程就會獲取鎖,線程再也不阻塞數據庫

 

 

(2) 此時右邊的線程獲取鎖,左邊的線程執行此類操做,也會被阻塞。安全

 

 

(3) 固然,select都不行,update等寫操做也要阻塞等待。for update是排他鎖併發

 

 

二 mysql的鎖粒度分類

1 行級鎖

(1) 描述

行級鎖是mysql中鎖定粒度最細的一種鎖。表示只針對當前操做的行進行加鎖。行級鎖能大大減小數據庫操做的衝突,其加鎖粒度最小,但加鎖的開銷也最大。行級鎖分爲共享鎖和排他鎖oracle

(2)特色

開銷大,加鎖慢,會出現死鎖。發生鎖衝突的機率最低,併發度也最高。性能

 

2 表級鎖

(1) 描述

表級鎖是mysql中鎖定粒度最大的一種鎖,表示對當前操做的整張表加鎖,它實現簡單,資源消耗較少,被大部分mysql引擎支持。最常使用的MyISAM與InnoDB都支持表級鎖定。表級鎖定分爲表共享讀鎖(共享鎖)與表獨佔寫鎖(排他鎖)spa

(2)特色

開銷小,加鎖快,不會出現死鎖。發生鎖衝突的機率最高,併發度也最低。.net

 

3 頁級鎖

(1) 描述

頁級鎖是 MySQL 中鎖定粒度介於行級鎖和表級鎖中間的一種鎖。表級鎖速度快,但衝突多,行級衝突少,但速度慢。所以,採起了折衷的頁級鎖,一次鎖定相鄰的一組記錄。BDB 支持頁級鎖。線程

(2)特色

開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表鎖和行鎖之間,併發度通常。

 

 

 

三 鎖級別分類 - 共享鎖 & 排他鎖 & 意向鎖

1 共享鎖(Share Lock)

共享鎖又稱讀鎖,是讀取操做建立的鎖。其餘用戶能夠併發讀取數據,但任何事務都不能對數據進行修改(獲取數據上的排他鎖),直到已釋放全部共享鎖。

若是事務T對數據A加上共享鎖後,則其餘事務只能對A再加共享鎖,不能加排他鎖。獲准共享鎖的事務只能讀數據,不能修改數據。

用法

SELECT ... LOCK IN SHARE MODE;

在查詢語句後面增長LOCK IN SHARE MODE,MySQL 就會對查詢結果中的每行都加共享鎖,當沒有其餘線程對查詢結果集中的任何一行使用排他鎖時,能夠成功申請共享鎖,不然會被阻塞。其餘線程也能夠讀取使用了共享鎖的表,並且這些線程讀取的是同一個版本的數據。

2 排他鎖(Exclusive Lock)

排他鎖又稱寫鎖、獨佔鎖,若是事務T對數據A加上排他鎖後,則其餘事務不能再對A加任何類型的封鎖。獲准排他鎖的事務既能讀數據,又能修改數據。

用法

SELECT ... FOR UPDATE;

在查詢語句後面增長FOR UPDATE,MySQL 就會對查詢結果中的每行都加排他鎖,當沒有其餘線程對查詢結果集中的任何一行使用排他鎖時,能夠成功申請排他鎖,不然會被阻塞。

3 意向鎖(Intention Lock)

意向鎖是表級鎖,其設計目的主要是爲了在一個事務中揭示下一行將要被請求鎖的類型。InnoDB 中的兩個表鎖:

  • 意向共享鎖(IS):表示事務準備給數據行加入共享鎖,也就是說一個數據行加共享鎖前必須先取得該表的IS鎖;
  • 意向排他鎖(IX):相似上面,表示事務準備給數據行加入排他鎖,說明事務在一個數據行加排他鎖前必須先取得該表的IX鎖。

意向鎖是 InnoDB 自動加的,不須要用戶干預。

對於INSERTUPDATEDELETE,InnoDB 會自動給涉及的數據加排他鎖;對於通常的SELECT語句,InnoDB 不會加任何鎖,事務能夠經過如下語句顯式加共享鎖或排他鎖。

共享鎖:SELECT ... LOCK IN SHARE MODE;

排他鎖:SELECT ... FOR UPDATE;

 

 

四 InnoDB中的行鎖,表鎖,間隙鎖的定性分析

1 客觀鎖 vs 悲觀鎖場景分析

假設有一張商品表 goods,它包含 id,商品名稱,庫存量三個字段,表結構以下:

CREATE TABLE `goods` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) DEFAULT NULL,
  `stock` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_name` (`name`) USING HASH
) ENGINE=InnoDB 

插入以下數據:

INSERT INTO `goods` VALUES ('1', 'prod11', '1000');
INSERT INTO `goods` VALUES ('2', 'prod12', '1000');
INSERT INTO `goods` VALUES ('3', 'prod13', '1000');
INSERT INTO `goods` VALUES ('4', 'prod14', '1000');
...

假設有A、B兩個用戶同時各購買一件 id=1 的商品,用戶A獲取到的庫存量爲 1000,用戶B獲取到的庫存量也爲 1000,用戶A完成購買後修改該商品的庫存量爲 999,用戶B完成購買後修改該商品的庫存量爲 999,此時庫存量數據產生了不一致。

有兩種解決方案:

(1) 悲觀鎖方案:每次獲取商品時,對該商品加排他鎖。也就是在用戶A獲取獲取 id=1 的商品信息時對該行記錄加鎖,期間其餘用戶阻塞等待訪問該記錄。悲觀鎖適合寫入頻繁的場景。

begin;
select * from goods where id = 1 for update;
update goods set stock = stock - 1 where id = 1;
commit;

(2) 樂觀鎖方案:每次獲取商品時,不對該商品加鎖。在更新數據的時候須要比較程序中的庫存量與數據庫中的庫存量是否相等,若是相等則進行更新,反之程序從新獲取庫存量,再次進行比較,直到兩個庫存量的數值相等才進行數據更新。樂觀鎖適合讀取頻繁的場景。

#不加鎖獲取 id=1 的商品對象
select * from goods where id = 1

begin;
#更新 stock 值,這裏須要注意 where 條件 「stock = cur_stock」,只有程序中獲取到的庫存量與數據庫中的庫存量相等才執行更新
update goods set stock = stock - 1 where id = 1 and stock = cur_stock;
commit;

若是咱們須要設計一個商城系統,該選擇以上的哪一種方案呢?

查詢商品的頻率比下單支付的頻次高,基於以上可能會優先考慮第二種方案(這裏只考慮以上兩種方案的狀況下)。

 

2 行鎖定性分析

因爲InnoDB預設是Row-Level Lock,因此只有「明確」的指定主鍵,MySQL纔會執行Row lock  ,不然MySQL將會執行Table Lock. 

一、只根據主鍵進行查詢,而且查詢到數據,主鍵字段產生行鎖。

begin;
select * from goods where id = 1 for update;
commit;

 

二、只根據主鍵進行查詢,沒有查詢到數據,不產生鎖。

begin;
select * from goods where id = 1 for update;
commit;

 

三、根據主鍵、非主鍵索引(name)進行查詢,而且查詢到數據,主鍵字段產生行鎖,name字段產生行鎖。

begin;
select * from goods where id = 1 and name='prod11' for update;
commit;

 

四、根據主鍵、非主鍵含索引(name)進行查詢,沒有查詢到數據,不產生鎖。

begin;
select * from goods where id = 1 and name='prod12' for update;
commit;

 

五、根據非索引(stock)進行查詢,而且查詢到數據,stock字段產生表鎖。

begin;
select * from goods where stock='1000' for update;
commit;

 

六、根據非索引(stock)進行查詢,沒有查詢到數據,stock字段產生表鎖。

begin;
select * from goods where stock='9999' for update;
commit;

 

7. 只根據主鍵進行查詢,查詢條件爲模糊(不等於, like等),不管是否查詢到數據,主鍵字段產生表鎖。

begin;
select * from goods where id <> 1 for update;
commit;

 

 
 

3 行鎖,表鎖小結

InnoDb行鎖是經過給索引上的索引項加鎖來實現的,這一點mysql與oracle不一樣,後者是經過在數據塊中對相應數據行加鎖來實現的。InnoDB這種行鎖實現的特色意味着: 只有經過索引條件檢索數據,InnoDB才使用行級鎖,不然,InnoDB將使用表鎖。

在實際應用中,要特別注意 InnoDB 行鎖的這一特性,否則的話,可能致使大量的鎖衝突,從而影響併發性能。

  • 在不經過索引條件查詢的時候,InnoDB 確實使用的是表鎖,而不是行鎖。
  • 因爲 MySQL 的行鎖是針對索引加的鎖,不是針對記錄加的鎖,所以雖然是訪問不一樣行的記錄,可是若是是使用相同的索引鍵,是會出現鎖衝突的。應用設計的時候要注意這一點。
  • 當表有多個索引的時候,不一樣的事務可使用不一樣的索引鎖定不一樣的行,另外,不管是使用主鍵索引、惟一索引仍是普通索引,InnoDB 都會使用行鎖來對數據加鎖。
  • 即使在條件中使用了索引字段,可是否使用索引來檢索數據是由 MySQL 經過判斷不一樣的執行計劃的代價來決定的。若是 MySQL 認爲全表掃描效率更高,好比對一些很小的表,它就不會使用索引,這種狀況下 InnoDB 將使用表鎖,而不是行鎖。所以,在分析鎖衝突時,別忘了檢查 SQL 的執行計劃,以確認是否真正使用了索引。

 

4 間隙鎖

mysql innodb的間隙鎖定(next-key locking)是爲了防止幻讀。當mysql的隔離級別爲repeatable read時候會觸發間隙鎖定。

next-key的具體工做方式是:

(1) 選擇一個不存在的行,則鎖住全部的insert行爲

(2) 用範圍select,如select * from dual where id > 100, 會鎖住全部id > 100的insert行爲

 

InnoDB對索引記錄的鎖定也影響索引記錄以前的「間隙gap」 。若是一個用戶對索引記錄R加了一個共享或排他鎖定,那其餘用戶將不能在R以前當即插入新的記錄。這種間隙鎖定用於防止所謂的「phantom problem」。假設需讀取和鎖定表 CHILD 中標識符大於 100 的子行,並更新所搜索到的記錄中某些字段。

SELECT * FROM CHILD WHERE ID > 100 FOR UPDATE;

假設表 CHILD 中有一個索引字段 ID。咱們的查詢將從 ID 大於100的第一條記錄開始掃描索引記錄。 如今,假設加在索引記錄上的鎖定不能阻止在間隙處的插入,一個新的子記錄將可能在事務處理中被插入到表中。 若是如今在事務中再次執行

SELECT * FROM CHILD WHERE ID > 100 FOR UPDATE;

在查詢返回的記錄集中將會有一個新的子記錄。這與事務的隔離原則是相反的:一個事務應該可以運行,以便它已經讀的數據在事務過程當中不改變。若是咱們把一套行視爲數據項,新的「幽靈」子記錄可能會違反這一隔離原則。 

 

當InnoDB掃描一個索引之時,它也鎖定因此記錄中最後一個記錄以後的間隙。剛在前一個例子中發生:InnoDB設置的鎖定防止任何插入到id可能大過100的表。 

你能夠用next-key鎖定在你的應用程序上實現一個惟一性檢查:若是你以共享模式讀數據,而且沒有看到你將要插入的行的重複,則你能夠安全地插入你的行,而且知道在讀過程當中對你的行的繼承者設置的next-key鎖定與此同時阻止任何人對你的行插入一個重複。所以,the next-key鎖定容許你鎖住在你的表中並不存在的一些東西。

gap間隙鎖具體鎖定的範圍參看一下這篇   http://blog.itpub.net/30221425/viewspace-1787312/  個人理解是索引數據的先後間隙, 假設索引數據是m,範圍是 [mPre, mNext)

 

 

五 mysql的死鎖分析

1 死鎖分析

MyISAM 中是不會產生死鎖的,由於 MyISAM 老是一次性得到所需的所有鎖,要麼所有知足,要麼所有等待。而在 InnoDB 中,鎖是逐步得到的,就形成了死鎖的可能。

在 MySQL 中,行級鎖並非直接鎖記錄,而是鎖索引。索引分爲主鍵索引和非主鍵索引兩種,若是一條 SQL 語句操做了主鍵索引,MySQL 就會鎖定這條主鍵索引;若是一條 SQL 語句操做了非主鍵索引,MySQL 就會先鎖定該非主鍵索引,再鎖定相關的主鍵索引。 在進行UPDATEDELETE操做時,MySQL 不只鎖定WHERE條件掃描過的全部索引記錄,並且會鎖定相鄰的鍵值,即所謂的next-key locking.

當兩個事務同時執行,一個鎖住了主鍵索引,在等待其餘相關索引;另外一個鎖定了非主鍵索引,在等待主鍵索引。這樣就會發生死鎖。

發生死鎖後,InnoDB 通常均可以檢測到,並使一個事務釋放鎖回退,另外一個獲取鎖完成事務。 

2 避免死鎖

有多種方法能夠避免死鎖,這裏只介紹常見的三種:

  1. 若是不一樣程序會併發存取多個表,儘可能約定以相同的順序訪問表,能夠大大下降發生死鎖的可能性;
  2. 在同一個事務中,儘量作到一次鎖定所須要的全部資源,減小死鎖產生機率;
  3. 對於很是容易產生死鎖的業務部分,能夠嘗試使用升級鎖定顆粒度,經過表級鎖定來減小死鎖產生的機率。
相關文章
相關標籤/搜索