深刻理解Mysql——鎖、事務與併發控制

本文對鎖、事務、併發控制作一個總結,看了網上不少文章,描述很是不許確。若有與您觀點不一致,歡迎有理有據的拍磚!html

mysql服務器邏輯架構

這裏寫圖片描述

每一個鏈接都會在mysql服務端產生一個線程(內部經過線程池管理線程),好比一個select語句進入,mysql首先會在查詢緩存中查找是否緩存了這個select的結果集,若是沒有則繼續執行 解析、優化、執行的過程;不然會之間從緩存中獲取結果集。mysql

mysql併發控制——共享鎖、排他鎖

共享鎖

共享鎖也稱爲讀鎖,讀鎖容許多個鏈接能夠同一時刻併發的讀取同一資源,互不干擾;算法

排他鎖

排他鎖也稱爲寫鎖,一個寫鎖會阻塞其餘的寫鎖或讀鎖,保證同一時刻只有一個鏈接能夠寫入數據,同時防止其餘用戶對這個數據的讀寫。sql

鎖策略

鎖的開銷是較爲昂貴的,鎖策略其實就是保證了線程安全的同時獲取最大的性能之間的平衡策略。數據庫

  • mysql鎖策略:talbe lock(表鎖)

表鎖是mysql最基本的鎖策略,也是開銷最小的鎖,它會鎖定整個表;緩存

具體狀況是:若一個用戶正在執行寫操做,會獲取排他的「寫鎖」,這可能會鎖定整個表,阻塞其餘用戶的讀、寫操做;安全

若一個用戶正在執行讀操做,會先獲取共享鎖「讀鎖」,這個鎖運行其餘讀鎖併發的對這個表進行讀取,互不干擾。只要沒有寫鎖的進入,讀鎖能夠是併發讀取統一資源的。服務器

一般發生在DDL語句\DML不走索引的語句中,好比這個DML update table set columnA=」A」 where columnB=「B」. 
若是columnB字段不存在索引(或者不是組合索引前綴),會鎖住全部記錄也就是鎖表。若是語句的執行可以執行一個columnB字段的索引,那麼會鎖住知足where的行(行鎖)。session

  • mysql鎖策略:row lock(行鎖)

行鎖能夠最大限度的支持併發處理,固然也帶來了最大開銷,顧名思義,行鎖的粒度實在每一條行數據。架構

事務

事務就是一組原子性的sql,或者說一個獨立的工做單元。 
事務就是說,要麼mysql引擎會所有執行這一組sql語句,要麼所有都不執行(好比其中一條語句失敗的話)。

好比,tim要給bill轉帳100塊錢: 
1.檢查tim的帳戶餘額是否大於100塊; 
2.tim的帳戶減小100塊; 
3.bill的帳戶增長100塊; 
這三個操做就是一個事務,必須打包執行,要麼所有成功,要麼所有不執行,其中任何一個操做的失敗都會致使全部三個操做「不執行」——回滾。

CREATE DATABASE IF NOT EXISTS employees;
USE employees;

CREATE TABLE `employees`.`account` (
  `id` BIGINT (11) NOT NULL AUTO_INCREMENT,
  `p_name` VARCHAR (4),
  `p_money` DECIMAL (10, 2) NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`)
) ;
INSERT INTO `employees`.`account` (`id`, `p_name`, `p_money`) VALUES ('1', 'tim', '200'); 
INSERT INTO `employees`.`account` (`id`, `p_name`, `p_money`) VALUES ('2', 'bill', '200'); 

START TRANSACTION;
SELECT p_money FROM account WHERE p_name="tim";-- step1
UPDATE account SET p_money=p_money-100 WHERE p_name="tim";-- step2
UPDATE account SET p_money=p_money+100 WHERE p_name="bill";-- step3
COMMIT;

一個良好的事務系統,必須知足ACID特色:

事務的ACID

  • A:atomiciy原子性 
    一個事務必須保證其中的操做要麼所有執行,要麼所有回滾,不可能存在只執行了一部分這種狀況出現。

  • C:consistency一致性 
    數據必須保證從一種一致性的狀態轉換爲另外一種一致性狀態。 
    好比上一個事務中執行了第二步時系統崩潰了,數據也不會出現bill的帳戶少了100塊,可是tim的帳戶沒變的狀況。要麼維持原裝(所有回滾),要麼bill少了100塊同時tim多了100塊,只有這兩種一致性狀態的

  • I:isolation隔離性 
    在一個事務未執行完畢時,一般會保證其餘Session 沒法看到這個事務的執行結果

  • D:durability持久性 
    事務一旦commit,則數據就會保存下來,即便提交完以後系統崩潰,數據也不會丟失。

隔離級別

這裏寫圖片描述

查看系統隔離級別:
select @@global.tx_isolation;
查看當前會話隔離級別
select @@tx_isolation;
設置當前會話隔離級別
SET session TRANSACTION ISOLATION LEVEL serializable;
設置全局系統隔離級別
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

READ UNCOMMITTED(未提交讀,可髒讀)

事務中的修改,即便沒有提交,對其餘會話也是可見的。 
能夠讀取未提交的數據——髒讀。髒讀會致使不少問題,通常不適用這個隔離級別。 
實例:

-- ------------------------- read-uncommitted實例 ------------------------------
-- 設置全局系統隔離級別
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- Session A
START TRANSACTION;
SELECT * FROM USER;
UPDATE USER SET NAME="READ UNCOMMITTED";
-- commit;

-- Session B
SELECT * FROM USER;

//SessionB Console 能夠看到Session A未提交的事物處理,在另外一個Session 中也看到了,這就是所謂的髒讀
id  name
2   READ UNCOMMITTED
34  READ UNCOMMITTED

READ COMMITTED(提交讀或不可重複讀,幻讀)

通常數據庫都默認使用這個隔離級別(mysql不是),這個隔離級別保證了一個事務若是沒有徹底成功(commit執行完),事務中的操做對其餘會話是不可見的

-- ------------------------- read-cmmitted實例 ------------------------------
-- 設置全局系統隔離級別
SET GLOBAL TRANSACTION ISOLATION LEVEL READ  COMMITTED;
-- Session A
START TRANSACTION;
SELECT * FROM USER;
UPDATE USER SET NAME="READ COMMITTED";
-- COMMIT;

-- Session B
SELECT * FROM USER;

//Console OUTPUT:
id  name
2   READ UNCOMMITTED
34  READ UNCOMMITTED


---------------------------------------------------
-- 當 Session  A執行了commit,Session B獲得以下結果:
id  name
2   READ COMMITTED
34  READ COMMITTED

也就驗證了read committed級別在事物未完成commit操做以前修改的數據對其餘Session 不可見,執行了commit以後纔會對其餘Session 可見。 
咱們能夠看到Session B兩次查詢獲得了不一樣的數據。

read committed隔離級別解決了髒讀的問題,可是會對其餘Session 產生兩次不一致的讀取結果(由於另外一個Session 執行了事務,一致性變化)。

REPEATABLE READ(可重複讀)

一個事務中屢次執行統一讀SQL,返回結果同樣。 
這個隔離級別解決了髒讀的問題,幻讀問題。這裏指的是innodb的rr級別,innodb中使用next-key鎖對」當前讀」進行加鎖,鎖住行以及可能產生幻讀的插入位置,阻止新的數據插入產生幻行。 
下文中詳細分析。

具體請參考mysql手冊

https://dev.mysql.com/doc/refman/5.7/en/innodb-storage-engine.html

SERIALIZABLE(可串行化)

最強的隔離級別,經過給事務中每次讀取的行加鎖,寫加寫鎖,保證不產生幻讀問題,可是會致使大量超時以及鎖爭用問題。

多版本併發控制-MVCC

MVCC(multiple-version-concurrency-control)是個行級鎖的變種,它在普通讀狀況下避免了加鎖操做,所以開銷更低。 
雖然實現不一樣,但一般都是實現非阻塞讀,對於寫操做只鎖定必要的行

  • 一致性讀 (就是讀取快照) 
    select * from table ….;
  • 當前讀(就是讀取實際的持久化的數據) 
    特殊的讀操做,插入/更新/刪除操做,屬於當前讀,處理的都是當前的數據,須要加鎖。 
    select * from table where ? lock in share mode; 
    select * from table where ? for update; 
    insert; 
    update ; 
    delete;

注意:select …… from where…… (沒有額外加鎖後綴)使用MVCC,保證了讀快照(mysql稱爲consistent read),所謂一致性讀或者讀快照就是讀取當前事務開始以前的數據快照,在這個事務開始以後的更新不會被讀到。詳細狀況下文select的詳述。

對於加鎖讀SELECT with FOR UPDATE(排他鎖) or LOCK IN SHARE MODE(共享鎖)、update、delete語句,要考慮是不是惟一索引的等值查詢。

寫鎖-recordLock,gapLock,next key lock

對於使用到惟一索引 等值查詢:好比,where columnA=」…」 ,若是columnA上的索引被使用到, 
那麼會在知足where的記錄上加行鎖(for update是排他鎖,lock in shared 是共享鎖,其餘寫操做加排他鎖)。這裏是行級鎖,record lock。

對於範圍查詢(使用非惟一的索引): 
好比(作範圍查詢):where columnA between 10 and 30 ,會致使其餘會話中10之後的數據都沒法插入(next key lock),從而解決了幻讀問題。

這裏是next key lock 會包括涉及到的全部行。 
next key lock=recordLock+gapLock,不只鎖住相關數據,並且鎖住邊界,從而完全避免幻讀可點擊查看這篇推薦文章

對於沒有索引 
鎖表 
一般發生在DDL語句\DML不走索引的語句中,好比這個DML update table set columnA=」A」 where columnB=「B」. 
若是columnB字段不存在索引(或者不是組合索引前綴),會鎖住全部記錄也就是鎖表。若是語句的執行可以執行一個columnB字段的索引,那麼會鎖住知足where的行(行鎖)。

INNODB的MVCC一般是經過在每行數據後邊保存兩個隱藏的列來實現(實際上是三列,第三列是用於事務回滾,此處略去), 
一個保存了行的建立版本號,另外一個保存了行的更新版本號(上一次被更新數據的版本號) 
這個版本號是每一個事務的版本號,遞增的。

這樣保證了innodb對讀操做不須要加鎖也能保證正確讀取數據。

MVCC select無鎖操做 與 維護版本號

下邊在mysql默認的Repeatable Read隔離級別下,具體看看MVCC操做:

  • Select(快照讀,所謂讀快照就是讀取當前事務以前的數據。): 
    a.InnoDB只select查找版本號早於當前版本號的數據行,這樣保證了讀取的數據要麼是在這個事務開始以前就已經commit了的(早於當前版本號),要麼是在這個事務自身中執行建立操做的數據(等於當前版本號)。

    b.查找行的更新版本號要麼未定義,要麼大於當前的版本號(爲了保證事務能夠讀到老數據),這樣保證了事務讀取到在當前事務開始以後未被更新的數據。 
    注意: 這裏的select不能有for update、lock in share 語句。 
    總之要只返回知足如下條件的行數據,達到了快照讀的效果:

(行建立版本號< =當前版本號 && (行更新版本號==null or 行更新版本號>當前版本號 ) )
  •  
  • Insert

    InnoDB爲這個事務中新插入的行,保存當前事務版本號的行做爲行的行建立版本號。

  • Delete 
    InnoDB爲每個刪除的行保存當前事務版本號,做爲行的刪除標記。

  • Update

    將存在兩條數據,保持當前版本號做爲更新後的數據的新增版本號,同時保存當前版本號做爲老數據行的更新版本號。

當前版本號—寫—>新數據行建立版本號 && 當前版本號—寫—>老數據更新版本號();
  •  

髒讀 vs 幻讀 vs 不可重複讀

髒讀一事務未提交的中間狀態的更新數據 被其餘會話讀取到。 當一個事務正在訪問數據,而且對數據進行了修改,而這種修改尚未 提交到數據庫中(commit未執行),這時,另外會話也訪問這個數據,由於這個數據是尚未提交, 那麼另一個會話讀到的這個數據是髒數據,依據髒數據所作的操做也多是不正確的。

不可重複讀簡單來講就是在一個事務中讀取的數據可能產生變化,ReadCommitted也稱爲不可重複讀

在同一事務中,屢次讀取同一數據返回的結果有所不一樣。換句話說就是,後續讀取能夠讀到另外一會話事務已提交的更新數據。 相反,「可重複讀」在同一事務中屢次讀取數據時,可以保證所讀數據同樣,也就是,後續讀取不能讀到另外一會話事務已提交的更新數據。

幻讀:會話T1事務中執行一次查詢,而後會話T2新插入一行記錄,這行記錄剛好能夠知足T1所使用的查詢的條件。而後T1又使用相同 的查詢再次對錶進行檢索,可是此時卻看到了事務T2剛纔插入的新行。這個新行就稱爲「幻像」,由於對T1來講這一行就像忽然 出現的同樣。 
innoDB的RR級別沒法作到徹底避免幻讀,下文詳細分析。

----------------------------------前置準備----------------------------------------
prerequisite:
-- 建立表
mysql>
CREATE TABLE `t_bitfly` (
   `id` bigint(20) NOT NULL DEFAULT '0',
   `value` varchar(32) DEFAULT NULL,
   PRIMARY KEY (`id`)
 )

-- 確保當前隔離級別爲默認的RR級別

mysql> select @@global.tx_isolation, @@tx_isolation;
+-----------------------+-----------------+
| @@global.tx_isolation | @@tx_isolation  |
+-----------------------+-----------------+
| REPEATABLE-READ       | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.00 sec)
---------------------------------------開始--------------------------------------------- 


session A                                           |   session B
                                                    |
                                                    |
mysql> START TRANSACTION;                           |   mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)                |   Query OK, 0 rows affected (0.00 sec)                                        
                                                    |   
                                                    |
mysql> SELECT * FROM test.t_bitfly;                 |   mysql> SELECT * FROM test.t_bitfly; 
Empty set (0.00 sec)                                |   Empty set (0.00 sec)
                                                    |
                                                    |   mysql> INSERT INTO t_bitfly VALUES (1, 'test');
                                                    |   Query OK, 1 row affected (0.00 sec)
                                                    |
                                                    |
mysql> SELECT * FROM test.t_bitfly;                 |
Empty set (0.00 sec)                                |
                                                    |
                                                    |   mysql> commit;
                                                    |   Query OK, 0 rows affected (0.01 sec)                                                
mysql> SELECT * FROM test.t_bitfly;                 |
Empty set (0.00 sec)                                |
-- 能夠看到雖然兩次執行結果返回的數據一致,         |
-- 可是不能說明沒有幻讀。接着看:                   |
                                                    |
mysql> INSERT INTO t_bitfly VALUES (1, 'test');     |
ERROR 1062 (23000):                                 |
Duplicate entry '1' for key 'PRIMARY'               |
                                                    |
-- 明明爲空的表,爲何說主鍵重複?——幻讀出現 !!!       |

如何保證rr級別絕對不產生幻讀?

在使用的select …where語句中加入 for update(排他鎖) 或者 lock in share mode(共享鎖)語句來實現。其實就是鎖住了可能形成幻讀的數據,阻止數據的寫入操做。

實際上是由於數據的寫入操做(insert 、update)須要先獲取寫鎖,因爲可能產生幻讀的部分,已經獲取到了某種鎖,因此要在另一個會話中獲取寫鎖的前提是當前會話中釋放全部因加鎖語句產生的鎖。

mysql死鎖問題

死鎖,就是產生了循環等待鏈條,我等待你的資源,你卻等待個人資源,咱們都相互等待,誰也不釋放本身佔有的資源,致使無線等待下去。 
好比:

//Session A
START TRANSACTION;
UPDATE account SET p_money=p_money-100 WHERE p_name="tim";
UPDATE account SET p_money=p_money+100 WHERE p_name="bill";
COMMIT;
//Thread B
START TRANSACTION;
UPDATE account SET p_money=p_money+100 WHERE p_name="bill";
UPDATE account SET p_money=p_money-100 WHERE p_name="tim";
COMMIT;

當線程A執行到第一條語句UPDATE account SET p_money=p_money-100 WHERE p_name=」tim」;鎖定了p_name=」tim」的行數據;而且試圖獲取p_name=」bill」的數據;

,此時,剛好,線程B也執行到第一條語句:UPDATE account SET p_money=p_money+100 WHERE p_name=」bill」;

鎖定了 p_name=」bill」的數據,同時試圖獲取p_name=」tim」的數據; 
此時,兩個線程就進入了死鎖,誰也沒法獲取本身想要獲取的資源,進入無線等待中,直到超時!

innodb_lock_wait_timeout 等待鎖超時回滾事務: 
直觀方法是在兩個事務相互等待時,當一個等待時間超過設置的某一閥值時,對其中一個事務進行回滾,另外一個事務就能繼續執行。這種方法簡單有效,在innodb中,參數innodb_lock_wait_timeout用來設置超時時間。

wait-for graph算法來主動進行死鎖檢測: 
innodb還提供了wait-for graph算法來主動進行死鎖檢測,每當加鎖請求沒法當即知足須要並進入等待時,wait-for graph算法都會被觸發。

如何儘量避免死鎖

1)以固定的順序訪問表和行。好比兩個更新數據的事務,事務A 更新數據的順序 爲1,2;事務B更新數據的順序爲2,1。這樣更可能會形成死鎖。

2)大事務拆小。大事務更傾向於死鎖,若是業務容許,將大事務拆小。

3)在同一個事務中,儘量作到一次鎖定所須要的全部資源,減小死鎖機率。

4)下降隔離級別。若是業務容許,將隔離級別調低也是較好的選擇,好比將隔離級別從RR調整爲RC,能夠避免掉不少由於gap鎖形成的死鎖。

5)爲表添加合理的索引。能夠看到若是不走索引將會爲表的每一行記錄添加上鎖,死鎖的機率大大增大。

顯式鎖 與 隱式鎖 
隱式鎖:咱們上文說的鎖都屬於不須要額外語句加鎖的隱式鎖。 
顯示鎖

SELECT ... LOCK IN SHARE MODE(加共享鎖);
SELECT ... FOR UPDATE(加排他鎖);

詳情上文已經說過。

經過以下sql能夠查看等待鎖的狀況

select * from information_schema.innodb_trx where trx_state="lock wait";
或
show engine innodb status;

mysql中的事務

show variables like "autocommit";

set autocommit=0; //0表示AutoCommit關閉
set autocommit=1; //1表示AutoCommit開啓
  • 自動提交(AutoCommit,mysql默認)

mysql默認採用AutoCommit模式,也就是每一個sql都是一個事務,並不須要顯示的執行事務。  若是autoCommit關閉,那麼每一個sql都默認開啓一個事務,只有顯式的執行「commit」後這個事務纔會被提交。

相關文章
相關標籤/搜索