一、事務四大屬性
分別是原子性、一致性、隔離性、持久性。
1、原子性(Atomicity)
原子性是指事務包含的所有操作要么全部成功,要么全部失敗回滾,因此事務的操作如果成功就必須要完全應用到數(shù)據(jù)庫,如果操作失敗則不能對數(shù)據(jù)庫有任何影響。
2、一致性(Consistency)
一致性是指事務必須使數(shù)據(jù)庫從一個一致性狀態(tài)變換到另一個一致性狀態(tài),也就是說一個事務執(zhí)行之前和執(zhí)行之后都必須處于一致性狀態(tài)。舉例來說,假設用戶A和用戶B兩者的錢加起來一共是1000,那么不管A和B之間如何轉(zhuǎn)賬、轉(zhuǎn)幾次賬,事務結(jié)束后兩個用戶的錢相加起來應該還得是1000,這就是事務的一致性。
3、隔離性(Isolation)
隔離性是當多個用戶并發(fā)訪問數(shù)據(jù)庫時,比如同時操作同一張表時,數(shù)據(jù)庫為每一個用戶開啟的事務,不能被其他事務的操作所干擾,多個并發(fā)事務之間要相互隔離。關于事務的隔離性數(shù)據(jù)庫提供了多種隔離級別,稍后會介紹到。
4、持久性(Durability)
持久性是指一個事務一旦被提交了,那么對數(shù)據(jù)庫中的數(shù)據(jù)的改變就是永久性的,即便是在數(shù)據(jù)庫系統(tǒng)遇到故障的情況下也不會丟失提交事務的操作。例如我們在使用JDBC操作數(shù)據(jù)庫時,在提交事務方法后,提示用戶事務操作完成,當我們程序執(zhí)行完成直到看到提示后,就可以認定事務已經(jīng)正確提交,即使這時候數(shù)據(jù)庫出現(xiàn)了問題,也必須要將我們的事務完全執(zhí)行完成。否則的話就會造成我們雖然看到提示事務處理完畢,但是數(shù)據(jù)庫因為故障而沒有執(zhí)行事務的重大錯誤。這是不允許的。
二、事務的隔離級別
1、為什么要設置隔離級別
在數(shù)據(jù)庫操作中,在并發(fā)的情況下可能出現(xiàn)如下問題:
-
更新丟失(Lost update)
如果多個線程操作,基于同一個查詢結(jié)構(gòu)對表中的記錄進行修改,那么后修改的記錄將會覆蓋前面修改的記錄,前面的修改就丟失掉了,這就叫做更新丟失。這是因為系統(tǒng)沒有執(zhí)行任何的鎖操作,因此并發(fā)事務并沒有被隔離開來。
第1類丟失更新:事務A撤銷時,把已經(jīng)提交的事務B的更新數(shù)據(jù)覆蓋了。
image.png
第2類丟失更新:事務A覆蓋事務B已經(jīng)提交的數(shù)據(jù),造成事務B所做的操作丟失。
image.png
臟讀(Dirty Reads)
臟讀(Dirty Read):A事務讀取B事務尚未提交的數(shù)據(jù)并在此基礎上操作,而B事務執(zhí)行回滾,那么A讀取到的數(shù)據(jù)就是臟數(shù)據(jù)。
image.png
解決辦法:如果在第一個事務提交前,任何其他事務不可讀取其修改過的值,則可以避免該問題。
不可重復讀(Non-repeatable Reads)
一個事務對同一行數(shù)據(jù)重復讀取兩次,但是卻得到了不同的結(jié)果。事務T1讀取某一數(shù)據(jù)后,事務T2對其做了修改,當事務T1再次讀該數(shù)據(jù)時得到與前一次不同的值。
解決辦法:如果只有在修改事務完全提交之后才可以讀取數(shù)據(jù),則可以避免該問題。
幻象讀
指兩次執(zhí)行同一條 select 語句會出現(xiàn)不同的結(jié)果,第二次讀會增加一數(shù)據(jù)行,并沒有說這兩次執(zhí)行是在同一個事務中。一般情況下,幻象讀應該正是我們所需要的。但有時候卻不是,如果打開的游標,在對游標進行操作時,并不希望新增的記錄加到游標命中的數(shù)據(jù)集中來。隔離級別為 游標穩(wěn)定性 的,可以阻止幻象讀。例如:目前工資為1000的員工有10人。那么事務1中讀取所有工資為1000的員工,得到了10條記錄;這時事務2向員工表插入了一條員工記錄,工資也為1000;那么事務1再次讀取所有工資為1000的員工共讀取到了11條記錄。
解決辦法:如果在操作事務完成數(shù)據(jù)處理之前,任何其他事務都不可以添加新數(shù)據(jù),則可避免該問題。
正是為了解決以上情況,數(shù)據(jù)庫提供了幾種隔離級別。
2、事務的隔離級別
數(shù)據(jù)庫事務的隔離級別有4個,由低到高依次為Read uncommitted(未授權(quán)讀取、讀未提交)、Read committed(授權(quán)讀取、讀提交)、Repeatable read(可重復讀取)、Serializable(序列化),這四個級別可以逐個解決臟讀、不可重復讀、幻象讀這幾類問題。
Read uncommitted(未授權(quán)讀取、讀未提交):
如果一個事務已經(jīng)開始寫數(shù)據(jù),則另外一個事務則不允許同時進行寫操作,但允許其他事務讀此行數(shù)據(jù)。該隔離級別可以通過“排他寫鎖”實現(xiàn)。這樣就避免了更新丟失,卻可能出現(xiàn)臟讀。也就是說事務B讀取到了事務A未提交的數(shù)據(jù)。
Read committed(授權(quán)讀取、讀提交):
讀取數(shù)據(jù)的事務允許其他事務繼續(xù)訪問該行數(shù)據(jù),但是未提交的寫事務將會禁止其他事務訪問該行。該隔離級別避免了臟讀,但是卻可能出現(xiàn)不可重復讀。事務A事先讀取了數(shù)據(jù),事務B緊接了更新了數(shù)據(jù),并提交了事務,而事務A再次讀取該數(shù)據(jù)時,數(shù)據(jù)已經(jīng)發(fā)生了改變。
Repeatable read(可重復讀取):
可重復讀是指在一個事務內(nèi),多次讀同一數(shù)據(jù)。在這個事務還沒有結(jié)束時,另外一個事務也訪問該同一數(shù)據(jù)。那么,在第一個事務中的兩次讀數(shù)據(jù)之間,即使第二個事務對數(shù)據(jù)進行修改,第一個事務兩次讀到的的數(shù)據(jù)是一樣的。這樣就發(fā)生了在一個事務內(nèi)兩次讀到的數(shù)據(jù)是一樣的,因此稱為是可重復讀。讀取數(shù)據(jù)的事務將會禁止寫事務(但允許讀事務),寫事務則禁止任何其他事務。這樣避免了不可重復讀取和臟讀,但是有時可能出現(xiàn)幻象讀。(讀取數(shù)據(jù)的事務)這可以通過“共享讀鎖”和“排他寫鎖”實現(xiàn)。
Serializable(序列化):
提供嚴格的事務隔離。它要求事務序列化執(zhí)行,事務只能一個接著一個地執(zhí)行,但不能并發(fā)執(zhí)行。如果僅僅通過“行級鎖”是無法實現(xiàn)事務序列化的,必須通過其他機制保證新插入的數(shù)據(jù)不會被剛執(zhí)行查詢操作的事務訪問到。序列化是最高的事務隔離級別,同時代價也花費最高,性能很低,一般很少使用,在該級別下,事務順序執(zhí)行,不僅可以避免臟讀、不可重復讀,還避免了幻像讀。
三、悲觀鎖和樂觀鎖
雖然數(shù)據(jù)庫的隔離級別可以解決大多數(shù)問題,但是靈活度較差,為此又提出了悲觀鎖和樂觀鎖的概念。
1、悲觀鎖
悲觀鎖,它指的是對數(shù)據(jù)被外界(包括本系統(tǒng)當前的其他事務,以及來自外部系統(tǒng)的事務處理)修改持保守態(tài)度。因此,在整個數(shù)據(jù)處理過程中,將數(shù)據(jù)處于鎖定狀態(tài)。悲觀鎖的實現(xiàn),往往依靠數(shù)據(jù)庫提供的鎖機制。也只有數(shù)據(jù)庫層提供的鎖機制才能真正保證數(shù)據(jù)訪問的排他性,否則,即使在本系統(tǒng)的數(shù)據(jù)訪問層中實現(xiàn)了加鎖機制,也無法保證外部系統(tǒng)不會修改數(shù)據(jù)。
- 使用場景舉例:以MySQL InnoDB為例
商品t_items表中有一個字段status,status為1代表商品未被下單,status為2代表商品已經(jīng)被下單(此時該商品無法再次下單),那么我們對某個商品下單時必須確保該商品status為1。假設商品的id為1。
如果不采用鎖,那么操作方法如下:
//1.查詢出商品信息
select status from t_items where id=1;
//2.根據(jù)商品信息生成訂單,并插入訂單表 t_orders
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status為2
update t_items set status=2;
但是上面這種場景在高并發(fā)訪問的情況下很可能會出現(xiàn)問題。例如當?shù)谝徊讲僮髦校樵兂鰜淼纳唐穝tatus為1。但是當我們執(zhí)行第三步Update操作的時候,有可能出現(xiàn)其他人先一步對商品下單把t_items中的status修改為2了,但是我們并不知道數(shù)據(jù)已經(jīng)被修改了,這樣就可能造成同一個商品被下單2次,使得數(shù)據(jù)不一致。所以說這種方式是不安全的。
使用悲觀鎖來解決問題
在上面的場景中,商品信息從查詢出來到修改,中間有一個處理訂單的過程,使用悲觀鎖的原理就是,當我們在查詢出t_items信息后就把當前的數(shù)據(jù)鎖定,直到我們修改完畢后再解鎖。那么在這個過程中,因為t_items被鎖定了,就不會出現(xiàn)有第三者來對其進行修改了。需要注意的是,要使用悲觀鎖,我們必須關閉mysql數(shù)據(jù)庫的自動提交屬性,因為MySQL默認使用autocommit模式,也就是說,當你執(zhí)行一個更新操作后,MySQL會立刻將結(jié)果進行提交。我們可以使用命令設置MySQL為非autocommit模式:set autocommit=0;
設置完autocommit后,我們就可以執(zhí)行我們的正常業(yè)務了。具體如下:
//0.開始事務
begin;/begin work;/start transaction; (三者選一就可以)
//1.查詢出商品信息
select status from t_items where id=1 for update;
//2.根據(jù)商品信息生成訂單
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status為2
update t_items set status=2;
//4.提交事務
commit;/commit work;
上面的begin/commit為事務的開始和結(jié)束,因為在前一步我們關閉了mysql的autocommit,所以需要手動控制事務的提交。
上面的第一步我們執(zhí)行了一次查詢操作:select status from t_items where id=1 for update;
與普通查詢不一樣的是,我們使用了select…for update
的方式,這樣就通過數(shù)據(jù)庫實現(xiàn)了悲觀鎖。此時在t_items表中,id為1的那條數(shù)據(jù)就被我們鎖定了,其它的事務必須等本次事務提交之后才能執(zhí)行。這樣我們可以保證當前的數(shù)據(jù)不會被其它事務修改。需要注意的是,在事務中,只有SELECT ... FOR UPDATE
或LOCK IN SHARE MODE
操作同一個數(shù)據(jù)時才會等待其它事務結(jié)束后才執(zhí)行,一般SELECT ...
則不受此影響。拿上面的實例來說,當我執(zhí)行select status from t_items where id=1 for update;
后。我在另外的事務中如果再次執(zhí)行select status from t_items where id=1 for update;
則第二個事務會一直等待第一個事務的提交,此時第二個查詢處于阻塞的狀態(tài),但是如果我是在第二個事務中執(zhí)行select status from t_items where id=1;
則能正常查詢出數(shù)據(jù),不會受第一個事務的影響。
- Row Lock與Table Lock
使用select…for update
會把數(shù)據(jù)給鎖住,不過我們需要注意一些鎖的級別,MySQL InnoDB默認Row-Level Lock,所以只有「明確」地指定主鍵或者索引,MySQL 才會執(zhí)行Row lock (只鎖住被選取的數(shù)據(jù)) ,否則MySQL 將會執(zhí)行Table Lock (將整個數(shù)據(jù)表單給鎖住)。舉例如下:
1、select * from t_items where id=1 for update;
這條語句明確指定主鍵(id=1),并且有此數(shù)據(jù)(id=1的數(shù)據(jù)存在),則采用row lock。只鎖定當前這條數(shù)據(jù)。
2、select * from t_items where id=3 for update;
這條語句明確指定主鍵,但是卻查無此數(shù)據(jù),此時不會產(chǎn)生lock(沒有元數(shù)據(jù),又去lock誰呢?)。
3、select * from t_items where name='手機' for update;
這條語句沒有指定數(shù)據(jù)的主鍵,那么此時產(chǎn)生table lock,即在當前事務提交前整張數(shù)據(jù)表的所有字段將無法被查詢。
4、select * from t_items where id>0 for update;
或者select * from t_items where id<>1 for update;
(注:<>在SQL中表示不等于)
上述兩條語句的主鍵都不明確,也會產(chǎn)生table lock。
5、select * from t_items where status=1 for update;
(假設為status字段添加了索引)
這條語句明確指定了索引,并且有此數(shù)據(jù),則產(chǎn)生row lock。
6、select * from t_items where status=3 for update;
(假設為status字段添加了索引)
這條語句明確指定索引,但是根據(jù)索引查無此數(shù)據(jù),也就不會產(chǎn)生lock。
- 悲觀鎖小結(jié)
悲觀鎖并不是適用于任何場景,它也有它存在的一些不足,因為悲觀鎖大多數(shù)情況下依靠數(shù)據(jù)庫的鎖機制實現(xiàn),以保證操作最大程度的獨占性。如果加鎖的時間過長,其他用戶長時間無法訪問,影響了程序的并發(fā)訪問性,同時這樣對數(shù)據(jù)庫性能開銷影響也很大,特別是對長事務而言,這樣的開銷往往無法承受。所以與悲觀鎖相對的,我們有了樂觀鎖。
2、樂觀鎖
樂觀鎖( Optimistic Locking ) 相對悲觀鎖而言,樂觀鎖假設認為數(shù)據(jù)一般情況下不會造成沖突,所以只會在數(shù)據(jù)進行提交更新的時候,才會正式對數(shù)據(jù)的沖突與否進行檢測,如果發(fā)現(xiàn)沖突了,則返回用戶錯誤的信息,讓用戶決定如何去做。實現(xiàn)樂觀鎖一般來說有以下2種方式:
- 使用版本號
使用數(shù)據(jù)版本(Version)記錄機制實現(xiàn),這是樂觀鎖最常用的一種實現(xiàn)方式。何謂數(shù)據(jù)版本?即為數(shù)據(jù)增加一個版本標識,一般是通過為數(shù)據(jù)庫表增加一個數(shù)字類型的 “version” 字段來實現(xiàn)。當讀取數(shù)據(jù)時,將version字段的值一同讀出,數(shù)據(jù)每更新一次,對此version值加一。當我們提交更新的時候,判斷數(shù)據(jù)庫表對應記錄的當前版本信息與第一次取出來的version值進行比對,如果數(shù)據(jù)庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期數(shù)據(jù)。 - 使用時間戳
樂觀鎖定的第二種實現(xiàn)方式和第一種差不多,同樣是在需要樂觀鎖控制的table中增加一個字段,名稱無所謂,字段類型使用時間戳(timestamp), 和上面的version類似,也是在更新提交的時候檢查當前數(shù)據(jù)庫中數(shù)據(jù)的時間戳和自己更新前取到的時間戳進行對比,如果一致則OK,否則就是版本沖突。