MySQL的事務和鎖

什麼是事務

事務:是數據庫操做的最小工做單元,是做爲單個邏輯工做單元執行的一系列操做;這些操做做爲一個總體一
起向系統提交,要麼都執行、要麼都不執行;事務是一組不可再分割的操做集合(工做邏輯單元);mysql

事務的簡單操做

顯式啓動事務語句,begin或者start transaction;sql

提交commit;數據庫

回滾rollback;緩存

SET AUTOCOMMIT=0 禁止自動提交安全

SET AUTOCOMMIT=1 開啓自動提交併發

事務的四大特性(ACID)

  • 原子性(Atomicity):事務一個事務(transaction)中的全部操做,要麼所有完成,要麼所有不完成,不會結束在中間某個環節。事務在執行過程當中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務歷來沒有執行過同樣。
  • 一致性(Consistency):一致性是指事務從一種一致性狀態轉變成另外一種一致性狀態。在事務開始以前和事務結束以後,數據庫的完整性約束沒有破壞。即:A和B一共5000元,不管雙方進行多少次轉帳,他們的總和都只能是5000元。或者說表中有一個字段爲name,爲惟一約束,即在表中姓名不能重複。若是一個事務對name字段進行了修改,可是事務提交或者事務操做發生回滾後,表中的姓名變得非惟一了,這就破壞了事務的一致性要求。所以,事務是一致性單元,若是事務中某個動做失敗了,系統能夠自動撤銷事務,返回初始狀態。
  • 隔離性(Ioslation):隔離性是當多個用戶併發訪問數據庫時,好比操做同一張表時,數據庫爲每個用戶開啓的事務,不能被其餘事務的操做所幹擾,多個併發事務之間要相互隔離。即該事務提交前對其餘事物都不可見。經過鎖或者MVCC實現,MVCC(多版本併發控制)在可重複讀的位置舉例介紹。
  • 持久性(Durability):持久性是指一個事務一旦被提交了,那麼對數據庫中的數據的改變就是永久性的,即使是在數據庫系統遇到故障的狀況下也不會丟失提交事務的操做。

事務的隔離性經過鎖和MVCC機制實現,原子性、一致性和持久性經過redo/undo log 來完成。redo log 稱爲重作日誌,用來保證事務的原子性和持久性。undo log 稱爲撤銷日誌,用來保證事務的一致性。mvc

redo log/undo log

redo log

redo log重作日誌,用來保證事務的原子性和持久性。由兩部分組成:一是內存中的重作日誌緩衝(redo log buffer),其是易失的;二是重作日誌文件,其是持久的。InnoDB存儲引擎當事務提交時,必須先將該事務的全部日誌寫入重作日誌進行持久化,待事務的commit操做完成纔算完成。當數據庫掛了以後,經過掃描redo日誌,就能找出那些沒有刷盤的數據頁(在崩潰以前可能數據頁僅僅在內存中修改了,可是還沒來得及寫盤),保證數據不丟。函數

因爲重作日誌文件打開並無使用O_DIRECT選項,所以重作日誌緩衝先寫入文件緩存系統。爲了確保每第二天志都寫入重作日誌文件,在每次將重作日誌緩衝寫入重作日誌後,InnoDB存儲引擎都須要調用一次fsync操做。高併發

O_DIRECT在執行磁盤IO時繞過緩衝區高速緩存,從用戶空間直接將數據傳遞到文件或磁盤設備,稱爲直接IO(direct IO)或者裸IO(raw IO)。性能

fsync函數的功能是確保文件全部已修改的內容已經正確同步到硬盤上,該調用會阻塞等待直到設備報告IO完成。

事務更新數據操做流程:

1.當事務執行更新數據的操做時,會先從mysql中讀取出數據到內存中,而後對內存中數據進行修改操做。

2.生成一條重作日誌並寫入redo log buffer,記錄的是數據被修改後的值。

3.按期將內存中修改的數據刷新到磁盤中,這是由innodb_flush_log_at_trx_commit決定的,重作日誌文件打開並無使用O_DIRECT選項,所以重作日誌緩衝先寫入文件緩存系統,最後經過執行fsync將數據寫入磁盤。

  • 當設置該值爲 1 時,每次事務提交都要作一次 fsync,這是最安全的配置,即便宕機也不會丟失事務;
  • 當設置爲 2 時,則在事務提交時只作 write 操做,只保證寫到文件系統的緩存,不進行fsync操做。所以mysql數據庫發生宕機而操做系統不發生宕機時不會丟失數據。操做系統宕機會丟失文件系統緩存中未刷新到重作日誌中的事務;
  • 當設置爲 0 時,事務提交不會觸發 redo 寫操做,而是留給後臺線程每秒一次的fsync操做,所以數據庫宕機將最多丟失一秒鐘內的事務。

4.commit提交後數據寫入redo log file中,而後將數據寫入到數據庫。

undo log

Undo log是InnoDB MVCC事務特性的重要組成部分。當咱們對記錄作了變動操做時就會產生undo記錄,Undo記錄默認被記錄到系統表空間(ibdata)中,但從5.6開始,也可使用獨立的Undo 表空間。
在Innodb當中,INSERT操做在事務提交前只對當前事務可見,Undo log在事務提交後即會被刪除,由於新插入的數據沒有歷史版本,因此無需維護Undo log。而對於UPDATE、DELETE,責須要維護多版本信息。 在InnoDB當中,UPDATE和DELETE操做產生的Undo log都屬於同一類型:update_undo。(update能夠視爲insert新數據到原位置,delete舊數據,undo log暫時保留舊數據)。

​ Session1(如下簡稱S1)和Session2(如下簡稱S2)同時訪問(不必定同時發起,但S1和S2事務有重疊)同一數據A,S1想要將數據A修改成數據B,S2想要讀取數據A的數據。沒有MVCC只能依賴加鎖了,誰擁有鎖誰先執行,另外一個等待。可是高併發下效率很低。InnoDB存儲引擎經過多版本控制的方式來讀取當前執行時間數據庫中行的數據,若是讀取的行正在執行DELETE或UPDATE操做,這是讀取操做不會所以等待行上鎖的釋放。相反的,InnoDB會去讀取行的一個快照數據(Undo log)。在InnoDB當中,要對一條數據進行處理,會先看這條數據的版本號是否大於自身事務版本(非RU隔離級別下當前事務發生以後的事務對當前事務來講是不可見的),若是大於,則從歷史快照(undo log鏈)中獲取舊版本數據,來保證數據一致性。而因爲歷史版本數據存放在undo頁當中,對數據修改所加的鎖對於undo頁沒有影響,因此不會影響用戶對歷史數據的讀,從而達到非一致性鎖定讀,提升併發性能。

另外,若是出現了錯誤或者用戶手動執行了rollback,系統能夠利用undo log中的備份將數據恢復到事務開始以前的狀態。與redo log不一樣的是,磁盤上不存在單獨的undo log 文件,他存放在數據庫內部的特殊段(segment)中。

事務的隔離等級

mysql默認的隔離等級是可重複讀,若是想要在mysql啓動時就修改mysql的隔離等級,須要修改配置文件,在[mysqld]中添加以下內容:

[mysqld]
transaction-isolation = READ-COMMITTED

若是想要查看當前事務隔離級別,可使用:

mysql>select @@tx_isolation\G;

髒讀:事務A讀取了事務B更新、未提交的數據,而後B回滾操做,那麼A讀取到的數據是髒數據(沒有用的數據)。

不可重複讀:事務 B 在事務A屢次讀取的過程當中,對數據做了更新操做並提交,致使事務A兩次讀取同一數據不一致。主要針對數據更新的。

幻讀:同一事務中,兩次按相同條件查詢到的記錄不同。形成幻讀的緣由在於事務處理沒有結束時,其餘事務對同一數據集合增長或刪除了記錄。在mysql中MVCC在必定程度上解決了幻讀,但並無徹底解決。以下:

事務在插入已經檢查過不存在的記錄時,插入失敗,出現衝突顯示這條數據已經存在了。好比:A查詢id爲2的數據不存在則插入一條id爲2的數據,在事務A查詢完畢後,事務B插入了一條id爲2的數據,並提交了。此時事務A向表中插入id爲2的數據插入失敗。第二次的insert其實也屬於隱式的讀取,只不過是在mysql的機制中讀取的,插入數據也是要先讀取一下有沒有主鍵衝突才能決定是否執行插入。錯誤提示以下:

Duplicate entry 2 for key 'id'  # 關鍵字id的重複條目2

注意:不可重複讀和幻讀很容易混淆,不可重複讀側重於修改,幻讀側重於新增或刪除。

讀未提交

最低級別,任何狀況都沒法保證,可能形成髒讀、幻讀、不可重複讀,效率最高,但最不安全。

讀提交

可避免髒讀的發生。

可重複讀

可避免髒讀、不可重複讀的發生。(mysql默認的級別)

可重複讀就是在一個事務內,對於同一個查詢請求,屢次執行,獲取到的數據集是同樣的。這通常是經過保存事務的快照實現的。MVCC會保存某個時間點上的快照,意味着事務能夠看到一個一致性的狀態。可是不一樣事務在同一個時間點上看到同一個表中的數據多是不一樣的。

串行化

可避免髒讀、不可重複讀、幻讀的發生。可是效率最低。事務的最高級別,在每一個讀的數據行上,加上鎖,使之不可能相互衝突。若是有事務對該數據行操做,那麼其餘事務就要等他結束才能進行操做。

MVCC

可重複讀使用的是一種叫MVCC的控制方式 ,即Mutil-Version Concurrency Control,多版本併發控制。InnoDB在每行記錄後面保存兩個隱藏的列來,分別保存了這個行的建立時間和行的刪除時間。這裏存儲的並非實際的時間值,而是系統版本號。每開啓一個新事務,事務的版本號就會遞增。因此增刪改查中對版本號的做用以下:

insert:把當前系統(事務)版本號做爲行記錄的版本號。

  • 建立一個事務,ID爲1,插入兩條數據。

    begin;
    insert into user(name) values('xiaoqi');
    insert into user(name) values('dada');
    commit;
    id name 建立時間(事務ID) 刪除時間(事務ID)
    1 xiaoqi 1 undefined
    2 dada 1 undefined

select:事務每次只能讀到行記錄的版本號小於等於這次系統版本號的記錄,這樣能夠確保事務讀取的行,要麼是在事務開始前已經存在的,要麼是事務自身插入或者修改過的。在事務自身執行過程當中,不會讀取到其餘事務進行的操做。

行的刪除版本要麼未定義,要麼大於當前事務版本號(這能夠確保事務讀取到的行,在事務開始以前未被刪除), 只有條件一、2同時知足的記錄,才能返回做爲查詢結果。

delete:把當前系統版本號做爲行記錄的刪除版本號。

  • 建立第二個事務,ID爲2,進行刪除處理。

    begin;
    select * from user;   # 執行事務2的第一步
    select * from user;   # 執行事務2的第二步
    commit;

假設1:在執行ID爲2的事務第一步的時候,有另外一個事務ID爲3往這個表裏插入了一條數據;

  • 建立第三個事務,ID爲3

    begin;
    insert into user(name) values('jianren');
    commit;

    表數據以下:

    id name 建立時間(事務ID) 刪除時間(事務ID)
    1 xiaoqi 1 undefined
    2 dada 1 undefined
    3 jianren 3 undefined

    而後,繼續執行ID爲2的事務的第二步,因爲id=3的數據的建立時間(事務ID爲3),執行當前事務的ID爲2,而InnoDB只會查找事務ID小於等於當前事務ID的數據行,因此id=3的數據行並不會在執行事務2中的第二步被檢索出來,在事務2中的兩條select 語句檢索出來的數據均以下表:

    id name 建立時間(事務ID) 刪除時間(事務ID)
    1 xiaoqi 1 undefined
    2 dada 1 undefined

假設2:在執行ID爲2的事務第一步以後,假設執行完ID爲3的事務後,接着又執行了ID爲4的事務。

  • 建立第四個事務,ID爲4

    begin; 
    delete from user where id=1; 
    commit;

    此時數據庫中表以下:

    id name 建立時間(事務ID) 刪除時間(事務ID)
    1 xiaoqi 1 4
    2 dada 1 undefined
    3 jianren 3 undefined

    而後執行ID爲2的事務的第二步。根據SELECT 檢索條件能夠知道,它會檢索建立時間(建立事務的ID)小於當前事務ID的行和刪除時間(刪除事務的ID)大於當前事務ID的行,而id=3的行上面已經說過,而id=1的行因爲刪除時間(刪除事務的ID)大於當前事務的ID,因此事務2的第二步select * from user;也會把id=1的數據檢索出來。最終,事務2中的兩條select 語句檢索出來的數據都以下:

    id name 建立時間(事務ID) 刪除時間(事務ID)
    1 xiaoqi 1 4
    2 dada 1 undefined

update:插入一條新記錄,並把當前事務版本號做爲行記錄的版本號,同時保存當前系統版本號到原有的行做爲刪除版本號。

假設3:假設在執行完事務2的第一步後,其它用戶執行了事務三、4,這時,又有一個用戶對這張表執行了UPDATE操做,建立了第五個事務,ID爲5。

  • 第五個事務,ID爲5

    begin; 
    update user set name='dazi' where id=2;
    commit;

    此時數據庫中表以下:

    id name 建立時間(事務ID) 刪除時間(事務ID)
    1 xiaoqi 1 4
    2 dada 1 5
    3 jianren 3 undefined
    2 dazi 5 undefined

    繼續執行事務2的第二步,根據select 語句的檢索條件,獲得下表:

    id name 建立時間(事務ID) 刪除時間(事務ID)
    1 xiaoqi 1 4
    2 dada 1 5

    仍是和事務2中第一步select 獲得相同的結果。

這就是mysql中事務的可重複讀,即一個事務執行後,不管其餘事務對其進行怎樣的修改讀到的數據都是同樣的,也就是能夠重複讀多少次結果都同樣。不過這就可能出現同一時刻兩個用戶讀到的數據不一樣。

快照讀和當前讀

  • 快照讀:讀取的是快照版本,也就是歷史版本
  • 當前讀:讀取的是最新版本
  • 普通的select就是快照讀,而update,delete,insert,select...LOCK In SHARE MODE(共享鎖),SELECT...for update(排他鎖)就是當前讀,其實執行update,delete,insert的時候也是先進行讀取數據,而後進行操做。

InnoDB存儲引擎中的鎖

鎖的類型

共享行鎖

共享鎖數據行鎖,容許不一樣事務共享加鎖讀取,但不容許其它事務修改或者加入排他鎖。若是有修改必須等待一個事務提交完成,才能夠執行,容易出現死鎖

例如:事務1已經得到了行r的共享鎖,那麼另外的事務2能夠當即得到行r的共享鎖,由於讀取並無改變行r的數據,稱這種狀況爲鎖兼容。

select * from user where id = 1 lock in share mode;  # 共享鎖的寫法

排他行鎖

排他鎖屬於行鎖,當一個事物加入排他鎖後,不容許其餘事務加共享鎖或者排它鎖讀取,更加不容許其餘事務修改加鎖的數據行。

例如:事務1已經得到了行r的共享鎖,如有事務3想得到行r的排他鎖,則必須等事務1釋放行r上的共享鎖,這種狀況稱爲鎖不兼容。

select * from user where id = 1 for update;  # 排他鎖的寫法

InnoDB存儲引擎支持多粒度鎖定,這種鎖定容許事務在行級上的鎖和表級上的鎖同時存在。爲了支持在不一樣粒度上進行加鎖操做,InnoDB存儲引擎還提供了一種額外的加鎖方式,稱爲意向鎖,意向排他鎖是表級別的鎖。意向鎖是將鎖定的對象分爲多個層次,意向鎖意味着事務但願在更細粒度上進行加鎖。

意向鎖

意向鎖的做用:意圖鎖是表級鎖,指示事務稍後須要對錶中的行使用哪一種類型的鎖(共享或獨佔)。意向鎖意味着事務但願在更細粒度上進行加鎖。

爲何沒有意向鎖,表鎖和行鎖不能共存?

假設事務A寫鎖鎖住了某一行,其餘事務就不可能修改這一行。這與」事務B鎖住整個表就能修改表中的任意一行「造成了衝突。因此,沒有意向鎖的時候,行鎖與表鎖共存就會存在問題!

有了意向鎖以後,前面例子中的事務A在申請行鎖(寫鎖)以前,數據庫會自動先給事務A申請表的意向排他鎖。當事務B去申請表的寫鎖時就會失敗,由於表上有意向排他鎖以後事務B申請表的寫鎖時會被阻塞。

若將上鎖的對象當作一棵樹,那麼對最下層的對象上鎖,也就是對最細粒度的對象進行上鎖,那麼首先須要對粗粒度的對象上鎖。如上圖,若是對頁上的記錄上排他鎖,那麼分別須要對數據庫A、表、頁上意向鎖,最後對記錄上排他鎖。若其中任何一個部分致使等待,那麼該操做須要等待粗粒度鎖的完成。這其實也是爲何Myisam數據庫引擎的查詢速度更優於InnoDB數據庫引擎的緣由,鎖加的多了天然就慢了。

InooDB存儲引擎意向鎖的設計目的主要在於爲了在一個事務中解釋下一行即將被請求的鎖類型。其支持兩種意向鎖:

意向共享鎖(IS Lock):事務在獲取表中某行上的共享鎖以前,必須先獲取表上的IS鎖。

意向排他鎖(IX Lock):事務在獲取表中某行的排他鎖以前,必須先獲取該表的IX鎖。

表級鎖類型的兼容性彙總在下表

意向共享鎖 意向排他鎖 共享表鎖 排他表鎖
意向共享鎖 兼容 兼容 兼容 不兼容
意向排他鎖 兼容 兼容 不兼容 不兼容
共享表鎖 兼容 不兼容 兼容 不兼容
排他表鎖 不兼容 不兼容 不兼容 不兼容

記錄鎖(Record Lock)

記錄鎖定是對索引記錄的鎖定。例如, SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE; 能夠防止從插入,更新或刪除行,其中的值的任何其它交易t.c110

記錄鎖定始終鎖定索引記錄,即便沒有定義索引的表也是如此。對於這種狀況,請 InnoDB建立一個隱藏的彙集索引,並將該索引用於記錄鎖定。

間隙鎖(Gap Lock)

間隙鎖定是對索引記錄之間的間隙的鎖定,或者是對第一個或最後一個索引記錄以前的間隙的鎖定,鎖定一個範圍,但不包含記錄自己。例如,SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;阻止其餘事務將value 15插入column中t.c1,不管該列 中是否已經存在該值,由於範圍內全部現有值之間的間隙都被鎖定。

間隙可能跨越單個索引值,多個索引值,甚至爲空。

對於使用惟一索引來鎖定惟一行來鎖定行的語句,不須要間隙鎖定。(這不包括搜索條件僅包含多列惟一索引的某些列的狀況;在這種狀況下,會發生間隙鎖定。)例如,若是該id列具備惟一索引,則如下語句僅使用一個索引記錄鎖定id值爲100 的行,其餘會話是否在前面的間隙中插入行都沒有關係:

SELECT * FROM child WHERE id = 100;

若是id未創建索引或索引不惟一,則該語句會鎖定前面的間隙。

間隙鎖能夠共存。一個事務進行的間隙鎖定不會阻止另外一事務對相同的間隙進行間隙鎖定。共享和專用間隙鎖之間沒有區別。它們彼此不衝突,而且執行相同的功能。

Next-Key Lock

是Record Lock+Gap Lock的組合,鎖定一個記錄範圍,並鎖定記錄自己。

例如一個索引有10,11,13和20這四個值,那麼該索引可能被鎖定的區間爲:

(-∞,10]
(10,11]
(11,13]
(13,20]
(20,+∞)

若是查詢的索引是彙集索引時,InnoDB存儲引擎會對Next-Key Lock進行優化,將其降級爲Record Lock,即僅鎖住索引自己,而不是範圍。

create table t(a int primary key);
insert into t values(1);
insert into t values(2);
insert into t values(5);

接着執行sql

建立事務A

begin;
select * from t where a=5 for update;
commit;

若是在事務A提交以前,建立事務B並插入一條數據,且提交。

# 事務B
begin;
insert into t values(4);
commit;  # 成功不須要阻塞

表t中有1,2,5三個值,在事務A對a = 5進行排他鎖鎖定。而因爲a是主鍵且惟一,所以鎖定的僅是5這個值,而不是(2,5)這個範圍,這樣事務B插入值4時能夠當即插入,不會阻塞。Next-Key Lock降級爲Record Lock,從而提升併發性。

如果輔助索引,即則會阻塞等待上一事務提交後才能繼續執行。好比:一個表a中有輔助索引四個值m爲一、二、四、6,一個輔助索引使用Next-Key Lock鎖定條件是4,代碼以下,那麼他鎖定的範圍是(2,4),但特別注意的是,InnoDB存儲引擎還會對輔助索引下一個鍵值加上gap lock,即還有一個輔助索引範圍爲(4,6)的鎖。所以若執行插入m=5的sql語句會被阻塞。

select * from a where m=4 for update;

經過Next-Key Lock能夠避免幻讀的出現。

相關文章
相關標籤/搜索