初識innoDB
InnoDB:支持事務安全的引擎,支持外鍵、行鎖、事務是他的最大特點。如果有大量的update和insert,建議使用InnoDB,特別是針對多個併發和QPS較高的情況 , 所以innoDB適用於大數據量 , 高併發的互聯網業務.
行鎖,細粒度,在數據量大,併發量高時,性能比較優異
事務,提供了commit,rollback,崩潰修復等能力,對數據一致性幫助很大
innoDB的隔離級別
ACID 事務管理
在講innoDB的隔離級別前我們需要先了解下什麼是隔離 , 隔離就是ACID中的I , 簡單說ACID就是事務擁有的特性.
原子性(Atomicity
原子性是指事務是一個不可分割的工作單位,事務中的操作要麼都發生,要麼都不發生。
一致性(Consistency)
事務前後數據的完整性必須保持一致。
隔離性(Isolation
事務的隔離性是多個用戶併發訪問數據庫時,數據庫爲每一個用戶開啓的事務,不能被其他事務的操作數據所幹擾,多個併發事務之間要相互隔離。
持久性(Durability)
持久性是指一個事務一旦被提交,它對數據庫中數據的改變就是永久性的,接下來即使數據庫發生故障也不應該對其有任何影響
隔離級別
innoDB的隔離級別分爲4級 , 默認的是可重複讀
隔離級別(隔離程度由低到高) |
概述 |
問題 |
未提交讀(READUNCOMMITTED) |
當前事務可以讀取其他事務未提交的數據 |
髒讀 , 不可重複讀 , 幻讀 , 丟失更新 |
提交讀(READCOMMITTED) |
當前事務讀取到的數據一定是其他事務已提交的數據 |
不可重複讀 , 幻讀 , 丟失更新 |
可重複讀(REPEATABLEREAD) |
在同一個事務裏,SELECT的結果是事務開始時時間點的狀態 |
幻讀(innoDB通過MVVC已經解決了幻讀 , 但是其實還是可以進行update和delete操作的) , 丟失更新 |
串行化(SERIALIZABLE) |
讀操作會隱式獲取共享鎖,可以保證不同事務間的互斥 |
解決所有事務相關問題 , 但是由於事務串行執行, 資源消耗大 |
什麼是髒讀,不可重複讀,幻讀
鎖問題 |
鎖問題描述 |
會出現鎖問題的隔離級別 |
解決辦法 |
髒讀 |
一個事務中會讀到其他併發事務未提交的數據,違反了事務的隔離性; |
Read Uncommitted |
提高事務隔離級別至Read Committed及以上; |
不可重複讀 |
一個事務會讀到其他併發事務已提交的數據,違反了數據庫的一致性要求;可能出現的問題爲幻讀.(與幻讀的區別在於, 這個更加強調的是數據的一致性 , 比如某條數據的列值是否被更改) |
Read Uncommitted、Read Committed |
默認的RR隔離級別下 ,解決辦法分爲兩種情況:1、當前讀:Next-Key Lock機制對相關索引記錄及索引間隙加鎖,防止併發事務修改數據或插入新數據到間隙;2、版本讀:MVVC,保證事務執行過程中只有第一次讀之前提交的修改和自己的修改可見,其他的均不可見;提高事務隔離級別至Serializable; |
幻讀 |
幻讀是指在同一事務下,連續執行兩次同樣的SQL語句可能導致不同的結果,第二次的SQL語句可能返回之前不存在的行記錄;(幻讀相對於不可重複讀的區別在於幻讀的關注點在於insert新插入數據的影響) |
Read Uncommitted、Read Committed、Repeatable Read |
innoDB原生實現中採用了MVVC解決了幻讀問題, 但準確的說是解決了部分 , 下面的模塊會進行簡單的介紹 . |
丟失更新 |
一個事務的更新被另一個事務覆蓋 |
Read Uncommitted、Read Committed、Repeatable Read |
默認的RR隔離級別下 ,解決辦法分爲兩種情況:1、樂觀鎖:數據表增加version字段,讀取數據時記錄原始version,更新數據時,比對version是否爲原始version,如不等,則證明有併發事務已更新過此行數據,則可回滾事務後重試直至無併發競爭;2、悲觀鎖:讀加排他鎖,保證整個事務執行過程中,其他併發事務無法讀取相關記錄,直至當前事務提交或回滾釋放鎖; |
舉個栗子
事務一 |
事務二 |
begin; |
begin; |
select * from test_lock where id=5; |
|
update test_lock set code=551 where id=5; |
|
select * from test_lock where id=5; (髒讀) |
|
commit; |
|
select * from test_lock where id=5; (不可重複讀) |
|
commit; |
事務一 |
事務二 |
begin; |
begin; |
select * from test_lock where id>3; |
|
insert test_lock (code,descr) values(4,44); |
|
commit; |
|
select * from test_lock where id>3; (幻讀) |
|
commit; |
丟失更新
事務一 |
事務二 |
begin; |
|
begin; |
|
select * from test_lock where id=5; |
|
select * from test_lock where id=5; |
|
update test_lock set num=6 where id=5; |
|
update test_lock set num=7 where id=5; |
|
commit; |
|
commit; |
InnoDB RR下的實現與幻讀的解決
innoDB默認的隔離級別就是可重複讀 , 常規RR級別下時候是有幻讀問題的 , InnoDB通過MVVC實現了可重複讀並解決了幻讀問題(部分解決)
MVVC概述
MVCC全稱Mutli Version Concurreny Control,多版本併發控制,也可稱之爲一致性非鎖定讀;它通過行的多版本控制方式來讀取當前執行時間數據庫中的行數據。實質上使用的是快照數據,這樣就可以實現不加鎖讀。MVCC 主要應用於 Read Commited 和 Repeatable read 兩個事務隔離級別 , mvcc是一種能夠進一步提高併發的方法 , 本文僅對其進行簡要介紹.
MVVC核心原理
(1)寫任務發生時,將數據克隆一份,以版本號區分;
(2)寫任務操作新克隆的數據,直至提交;
(3)併發讀任務可以繼續讀取舊版本的數據,不至於阻塞通過mvvc機制innoDB實現了可重複讀 , 並解決了由insert帶來的幻讀問題 , 但是innoDB是如何確定快照版本的呢 ?
ReadView確定快照版本
在innodb中,創建一個新事務的時候 , innodb會將當前系統中的活躍事務列表創建一個副本(read view),副本中保存的是系統當前不應該被本事務看到的其他事務id列表 . 當用戶在這個事務中要讀取該行記錄的時候,innodb會將該行當前的版本號與該read view進行比較.
ReadView生成時機:
RR下,事務在第一個Read操作時,會建立Read View
RC下,事務在每次Read操作時,都會建立Read View
確定快照的算法思路:
當數據(每行爲一單位)發生變動時 , 數據版本會更新操作的事務id
假設當前事務的id 爲 x ReadView中最小的事務id爲Min 最大的事務id爲Max 當前行的事務id爲now
Now < Min 說明當前行對應的事務完成時間是小於當前事務開始時間的 , 此行數據可見
Now > Max 說明當前行對應事務完成時間是晚於當前事務開始時間的 , 數據不可見 , 尋找上一版本數據
Min<Now<Max 若Now在ReadView中則不可見 , 若不在則可見.
對於快照讀這種策略 , 在update 或 insert 等操作情況下 , 則需要讀取最新的數據 , 我們把修改數據前隱式的讀取最新數據叫做當前讀 , 那當前讀的可重複讀和幻讀問題如何解決呢?
當前讀的可重複讀與幻讀問題
通過行鎖 + 間隙鎖構成nextKey lock , 保證修改插入數據的唯一性.
innoDB行鎖的實現
InnoDB 行鎖是通過給索引上的索引項加鎖來實現的,只有通過索引條件檢索數據,InnoDB 才使用行級鎖,否則,InnoDB 將使用表鎖
只有執行計劃真正使用了索引,才能使用行鎖
行鎖是針對索引加的鎖,不是針對記錄加的鎖,所以雖然多個session是訪問不同行的記錄, 但是如果是使用相同的索引鍵, 是會出現鎖衝突的
行鎖算法
鎖在SQL上的應用
樂觀鎖與悲觀鎖是兩種併發控制的思想,可用於解決丟失更新問題
樂觀鎖會“樂觀地”假定大概率不會發生併發更新衝突,訪問、處理數據過程中不加鎖,只在更新數據時再根據版本號或時間戳判斷是否有衝突,有則處理,無則提交事務;
悲觀鎖會“悲觀地”假定大概率會發生併發更新衝突,訪問、處理數據前就加排他鎖,在整個數據處理過程中鎖定數據,事務提交或回滾後才釋放鎖;
InnoDB支持多種鎖粒度,默認使用行鎖,鎖粒度最小,鎖衝突發生的概率最低,支持的併發度也最高,但系統消耗成本也相對較高;
共享鎖與排他鎖是InnoDB實現的兩種標準的行鎖;
InnoDB有三種鎖算法——記錄鎖、gap間隙鎖、還有結合了記錄鎖與間隙鎖的next-key鎖,InnoDB對於行的查詢加鎖是使用的是next-key locking這種算法,一定程度上解決了幻讀問題;
意向鎖是爲了支持多種粒度鎖同時存在;
普通select
在讀未提交(Read Uncommitted),讀提交(Read Committed, RC),可重複讀(Repeated Read, RR)這三種事務隔離級別下,普通select使用快照讀(snpashot read),不加鎖,併發非常高;
在串行化(Serializable)這種事務的隔離級別下,普通select會升級爲select ... in share mode;
加鎖Select
如果,在唯一索引(unique index)上使用唯一的查詢條件(unique search condition),會使用記錄鎖(record lock),而不會封鎖記錄之間的間隔,即不會使用間隙鎖(gap lock)與臨鍵鎖(next-key lock);
其他的查詢條件和索引條件,InnoDB會封鎖被掃描的索引範圍,並使用間隙鎖與臨鍵鎖,避免索引範圍區間插入記錄;
UPDATE_DELETE
和加鎖select類似,如果在唯一索引上使用唯一的查詢條件來update/delete,例如:update t set name=xxx where id=1;也只加記錄鎖;
否則,符合查詢條件的索引記錄之前,都會加排他臨鍵鎖(exclusive next-key lock),來封鎖索引記錄與之前的區間;
尤其需要特殊說明的是,如果update的是聚集索引(clustered index)記錄,則對應的普通索引(secondary index)記錄也會被隱式加鎖,這是由InnoDB索引的實現機制決定的:普通索引存儲PK的值,檢索普通索引本質上要二次掃描聚集索引。隱式鎖是在索引中對二級索引的記錄邏輯加鎖,實際上不產生鎖對象,不佔用內存空間。
INSERT操作
用排它鎖封鎖被插入的索引記錄,而不會封鎖記錄之前的範圍。
同時,會在插入區間加插入意向鎖(insert intention lock),但這個並不會真正封鎖區間,也不會阻止相同區間的不同KEY插入。
自增鎖(AUTO-INC Locks)是事務插入時自增列上特殊的表級別的鎖。索引末尾設置獨佔鎖。最簡單的一種情況:在訪問自增計數器時,InnoDB使用自增鎖,但是鎖定僅僅持續到當前SQL語句的末尾,而不是整個事務的結束
插入意向鎖
假設數據表中存在(1,1)、(5,5)和(10,10)三條記錄。
事務開啓,嘗試獲取插入意向鎖。例如,事務一執行了select * from test where id>8 for update,事務二要插入(9,9),此時先要獲取插入意向鎖,由於事務一已經在對應的記錄和間隙上加了X鎖,因此事務二被阻塞,並且阻塞的原因是獲取插入意向鎖時被事務一的X鎖阻塞。
獲取意向鎖之後,插入之前進行重複索引檢查。重複索引檢查爲當前讀,需要添加S鎖。
如果是已經存在唯一索引,且索引未加鎖。直接拋出Duplicate key的錯誤。如果存在唯一索引,且索引加鎖,等待鎖釋放。
重複檢查通過之後,加入X鎖,插入記錄
事務一 |
事務二 |
select * from test where id>8 for update |
|
insert into test (9,9) 執行之後獲取意向鎖被阻塞,等待事務一 |
|
commit; |
|
獲取意向鎖,當前讀S鎖重複檢查OK,加X鎖,insert Query OK, 1 row affected |
死鎖
舉個栗子
事務一 |
事務二 |
事務三 |
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values (2,2); Query OK, 1 row affected (0.01 sec) |
||
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values (2,2); 執行之後被阻塞,等待事務一 |
||
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values (2,2); 執行之後被阻塞,等待事務一 |
||
mysql>rollback; Query OK, 0 rows affected (0.00 sec) |
||
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction |
||
Query OK, 1 row affected (16.13 sec) |
事務一 |
事務二 |
begin; |
begin; |
select * from t where code = 6 for update; |
|
select * from t where = 8 for update; |
|
insert into t values (4,5); |
|
insert into t values (4,5); |
|
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction |
|
Query OK, 1 row affected (5.45 sec) |
死鎖產生
死鎖是指兩個或多個事務在同一資源上相互佔用(間隙鎖、S鎖),並請求鎖定對方佔用的資源,從而導致惡性循環。
當事務試圖以不同的順序鎖定資源時,就可能產生死鎖。多個事務同時鎖定同一個資源時也可能會產生死鎖。
鎖的行爲和順序和存儲引擎相關。以同樣的順序執行語句,有些存儲引擎會產生死鎖有些不會——死鎖有雙重原因:真正的數據衝突;存儲引擎的實現方式。
檢測死鎖
數據庫系統實現了各種死鎖檢測和死鎖超時的機制。InnoDB存儲引擎能檢測到死鎖的循環依賴並立即返回一個錯誤。
死鎖恢復
死鎖發生以後,只有部分或完全回滾其中一個事務,才能打破死鎖,InnoDB目前處理死鎖的方法是,將持有最少行級排他鎖的事務進行回滾。所以事務型應用程序在設計時必須考慮如何處理死鎖,多數情況下只需要重新執行因死鎖回滾的事務即可。
外部鎖的死鎖檢測
發生死鎖後,InnoDB 一般都能自動檢測到,並使一個事務釋放鎖並回退,另一個事務獲得鎖,繼續完成事務。但在涉及外部鎖,或涉及表鎖的情況下,InnoDB 並不能完全自動檢測到死鎖, 這需要通過設置鎖等待超時參數 innodb_lock_wait_timeout 來解決
死鎖影響性能
死鎖會影響性能而不是會產生嚴重錯誤,因爲InnoDB會自動檢測死鎖狀況並回滾其中一個受影響的事務。在高併發系統上,當許多線程等待同一個鎖時,死鎖檢測可能導致速度變慢。有時當發生死鎖時,禁用死鎖檢測(使用innodb_deadlock_detect配置選項)可能會更有效,這時可以依賴innodb_lock_wait_timeout設置進行事務回滾。
InnoDB避免死鎖
-
爲了在單個InnoDB表上執行多個併發寫入操作時避免死鎖,可以在事務開始時通過爲預期要修改的每個元祖(行)使用SELECT ... FOR UPDATE語句來獲取必要的鎖,即使這些行的更改語句是在之後才執行的。
-
在事務中,如果要更新記錄,應該直接申請足夠級別的鎖,即排他鎖,而不應先申請共享鎖、更新時再申請排他鎖,因爲這時候當用戶再申請排他鎖時,其他事務可能又已經獲得了相同記錄的共享鎖,從而造成鎖衝突,甚至死鎖
-
如果事務需要修改或鎖定多個表,則應在每個事務中以相同的順序使用加鎖語句。在應用中,如果不同的程序會併發存取多個表,應儘量約定以相同的順序來訪問表,這樣可以大大降低產生死鎖的機會
-
通過SELECT ... LOCK IN SHARE MODE獲取行的讀鎖後,如果當前事務再需要對該記錄進行更新操作,則很有可能造成死鎖。
-
改變事務隔離級別
-
程序設計中總是捕獲並處理死鎖異常
-
如果出現死鎖,可以用 SHOW INNODB STATUS 命令來確定最後一個死鎖產生的原因。返回結果中包括死鎖相關事務的詳細信息,如引發死鎖的 SQL 語句,事務已經獲得的鎖,正在等待什麼鎖,以及被回滾的事務等。據此可以分析死鎖產生的原因和改進措施。
一些優化鎖性能的建議
-
儘量使用較低的隔離級別
-
精心設計索引, 並儘量使用索引訪問數據, 使加鎖更精確, 從而減少鎖衝突的機會
-
選擇合理的事務大小,小事務發生鎖衝突的機率也更小
-
給記錄集顯示加鎖時,最好一次性請求足夠級別的鎖。比如要修改數據的話,最好直接申請排他鎖,而不是先申請共享鎖,修改時再請求排他鎖,這樣容易產生死鎖
-
不同的程序訪問一組表時,應儘量約定以相同的順序訪問各表,對一個表而言,儘可能以固定的順序存取表中的行。這樣可以大大減少死鎖的機會
-
儘量用相等條件訪問數據,這樣可以避免間隙鎖對併發插入的影響
-
不要申請超過實際需要的鎖級別
-
除非必須,查詢時不要顯示加鎖。MySQL的MVCC可以實現事務中的查詢不用加鎖,優化事務性能;MVCC只在COMMITTED READ(讀提交)和REPEATABLE READ(可重複讀)兩種隔離級別下工作
-
對於一些特定的事務,可以使用表鎖來提高處理速度或減少死鎖的可能