1. 程式人生 > >資料庫與鎖機制

資料庫與鎖機制

資料庫事務的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鎖詳解:看這裡

下面以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,所以只有明確的指定主鍵或索引,MySQL 才會執行Row lock (只鎖住被選取的資料) ,否則MySQL 將會執行Table Lock (將整個資料表單給鎖住)