以下主要以MySQL(InnoDB引擎)數據庫為討論背景,純屬個人學習總結,不對的地方還請指出!
什么是事務?
事務是作為一個邏輯單元執行的一系列操作,要么一起成功,要么一起失敗。一個邏輯工作單元必須有四個屬性,稱為 ACID(原子性、致性、隔離性和持久性)屬性,只有這樣才能成為一個事務。
數據庫事物的四大特性(ACID):
1) 原子性:(Atomicity)
務必須是原子工作單元;對於其數據修改,要么全都執行,要么全都不執行。
2) 一致性:(Consistency)
事務在完成時,必須使所有的數據都保持一致狀態。在相關數據庫中,所有規則都必須應用於事務的修改,保持所有數據的完整性。事務結束時,所有的內部數據結構(如 B 樹索引或雙向鏈表)都必須是正確的。
3) 隔離線:(Isolation)
由並發事務所作的修改必須與任何其它並發事務所作的修改隔離。事務查看數據時數據所處的狀態,要么另一並發事務修改它之前的狀態,要么是另一事務修改它之后的狀態,事務不會查看中間狀態的數據。這為可串行性,因為它能夠重新裝載起始數據,並且重播一系列事務,以使數據結束時的狀態與原始事務執的狀態相同。
4) 持久性:(Durability)
事務完成之后,它對於系統的影響是永久性的。該修改即使出現系統故障也將一直保持。
事務並發時會發生什么問題?(在不考慮事務隔離情況下)
1)臟讀:
臟讀是指在一個事務處理過程里讀取了另一個未提交的事務中的數據。
例:
事務A修改num=123
------事務B讀取num=123(A操作還未提交時)
事務A回滾
此時就會出現B事務讀到的num並不是數據庫中真正的num的值,這種情況就叫“臟讀”。
2)不可重讀:
不可重復讀是指在對於數據庫中的某個數據,一個事務范圍內多次查詢卻返回了不同的數據值,這是由於在查詢間隔,被另一個事務修改並提交了。
例:
事務A讀取num=123(事務A並未結束)
------事務B修改num=321,並提交了事務
事務A再次讀取num=321
此時就會出現同一次事務A中兩次讀取num的值不一樣,這種情況就叫“不可重讀”。
3) 虛讀/幻讀:
是指當事務不是獨立執行時發生的一種現象,例如第一個事務對一個表中的數據進行了修改,這種修改涉及到表中的全部數據行。同時,第二個事務也修改這個表中的數據,這種修改是向表中插入一行新數據。那么,以后就會發生操作第一個事務的用戶發現表中還有沒有修改的數據行,就好象發生了幻覺一樣。
例:
事務A查詢或修改表中所有num=123的數據(事務A並未結束)
------事務B新增一條num=123的數據,並提交事務
事務A再次查詢num=123的數據
此時就會出現兩次查詢到的數據條數不一致,或者存在還有數據沒有被修改到,這種情況就叫“虛讀/幻讀”。
注:
不可重復讀的重點是修改,同樣的條件,你讀取過的數據,再次讀取出來發現值不一樣;(主要在於update和delete)
幻讀的重點在於新增或者刪除,同樣的條件,第 1 次和第 2 次讀出來的記錄數不一樣。(主要在於insert)
為了解決以上事務並發時出現的一系列問題,就需要設置事務的隔離級別。
什么是數據庫事務的隔離級別?
多個線程開啟各自事務操作數據庫中數據時,數據庫系統要負責隔離操作,以保證各個線程在獲取數據時的准確性。
MySQL官方解釋,詳見https://dev.mysql.com/doc/refman/5.6/en/innodb-transaction-isolation-levels.html
數據庫共定義了四種隔離級別:
Read uncommitted:最低級別,以上情況均無法保證。(讀未提交)
Read committed:可避免臟讀情況發生(讀已提交)。
實現機制:修改時加排他鎖,直到事務提交后才釋放,讀取時加共享鎖,讀取完釋放。事務1讀取數據時加上共享鎖后(這 樣在事務1讀取數據的過程中,其他事務就不會修改該數據),不允許任何事物操作該數據,只能讀取,之后1如果有更新操作,那么會轉換為排他鎖,其他事務更 無權參與進來讀寫,這樣就防止了臟讀問題。
但是當事務1讀取數據過程中,有可能其他事務也讀取了該數據,讀取完畢后共享鎖釋放,此時事務1修改數據,修改 完畢提交事務,其他事務再次讀取數據時候發現數據不一致,就會出現不可重復讀問題,所以這樣不能夠避免不可重復讀問題。
Repeatable read:可避免臟讀、不可重復讀情況的發生。(可重復讀)
實現機制:讀取數據時加共享鎖,寫數據時加排他鎖,都是事務提交才釋放鎖。讀取時候不允許其他事物修改該數據,不管數據在事務過程中讀取多少次,數據都是一致的,避免了不可重復讀問題。
Serializable:可避免臟讀、不可重復讀、虛讀情況的發生。(串行化)
實現機制:所有的讀操作均為當前讀,讀加讀鎖 (S鎖),寫加寫鎖 (X鎖)。采用的是范圍鎖RangeS RangeS_S模式,鎖定檢索范圍為只讀,這樣就避免了幻影讀問題。
Serializable隔離級別下,讀寫沖突,因此並發度急劇下降,在MySQL/InnoDB下不建議使用。
那具體怎么避免臟讀、不可重復讀、幻讀等這些情況的出現呢?
1) 設置數據庫的事務隔離級別:
四種隔離級別最高的是Serializable級別,最低的是Read uncommitted級別,當然級別越高,執行效率就越低。像Serializable這樣的級別,就是以鎖表的方式(類似於Java多線程中的鎖)使得其他的線程只能在鎖外等待,所以平時選用何種隔離級別應該根據實際情況。
在MySQL數據庫中,支持上面四種隔離級別,默認的為Repeatable read (可重復讀);而在Oracle數據庫中,只支持Serializable (串行化)級別和Read committed (讀已提交)這兩種級別,其中默認的為Read committed級別。
您可以在全局、當前會話或僅下一個事務中設置事務特征:
使用GLOBAL關鍵字:
該聲明適用於所有后續會話。
現有會話不受影響。
使用SESSION關鍵字:
該語句適用於當前會話中執行的所有后續事務。
該等陳述在交易中是允許的,但不會影響當前正在進行的交易。
如果在事務之間執行,則該語句將覆蓋任何先前的語句,該語句設置命名特征的下一個事務值。
沒有任何SESSION或 GLOBAL關鍵字:
該聲明僅適用於會話中執行的下一個單個事務。
后續事務將恢復為使用指定特征的會話值。
在MySQL數據庫中查看當前事務的隔離級別:
全局:
SELECT @@GLOBAL.tx_isolation, @@GLOBAL.tx_read_only;
會話:
SELECT @@SESSION.tx_isolation, @@SESSION.tx_read_only;
或
select @@tx_isolation;

在MySQL數據庫中設置事務的隔離 級別:
set [glogal | session] transaction isolation level 隔離級別名稱;

或者
set tx_isolation=’隔離級別名稱;’
注:
設置數據庫的隔離級別一定要是在開啟事務之前!
如果是使用JDBC對數據庫的事務設置隔離級別的話,也應該是在調用Connection對象的setAutoCommit(false)方法之前。調用Connection對象的setTransactionIsolation(level)即可設置當前鏈接的隔離級別,至於參數level,可以使用Connection對象的字段:

在JDBC中設置隔離級別的部分代碼:

隔離級別的設置只對當前鏈接有效。對於使用MySQL命令窗口而言,一個窗口就相當於一個鏈接,當前窗口設置的隔離級別只對當前窗口中的事務有效;對於JDBC操作數據庫來說,一個Connection對象相當於一個鏈接,而對於Connection對象設置的隔離級別只對該Connection對象有效,與其他鏈接Connection對象無關。
2)只需要在添加事務額注解上加上這樣的代碼即可提升事務的隔離級別:
@Transactional(rollbackFor = OrderProcException.class, isolation = Isolation.SERIALIZABLE)
悲觀鎖:
總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完后再把資源轉讓給其它線程)。傳統的關系型數據庫里邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。Java中synchronized和ReentrantLock等獨占鎖就是悲觀鎖思想的實現。
樂觀鎖:(不能解決臟讀的問題)
總是假設最好的情況,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號機制或CAS算法實現。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫提供的類似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。
1)使用數據版本(Version)記錄機制實現,這是樂觀鎖最常用的一種實現方式。何謂數據版本?即為數據增加一個版本標識,一般是通過為數據庫表增加一個數字類型的 “version” 字段來實現。當讀取數據時,將version字段的值一同讀出,數據每更新一次,對此version值加一。當我們提交更新的時候,判斷數據庫表對應記錄的當前版本信息與第一次取出來的version值進行比對,如果數據庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期數據。或者在需要樂觀鎖控制的table中增加一個字段,名稱無所謂,字段類型使用時間戳(timestamp), 和上面的version類似,也是在更新提交的時候檢查當前數據庫中數據的時間戳和自己更新前取到的時間戳進行對比,如果一致則OK,否則就是版本沖突。
2)CAS算法即compare and swap(比較與交換),是一種有名的無鎖算法。無鎖編程,即不使用鎖的情況下實現多線程之間的變量同步,也就是在沒有線程被阻塞的情況下實現變量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三個操作數
需要讀寫的內存值 V
進行比較的值 A
擬寫入的新值 B
當且僅當 V 的值等於 A時,CAS通過原子方式用新值B來更新V的值,否則不會執行任何操作(比較和替換是一個原子操作)。一般情況下是一個自旋操作,即不斷的重試。
注:
樂觀鎖適用於寫比較少的情況下(並發量大/多讀場景),即沖突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果是多寫的情況,一般會經常產生沖突,這就會導致上層應用會不斷的進行retry,這樣反倒是降低了性能,所以一般多寫的場景下用悲觀鎖就比較合適。
Mysql InnoDB引擎的鎖機制(屬於悲觀鎖)
(之所以以InnoDB為主介紹鎖,是因為InnoDB支持事務,支持行鎖和表鎖用的比較多,Myisam不支持事務,只支持表鎖)
1)按照鎖的使用方式可分為:共享鎖、排它鎖、意向共享鎖、意向排他鎖
共享鎖/讀鎖(S):允許一個事務去讀一行,阻止其他事務獲得相同數據集的排他鎖。(其他事務可以讀但不能寫該數據集)
排他鎖/寫鎖(X):允許獲得排他鎖的事務更新數據,阻止其他事務取得相同數據集的共享讀鎖和排他寫鎖。 (其他事務不能讀和寫該數據集)
意向共享鎖(IS):通知數據庫接下來需要施加什么鎖並對表加鎖。如果需要對記錄A加共享鎖,那么此時innodb會先找到這張表,對該表加意向共享鎖之后,再對記錄A添加共享鎖。
意向排他鎖(IX):通知數據庫接下來需要施加什么鎖並對表加鎖。如果需要對記錄A加排他鎖,那么此時innodb會先找到這張表,對該表加意向排他鎖之后,再對記錄A添加排他鎖。
注:
A、意向共享鎖和意向排它鎖是數據庫主動加的,不需要我們手動處理;
B、對於UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及數據集加排他鎖(X);對於普通SELECT語句,InnoDB不會加任何鎖,事務可以通過以下語句顯示給記錄集加共享鎖或排他鎖。
共享鎖(S):SELECT * FROM table_name WHERE … LOCK IN SHARE MODE。
排他鎖(X):SELECT * FROM table_name WHERE … FOR UPDATE。
2)按照鎖的粒度可分為:行鎖、頁鎖(間隙鎖)、表鎖
行鎖是通過給索引上的索引項加鎖來實現的,只有通過索引條件來檢索數據才會用到行鎖,否則InnoDB將會使用表鎖。
表鎖:select * from table_nane where name = ‘小巷’ for update 。name字段不是唯一索引字段,所以是表鎖。(表排他鎖)
行鎖:select * from table_name where id = 1 for update 。id 字段為唯一索引字段,所以使用的就是行鎖,且是排它鎖。
頁鎖(又叫Gap鎖/間隙鎖):所謂表鎖鎖表,行鎖鎖行,那么頁鎖折中,鎖相鄰的一組數據。
通過加鎖控制,可以保證數據的一致性,但是同樣一條數據,不論用什么樣的鎖,只可以並發讀,並不可以讀寫並發(因為寫的時候加的是排他鎖所以不可以讀),這時就要引入數據多版本控制來實現讀寫並發。
MVCC(數據多版本並發控制,屬於樂觀鎖)
這項技術使得InnoDB的事務隔離級別下執行一致性讀操作有了保證,換言之,就是為了查詢一些正在被另一個事務更新的行,並且可以看到它們被更新之前的值。這是一個可以用來增強並發性的強大的技術,因為這樣的一來的話查詢就不用等待另一個事務釋放鎖。
數據多版本實現的原理是:
1,寫任務發生時,首先復制一份舊數據,以版本號區分
2,寫任務操作新克隆的數據,直至提交
3,並發讀的任務可以繼續從舊數據(快照)讀取數據,不至於堵塞
注:
快照讀和當前讀
快照讀:讀取的是快照版本,也就是歷史版本
當前讀:讀取的是最新版本
普通的SELECT就是快照讀,而UPDATE、DELETE、INSERT、SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE是當前讀。
具體實現:
在InnoDB中,給每行增加兩個隱藏字段來實現MVCC,一個用來記錄數據行的創建時間,另一個用來記錄行的過期時間(刪除時間)。在實際操作中,存儲的並不是時間,而是事務的版本號(即創建版本號和刪除版本號),每開啟一個新事務,事務的版本號就會遞增。(嚴格的來講,InnoDB會給數據庫中的每一行增加三個字段,它們分別是DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID)
於是乎,默認的隔離級別(REPEATABLE READ)下,增刪查改變成了這樣:
SELECT
讀取創建版本小於或等於當前事務版本號,並且刪除版本為空或大於當前事務版本號的記錄。這樣可以保證在讀取之前記錄是存在的。
INSERT
將當前事務的版本號保存至行的創建版本號
UPDATE
新插入一行,並以當前事務的版本號作為新行的創建版本號,同時將原記錄行的刪除版本號設置為當前事務版本號
DELETE
將當前事務的版本號保存至行的刪除版本號
例如:
此時books表中有5條數據,版本號為1
事務A,系統版本號2:select * from books;因為1<=2所以此時會讀取5條數據。
事務B,系統版本號3:insert into books ...,插入一條數據,新插入的數據版本號為3,而其他的數據的版本號仍然是2,插入完成之后commit,事務結束。
事務A,系統版本號2:再次select * from books;只能讀取<=2的數據,事務B新插入的那條數據版本號為3,因此讀不出來,解決了幻讀的問題。
注:
排它鎖 是 串行執行
共享鎖 是 讀讀並發
數據多版本 是 讀寫並發
樂觀鎖利用MVCC實現一致性非鎖定讀,這就有保證在同一個事務中多次讀取相同的數據返回的結果是一樣的,解決了不可重復讀的問題,也可以解決幻讀問題;悲觀鎖,serializable隔離級別,利用Gap Locks(頁鎖)、表鎖可以阻止其它事務在鎖定區間內插入數據,因此解決了幻讀問題。
總結:
數據庫並發問題,主要通過設置事務隔離級別來解決,而事務隔離級別一般則通過鎖機制的實現;
MySQL默認隔離級別(RR)使用MVCC+鎖混合的模式來解決臟讀、不可重讀、幻讀等問題。
MySQL(Innodb引擎)下
默認的事務級別為:可重復讀級別(RR);(可通過設置進行更改)
默認鎖級別為:行鎖;(可通過設置進行更改)
Where篩選條件中使用索引字段的,加的是行鎖;不是使用索引字段篩選的,加的是表鎖。
意向共享鎖和意向排它鎖是數據庫主動加的,不需要我們手動處理;
對於UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及數據集加排他鎖;
對於普通SELECT語句,InnoDB不會加任何鎖;(可以自己手動上鎖)
注:
READ UNCOMMITTED 讀未提交 read-uncommitted
READ COMMITTED 讀已提交 read-committed
REPEATABLE READ 可重復讀 repeatable-read
SERIALIZABLE 串行化 serializable
查看數據庫全局事務隔離級別:
SELECT @@GLOBAL.tx_isolation;
查看數據庫當前會話事務隔離級別:
SELECT @@SESSION.tx_isolation;
或
SELECT @@tx_isolation;
設置全局事務隔離級別:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
設置當前會話事務隔離級別:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
或
SET tx_isolation = 'read-committed';
