數(shù)據(jù)庫事務(wù)(Database Transaction) ,是指作為單個邏輯工作單元執(zhí)行的一系列操作,要么完全地執(zhí)行,要么完全地不執(zhí)行。 事務(wù)處理可以確保除非事務(wù)性單元內(nèi)的所有操作都成功完成,否則不會永久更新面向數(shù)據(jù)的資源。通過將一組相關(guān)操作組合為一個要么全部成功要么全部失敗的單元,可以簡化錯誤恢復(fù)并使應(yīng)用程序更加可靠。
事務(wù)的基本要素(ACID)
- 原子性(Atomicity):事務(wù)開始后所有操作,要么全部做完,要么全部不做,不可能停滯在中間環(huán)節(jié)。事務(wù)執(zhí)行過程中出錯,會回滾到事務(wù)開始前的狀態(tài),所有的操作就像沒有發(fā)生一樣。也就是說事務(wù)是一個不可分割的整體,就像化學(xué)中學(xué)過的原子,是物質(zhì)構(gòu)成的基本單位。
- 一致性(Consistency):事務(wù)開始前和結(jié)束后,數(shù)據(jù)庫的完整性約束沒有被破壞 。比如A向B轉(zhuǎn)賬,不可能A扣了錢,B卻沒收到。
- 隔離性(Isolation):同一時間,只允許一個事務(wù)請求同一數(shù)據(jù),不同的事務(wù)之間彼此沒有任何干擾。比如A正在從一張銀行卡中取錢,在A取錢的過程結(jié)束前,B不能向這張卡轉(zhuǎn)賬。
- 持久性(Durability):事務(wù)完成后,事務(wù)對數(shù)據(jù)庫的所有更新將被保存到數(shù)據(jù)庫,不能回滾。
當(dāng)單進(jìn)程執(zhí)行時,很容易保證事務(wù)的4中基本要素,并且不會引發(fā)問題,然而當(dāng)多個并發(fā)進(jìn)程共同執(zhí)行事務(wù)時候,可能會引發(fā)不同的問題。
事務(wù)隔離級別
| 隔離級別 | 臟讀 | 不可重復(fù)讀 | 幻讀 | 特點 |
|---|---|---|---|---|
| 未提交讀(read-uncommitted) | √ | √ | √ | 優(yōu)點在于并發(fā)能力高,適合那些對數(shù)據(jù)一致性沒有要求而追求高并發(fā)的場景,缺點是臟讀 |
| 讀寫提交(read-committed) | × | √ | √ | 出現(xiàn)不可重復(fù)讀 |
| 可重復(fù)讀(repeatable-read) | × | × | √ | mysql的默認(rèn)事務(wù)隔離級別。會出現(xiàn)幻讀 |
| 串行化(serializable) | × | × | × | 并行性低,一般不用。通常消除幻讀使用數(shù)據(jù)庫鎖的方式 |
- 臟讀:事務(wù)A讀取了事務(wù)B更新的數(shù)據(jù),然后B回滾操作,那么A讀取到的數(shù)據(jù)是臟數(shù)據(jù)
- 不可重復(fù)讀:事務(wù) A 多次讀取同一數(shù)據(jù),事務(wù) B 在事務(wù)A多次讀取的過程中,對數(shù)據(jù)作了更新并提交,導(dǎo)致事務(wù)A多次讀取同一數(shù)據(jù)時,結(jié)果不一致。
- 幻讀:幻讀是針對多條數(shù)據(jù)庫記錄的。例如,系統(tǒng)管理員A將數(shù)據(jù)庫中所有學(xué)生的成績從具體分?jǐn)?shù)改為ABCDE等級,但是系統(tǒng)管理員B就在這個時候插入了一條具體分?jǐn)?shù)的記錄,當(dāng)系統(tǒng)管理員A改結(jié)束后發(fā)現(xiàn)還有一條記錄沒有改過來,就好像發(fā)生了幻覺一樣,這就叫幻讀。
數(shù)據(jù)庫鎖
樂觀鎖
相對悲觀鎖而言,樂觀鎖假設(shè)認(rèn)為數(shù)據(jù)一般情況下不會造成沖突,所以在數(shù)據(jù)進(jìn)行提交更新的時候,才會正式對數(shù)據(jù)的沖突與否進(jìn)行檢測,如果發(fā)現(xiàn)沖突了,則讓返回用戶錯誤的信息,讓用戶決定如何去做。一般的實現(xiàn)樂觀鎖的方式就是記錄數(shù)據(jù)版本,并且不會造成線程的阻塞。
但是,版本的沖突會造成請求失敗的概率劇增,這時往往需要通過重入的機(jī)制將請求失敗的概率降低。而多次的重入又會帶來過多執(zhí)行SQL的問題,為了克服這個問題,可以考慮使用按時間戳或者限制重入次數(shù)的辦法。
悲觀鎖
正如其名,它指的是對數(shù)據(jù)被外界修改持保守態(tài)度。因此在整個數(shù)據(jù)處理過程中,將數(shù)據(jù)處于鎖定狀態(tài)。 悲觀鎖的實現(xiàn),往往依靠數(shù)據(jù)庫提供的鎖機(jī)制 (也只有數(shù)據(jù)庫層提供的鎖機(jī)制才能真正保證數(shù)據(jù)訪問的排他性,否則,即使在本系統(tǒng)中實現(xiàn)了加鎖機(jī)制,也無法保證外部系統(tǒng)不會修改數(shù)據(jù))。
數(shù)據(jù)庫兩種鎖可以實現(xiàn)悲觀鎖,排他鎖(Exclusive Lock,也叫X鎖)和共享鎖(Shared Lock,也叫S鎖)。排他鎖表示對數(shù)據(jù)進(jìn)行寫操作,如果一個事務(wù)對對象加了排他鎖,其他事務(wù)就不能再給它加任何鎖了;共享鎖會為操作對象加共享鎖,可以允許其他共享鎖的加鎖操作,但是不允許排它鎖加鎖操作。
鎖粒度
MySQL鎖的粒度幾種,從小到大分別是,行鎖(Record Lock)、間隙鎖(Gap Lock)、Next-Key Lock、表鎖。
1、 行鎖:鎖定一行記錄
2、 間隙鎖:鎖定一個范圍,但是不包含記錄本身
3、Next-Key Lock:Record Lock和Gap Lock的組合,鎖定一個區(qū)間以及對應(yīng)的記錄
4、 表鎖:鎖定整張表
MySQL innoDB加鎖是通過索引項判斷加鎖的范圍,即如果操作的對象上沒有索引或者沒有使用索引,則會導(dǎo)致鎖的粒度擴(kuò)大。因為InnoDB索引采用的BTree結(jié)構(gòu),所以,如果不是唯一索引,則如果使用查詢值索引不存在,或者使用的條件查詢則會引發(fā)Next-Key Lock。
意向鎖
有了共享鎖(S鎖)和排它鎖(X鎖),能夠?qū)崿F(xiàn)數(shù)據(jù)庫操作不同粒度的加鎖,但是在一個操作需要給整個表加X鎖的時候,需要確定整個表以及表的每一行沒有被加S鎖或者X鎖,那么如何確定表中每一行沒有被加鎖,需要掃描整個表嗎?如果表中有海量記錄需要長時間等待,此時需要有意向鎖。
Spring的@Transaction注解
在Spring中,為了 “擦除”令人厭煩的 try...catch...finally..語句,減少代碼中數(shù)據(jù)庫連接開閉和事務(wù)回滾提交的代碼, Spring利用其 AOP 為我們提供了一個數(shù)據(jù)庫事務(wù)的約定流程,這樣開發(fā)的代碼可讀性就更高,也更好維護(hù)。

@Transactional源碼分析:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
// 通過 bean name 指定事務(wù)管理器
@AliasFor("transactionManager")
String value() default "";
// 同 value 屬性
@AliasFor("value")
String transactionManager() default "";
// 傳播行為
Propagation propagation() default Propagation.REQUIRED;
// 隔離級別
Isolation isolation() default Isolation.DEFAULT;
// 超時時間
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
// 是否只讀事務(wù)
boolean readOnly() default false;
// 方法在發(fā)生指定異常時回滾,默認(rèn)是所有異常都囚滾
Class<? extends Throwable>[] rollbackFor() default {};
// 方法在發(fā)生指定異常名稱時回滾,默認(rèn)是所有異常都回滾
String[] rollbackForClassName() default {};
// 方法在發(fā)生指定異常時不回滾,默認(rèn)是所有異常都回滾
Class<? extends Throwable>[] noRollbackFor() default {};
// 方法在發(fā)生指定異常名稱時不回滾,默認(rèn)是所有異常都回滾
String[] noRollbackForClassName() default {};
}
事務(wù)管理器
事務(wù)的打開、回滾和提交是由事務(wù)管理器來完成的。在Spring中,事務(wù)管理器的頂層接口為PlatformTransactionManager,Spring也定義了一些其他的接口和類。
在Spring Boot中,當(dāng)你依賴于mybatis-spring-boot-starter之后,它會自動創(chuàng)建一個DataSource- TransactionManager對象作為事務(wù)管理器;如果依賴于spring-boot-starter-data-jpa,則它會自動創(chuàng)建JpaTransactionManager對象作為事務(wù)管理器,所以我們一般不需要自己創(chuàng)建事務(wù)管理器而直接使用它們即可。
傳播行為
傳播行為是方法之間調(diào)用事務(wù)采取的策略。在大部分的情況下,我們會認(rèn)為數(shù)據(jù)庫事務(wù)要么全部成功,要么全部失敗。但當(dāng)執(zhí)行批量任務(wù)時,我們有時不希望因為極少數(shù)的任務(wù)不能完成而回滾所有的批量任務(wù) 。
在Spring中,當(dāng)一個方法調(diào)用另外一個方法時,可以讓事務(wù)采取不同的策略工作,如新建事務(wù)或者掛起當(dāng)前事務(wù)等,這便是事務(wù)的傳播行為。例如,批量任務(wù)我們稱之為當(dāng)前方法,當(dāng)它調(diào)用單個任務(wù)時,稱單個任務(wù)為子方法。當(dāng)前方法調(diào)用子方法的時候,讓每一個子方法不在當(dāng)前事務(wù)中執(zhí)行,而是創(chuàng)建一個新的事務(wù),我們就說當(dāng)前方法調(diào)用子方法的傳播行為為新建事務(wù)。此外,還可能讓方法在無事務(wù)、獨立事務(wù)中執(zhí)行,這些完全取決于業(yè)務(wù)需求。
源碼分析:
public enum Propagation {
// 需要事務(wù)。它是默認(rèn)傳播行為,如果當(dāng)前存在事務(wù),就沿用當(dāng)前事務(wù),否則新建一個事務(wù)運(yùn)行子方法
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
// 支持事務(wù),如果當(dāng)前存在事務(wù),就沿用當(dāng)前事務(wù),如果不存在 ,則繼續(xù)采用無事務(wù)的方式運(yùn)行子方法
SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
// 必須使用事務(wù),如果當(dāng)前沒有事務(wù),則會拋出異常,如果存在當(dāng)前事務(wù) ,就沿用當(dāng)前事務(wù)
MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
// 無論當(dāng)前事務(wù)是否存在,都會創(chuàng)建新事務(wù)運(yùn)行方法,這樣新事務(wù)就可以擁有新的鎖和隔離級別等特性,與當(dāng)前事務(wù)相互獨立
REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
// 不支持事務(wù),當(dāng)前存在事務(wù)時,將掛起事務(wù),運(yùn)行方法
NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
// 不支持事務(wù),如果當(dāng)前方法存在事務(wù),則拋出異常,否則繼續(xù)使用無事務(wù)機(jī)制運(yùn)行
NEVER(TransactionDefinition.PROPAGATION_NEVER),
// 在當(dāng)前方法調(diào)用子方法時,如果子方法發(fā)生異常,只因滾子方法執(zhí)行過的 SQL,而不回滾當(dāng)前方法的事務(wù)
NESTED(TransactionDefinition.PROPAGATION_NESTED);
private final int value;
Propagation(int value) { this.value = value; }
public int value() { return this.value; }
}
常用的傳播行為是REQUIRED、REQUIRES_NEW、NESTED三種。
@Transactional自調(diào)用失效問題
Spring數(shù)據(jù)庫事務(wù)的實現(xiàn)原理是AOP,而AOP的原理是動態(tài)代理。在事務(wù)自調(diào)用的過程中,是類自身的調(diào)用,而不是代理對象去調(diào)用,那么就不會產(chǎn)生AOP,這樣Spring就不能把你的代碼織入到約定的流程中,于是就產(chǎn)生了@Transactional自調(diào)用失效的場景。
有兩種解決方法:用一個Service去調(diào)用另一個Service,這樣就是代理對象的調(diào)用,Spring就會將代碼織入事務(wù)流程;或者從Spring IoC容器中獲取代理對象去啟用AOP。
參考資料
- 《深入淺出Spring Boot 2.X》
- 《mysql數(shù)據(jù)庫事務(wù)與鎖》--堯亦