資料庫與鎖機制
資料庫事務的ACID四大特性:
原子性(Atomicity)
事務是一個原子操作單元,其對資料的修改,要麼全都執行,要麼全都不執行。
一致性(Consistency)
在事務開始和完成時,資料都必須保持一致狀態。比如使用者下單,訂單、訂單商品、使用者扣款資料必須同時成功,要麼就全部失敗,保證資料從一個一致狀態過渡到另一個一致狀態。
隔離性(Isolation)
資料庫系統提供一定的隔離機制,保證事務在不受外部併發操作影響的“獨立”環境執行。這意味著事務處理過程中的中間狀態對外部是不可見的,反之亦然。
永續性(Durability)
事務完成之後,它對於資料的修改是永久性的,即使出現系統故障也能夠保持。
資料庫併發事務存在問題:
髒讀: 一個事務可以讀取到其他事務未提交的內容。
不可重複讀: 在一個事務範圍內,先後兩次讀取了同一條記錄,卻獲得不同的結果。這是因為在第一次讀取後,有其他事務修改了這條記錄並提交到了資料庫,再次讀取後的記錄是被修改後的資料。
幻讀: 在一個事務範圍內,先後兩次讀取同一個範圍列表,卻獲得不同的結果集。這是因為在兩次讀取的過程中,有其他事務往這個範圍中插入了新的資料。
丟失更新:在不加鎖的情況下,一個事務內先讀取資料,做業務處理之後再更新該記錄。在多執行緒併發的時候,將會造成丟失更新的問題。這是因為一個事務讀取了資料,在做業務處理的過程中,有其他事務更新了資料並提交到了資料庫,當前事務再更新的時候,就會把之前的更新覆蓋掉,導致丟失更新的問題。打個比方,小郭去A視窗買2張高鐵票,售票員先查詢餘票,發現還有10張,就給小明辦理買票手續。此時小明在B視窗也買了1張同一班的高鐵票並取票離開了,B視窗的售票員將餘票更新成了9張。A視窗售票員給小郭辦好了出票手續,將之前查詢出來的10張高鐵票減去兩張,並更新資料庫中的餘票數量為8張。結果就是明明賣了三張高鐵票,餘票卻只減少了兩張!
隔離級別:
讀未提交(read uncommitted)
一個事務可以讀取到其他事務未提交的內容。
該級別併發度最高,但完全不能避免髒讀、不可重複讀、幻讀
讀已提交(read committed)
一個事務可以讀取其他事務已提交的內容。
避免了髒讀,但不能避免不可重複讀和幻讀
該級別為多數資料庫的預設隔離級別,如: oracle
可重複讀(repeatable read)
一個事務中反覆讀取同一條記錄得到是完全相同的結果
避免了髒讀、不可重複讀,正常情況下不能避免幻讀(mysql除外)
mysql innoDB的預設隔離級別為可重複讀,可以避免幻讀
序列化(serializable)
Serializable 是最高的事務隔離級別,在該級別下,事務序列化順序執行,可以避免髒讀、不可重複讀與幻讀。但是這種事務隔離級別效率非常低下,消耗資料庫效能,一般不使用。
資料庫鎖
- 表級鎖:每次操作鎖住整張表。開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖衝突的概率最高,併發度最低;
- 行級鎖:每次操作鎖住一行資料。開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖衝突的概率最低,併發度也最高;
- 頁面鎖:開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表鎖和行鎖之間,併發度一般。
mysql鎖詳解: ofollow,noindex">看這裡
下面以mysql innodb為例探究資料庫事務與鎖機制
下表作為測試,order_num為主鍵,item_id為索引
設定mysql 預設可重複讀隔離級別:
set session transaction isolation level repeatable read;
start transaction;
測試1(可重複讀):
事務A |
事務B |
select * from order_test; |
|
update order_test set buy_num=10; |
|
select * from order_test; |
|
commit; |
|
select * from order_test; |
|
事務A 三次查詢結果完全一致,事務B 的資料更新、提交後,A 查詢出來的還是之前的資料,解決了髒讀和不可重複讀的問題。 |
測試2(不完全可靠的可重複讀):
事務A |
事務B |
select buy_num from order_test where order_num=105; 返回橙子購買數量為15 |
|
update order_test set buy_num=10 where order_num=105; commit; 更新橙子購買數量為10,並提交 |
|
Update order_test set buy_num=buy_num+1 where order_num=105;橙子的購買數量加1 |
|
select buy_num from order_test where order_num=105; 返回橙子購買數量為11 !!!對於A 事務來說,相當於15+1=11 !! |
|
由於普通查詢語句未加任何鎖,事務A 未完成時,其他事務仍可對其所查詢的語句進行修改操作,mysql 實現的可重複讀並不絕對可靠。 |
測試3(丟失更新):
事務A |
事務B |
select buy_num from order_test where order_num=105; 返回橙子購買數量為15 |
|
update order_test set buy_num= 16 where order_num=105; commit; 將橙子購買數量+1,並提交,此時資料庫中橙子數量為16 |
|
Update order_test set buy_num=16 where order_num=105;根據第一步查詢出來的橙子數量,業務程式碼中加1 之後,再更新至資料庫 |
|
兩個獨立的事務分別對橙子數量+1 之後,資料庫中的橙子數量只是從15 增加到了16 !造成了丟失更新的問題。 |
針對測試2、3的問題,可在第一條查詢語句後面加上lock in share mode或者 for update
X(寫鎖) |
S(共享鎖) |
|
X |
衝突 |
衝突 |
S |
衝突 |
相容 |
其中lock in share mode將給資料新增共享鎖,容易造成死鎖,不推薦用於查詢出來之後需要在事務內進行更新的場合;for update將給資料新增寫鎖,推薦使用。下面具體看看為什麼新增共享鎖容易造成死鎖。
測試4(死鎖測試):
事務A |
事務B |
select buy_num from order_test where order_num=105 lock in share mode; 加共享鎖 |
|
Update order_test set buy_num=1 where order_num=105;加共享鎖 |
|
Update order_test set buy_num= 1 where order_num=105; 由於事務B 對該條記錄加了共享鎖,所以只能等待事務B 提交 |
|
Update order_test set buy_num=1 where order_num=105; 由於事務A對該條記錄也加了共享鎖,所以只能等待事務A提交 |
|
兩個事務都不能往下執行,互相等待對方釋放鎖,造成死鎖。如果將lock in share mode 換成 for update (寫鎖),則不會出現這個問題。 在查詢語句後加for update ,適用於先查詢資料,再根據查詢的結果,做業務處理計算出新值後,直接更新前面資料的場合,可以有效防止丟失更新問題, 很重要 |
測試5(間隙鎖):
事務A |
事務B |
select * from order_test where order_num >105 and order_num <124 for update; |
|
update order_test set buy_num=10 where order_num=111; 資料被鎖住了,無法更新 |
|
insert into order_test values(107,309,'茄子',8,now()); 等待執行,範圍內的值也無法插入 |
|
update order_test set buy_num=10 where order_num=105; 更新成功 |
|
insert into order_test values(90,309,'茄子',8,now()); 插入成功,範圍外的寫資料不受影響 |
|
InnoDB 中的行鎖+ 間隙鎖Next-Key Lock 。A 事務範圍更新語句將給範圍內的資料行新增行鎖,其他事務只能讀不能寫;範圍間的間隙新增間隙鎖,該示例中將在(105,111),(111,123),(123,124) 之間新增間隙鎖,間隙中不能插入新的記錄, 該機制使得mysql 在可重複讀級別(repeatable read ) 解決了幻讀的問題 |
測試6(mysql的表鎖):
事務A |
事務B |
update order_test set buy_num=25 where item_name =' 西瓜'; ( 無主鍵、索引,table lock) |
|
update order_test set buy_num=10 where order_num=105; 表被鎖住,無法更新 |
|
update order_test set buy_num=25 where order_num =90; ( 指定主鍵,若查無資料,加間隙鎖(- ∞,90)(90,105)) |
|
Insert into order_test values(95,100,'哈密瓜',9,now()); 間隙內,無法更新 |
|
insert into order_test values(89,309,'茄子',8,now()); 插入成功,間隙外不受影響 |
|
update order_test set buy_num =23 where order_num like '11%';( 主鍵不明確,table lock) |
|
update order_test set buy_num=10 where order_num=105; 表被鎖住,無法更新 |
|
InnoDB 預設是Row-Level Lock ,所以只有 明確 的指定主鍵或索引,SQL/">MySQL 才會執行Row lock ( 只鎖住被選取的資料) ,否則MySQL 將會執行Table Lock ( 將整個資料表單給鎖住) |