Spring事物基礎(chǔ)知識及AOP相關(guān)陷阱分析
一、事務(wù)的定義
事務(wù)(Transaction),是指訪問并可能更新數(shù)據(jù)庫中各種數(shù)據(jù)項的一個程序執(zhí)行單元(unit),是恢復(fù)和并發(fā)控制的基本單位。
事務(wù)的產(chǎn)生,其實是為了當(dāng)應(yīng)用程序訪問數(shù)據(jù)庫的時候,事務(wù)能夠簡化我們的編程模型,不需要我們?nèi)タ紤]各種各樣的潛在錯誤和并發(fā)問題.
二、事務(wù)的屬性
事務(wù)具有4個屬性,簡稱 ACID
屬性 | 說明 | |
---|---|---|
Atomicity | 原子性 | 一個事務(wù)是一個不可分割的工作單位,事務(wù)中包括的操作要么都做,要么都不做。 |
Consistency | 一致性 | 事務(wù)執(zhí)行的結(jié)果必須是使數(shù)據(jù)庫從一個一致性狀態(tài)c0變到另一個一致性狀態(tài)c1 |
Isolation | 隔離性 | 一個事務(wù)的執(zhí)行不能被其他事務(wù)干擾。即一個事務(wù)內(nèi)部的操作及使用的數(shù)據(jù)對并發(fā)的其他事務(wù)是隔離的,并發(fā)執(zhí)行的各個事務(wù)之間不能互相干擾。 |
Durability | 持久性 | 指一個事務(wù)一旦提交,它對數(shù)據(jù)庫中數(shù)據(jù)的改變就應(yīng)該是永久性的。接下來的其他操作或故障不應(yīng)該對其有任何影響。 |
三、Spring 事務(wù)的隔離級別
當(dāng)多個線程都開啟事務(wù)操作數(shù)據(jù)庫中的數(shù)據(jù)時,數(shù)據(jù)庫系統(tǒng)要能進(jìn)行隔離操作,以保證各個線程獲取數(shù)據(jù)的準(zhǔn)確性。
在介紹數(shù)據(jù)庫提供的各種隔離級別之前,我們先看看如果不考慮事務(wù)的隔離性,會發(fā)生的幾種問題
3.1 隔離級別引出的問題
3.1.1 臟讀
是指在沒有隔離的情況下,一個事務(wù)讀取了另外一個事務(wù)已修改但未提交(有可能回滾也有可能繼續(xù)修改)的緩沖區(qū)數(shù)據(jù)。
3.1.2 不可重復(fù)讀
數(shù)據(jù)庫中的某項數(shù)據(jù)在一個事務(wù)多次讀取,但是在多次讀取期間,其他事務(wù)對其有修改并提交,導(dǎo)致返回值不同,這就發(fā)生了不可重復(fù)讀。
不可重復(fù)讀側(cè)重修改。
3.1.3 幻讀
幻讀和不可重復(fù)讀相似。當(dāng)一個事務(wù)(T1)讀取幾行記錄后(事務(wù)并沒有結(jié)束),另一個并發(fā)事務(wù)(T2)插入了一些記錄時,幻讀就發(fā)生了。在后來的查詢中,第一個事務(wù)(T1)就會發(fā)現(xiàn)一些原來沒有的額外記錄。
幻讀側(cè)重新增或者刪除。
3.2 隔離級別
在理想狀態(tài)下,事務(wù)之間將完全隔離(即下表中的 Isolation.SERIALIZABLE ),從而可以防止這些問題發(fā)生。
然而,完全隔離會影響性能,因為隔離經(jīng)常涉及到鎖定在數(shù)據(jù)庫中的記錄(甚至有時是鎖表)。
完全隔離要求事務(wù)相互等待來完成工作,會阻礙并發(fā)。因此,可以根據(jù)業(yè)務(wù)場景選擇不同的隔離級別。
隔離級別 | 含義 |
---|---|
Isolation.DEFAULT | 使用后端數(shù)據(jù)庫默認(rèn)的隔離級別 |
Isolation.READ_UNCOMMITTED | 允許讀取尚未提交的更改??赡軐?dǎo)致臟讀、幻讀或不可重復(fù)讀。 |
Isolation.READ_COMMITTED | (Oracle 默認(rèn)級別)允許從已經(jīng)提交的并發(fā)事務(wù)讀取??煞乐古K讀,但幻讀和不可重復(fù)讀仍可能會發(fā)生。 |
Isolation.REPEATABLE_READ | (MYSQL默認(rèn)級別)對相同字段的多次讀取的結(jié)果是一致的,除非數(shù)據(jù)被當(dāng)前事務(wù)本身改變??煞乐古K讀和不可重復(fù)讀,但幻讀仍可能發(fā)生。 |
Isolation.SERIALIZABLE | 完全服從ACID的隔離級別,確保不發(fā)生臟讀、不可重復(fù)讀和幻讀。這在所有隔離級別中也是最慢的,因為它通常是通過完全鎖定當(dāng)前事務(wù)所涉及的數(shù)據(jù)表來完成的。 |
四、Spring 事務(wù)的傳播機(jī)制
Spring 事務(wù)的傳播機(jī)制描述了在嵌套事務(wù)當(dāng)中,當(dāng)前事務(wù)與外部事務(wù)(最近的那個,有可能沒有)的繼承關(guān)系。
比如一個事務(wù)方法里面調(diào)用了另外一個事務(wù)方法,那么兩個方法是各自作為獨(dú)立的方法提交還是內(nèi)層的事務(wù)合并到外層的事務(wù)一起提交,這就是需要事務(wù)傳播機(jī)制的配置來確定怎么樣執(zhí)行。
Spring 事務(wù)的傳播有如下機(jī)制
類型 | 描述 |
---|---|
PROPAGATION_REQUIRED | Spring默認(rèn)的傳播機(jī)制,能滿足絕大部分業(yè)務(wù)需求,如果外層有事務(wù),則當(dāng)前事務(wù)加入到外層事務(wù),一塊提交,一塊回滾。如果外層沒有事務(wù),新建一個事務(wù)執(zhí)行 |
PROPAGATION_REQUES_NEW | 該事務(wù)傳播機(jī)制是每次都會新開啟一個事務(wù),同時把外層事務(wù)掛起,當(dāng)當(dāng)前事務(wù)執(zhí)行完畢,恢復(fù)上層事務(wù)的執(zhí)行。如果外層沒有事務(wù),執(zhí)行當(dāng)前新開啟的事務(wù)即可 |
PROPAGATION_SUPPORT | 如果外層有事務(wù),則加入外層事務(wù),如果外層沒有事務(wù),則直接使用非事務(wù)方式執(zhí)行。完全依賴外層的事務(wù) |
PROPAGATION_NOT_SUPPORT | 該傳播機(jī)制不支持事務(wù),如果外層存在事務(wù)則掛起,執(zhí)行完當(dāng)前代碼,則恢復(fù)外層事務(wù),無論是否異常都不會回滾當(dāng)前的代碼 |
PROPAGATION_NEVER | 該傳播機(jī)制不支持外層事務(wù),即如果外層有事務(wù)就拋出異常 |
PROPAGATION_MANDATORY | 與NEVER相反,如果外層沒有事務(wù),則拋出異常 |
PROPAGATION_NESTED | 該傳播機(jī)制的特點(diǎn)是可以保存狀態(tài)保存點(diǎn),當(dāng)前事務(wù)回滾到某一個點(diǎn),從而避免所有的嵌套事務(wù)都回滾,即各自回滾各自的,如果子事務(wù)沒有把異常吃掉,基本還是會引起全部回滾的。 |
五、Spring 事務(wù)的應(yīng)用(聲明式)
Spring 聲明式事務(wù)是指依托注解 @Transactional
和 AOP
功能,在其方法兩端添加事務(wù)的操作,實現(xiàn)對被注解修飾方法的增強(qiáng)。
5.1 事務(wù)只讀
從事務(wù)開始(時間點(diǎn)a)到這個事務(wù)結(jié)束的過程中,其他事務(wù)所提交的數(shù)據(jù),該事務(wù)將看不見?。ú樵冎胁粫霈F(xiàn)別人在時間點(diǎn)a之后提交的數(shù)據(jù))。
事務(wù)只讀只適用于 當(dāng)傳播機(jī)制為
PROPAGATION_REQUIRED,PROPAGATION_REQUES_NEW
的情況
5.1.1 應(yīng)用場景
在諸如統(tǒng)計查詢、報表查詢的過程當(dāng)中,需要多次查詢,為了避免在查詢過程當(dāng)中對剩余查詢數(shù)據(jù)的修改,保證數(shù)據(jù)整體在某一時刻的一致性,需要使用只讀事務(wù)。
5.1.2 使用方式
@Transactional(propagation = Propagation.REQUIRES, readOnly = true) public List<Product> findAllProducts() { return this.productDao.findAllProducts(); }
5.2 事務(wù)回滾
在事務(wù)注解 @Transactional
中指定了某個異常后,捕獲到事務(wù)方法拋出了該異常或者其子類異常,會造成事務(wù)回滾。默認(rèn)當(dāng)捕獲到方法拋出的 RuntimeException
異常后,事務(wù)就會回滾。還可以設(shè)置當(dāng)出現(xiàn)某異常時候不回滾,即使是運(yùn)行時異常
5.2.1 使用方式
// 回滾Exception類型異常 @Transactional(rollbackFor = Exception.class) public void test1() throws Exception { // .. } // 回滾自定義類型異常 @Transactional(rollbackForClassName = "org.transaction.demo.CustomException") public void test2() throws Exception { // .. } // 不回滾自定義類型異常 @Transactional(noRollbackFor = CustomException.class) public void test3() throws Exception { // .. }
5.3 事務(wù)超時
如果一個事務(wù)長時間占用數(shù)據(jù)庫連接,會導(dǎo)致服務(wù)等待從而引起服務(wù)雪崩效應(yīng),所以設(shè)置一個合理的超時時間,是必要的。默認(rèn)不超時。事務(wù)超時會引起事務(wù)回滾。
事務(wù)超時只適用于 當(dāng)傳播機(jī)制為
PROPAGATION_REQUIRED,PROPAGATION_REQUES_NEW
的情況
5.3.1 使用方式
//設(shè)置事務(wù)超時時間,單位秒 @Transactional(timeout = 5) public void test() { // .. }
5.4 事務(wù)傳播機(jī)制的使用方式
//每次外層事務(wù)調(diào)用都會開啟一個新事務(wù) @Transactional(propagation = Propagation.REQUIRES_NEW) public void test() { // .. }
5.5 事務(wù)隔離機(jī)制的使用方式
指定事務(wù)隔離機(jī)制只適用于 當(dāng)傳播機(jī)制為 PROPAGATION_REQUIRED,PROPAGATION_REQUES_NEW
的情況
//設(shè)置事務(wù)隔離級別為串行 @Transactional(isolation = Isolation.SERIALIZABLE)) public void test() { // .. }
六、Spring 聲明式事務(wù)的 AOP 陷阱
總所周知,聲明式事務(wù)依托 AOP 功能實現(xiàn)對事務(wù)方法的增強(qiáng),而 AOP 底層則是代理,存在代理陷阱。
6.1 AOP 代理陷阱復(fù)現(xiàn)
@Transactional(rollbackFor = RuntimeException.class) public void insertUser(User user) { userMapper.insertUser(user); throw new RuntimeException(""); } /** * 內(nèi)部調(diào)用新增方法 */ public void insertMale(User user) { user.setGender("male"); this.insertUser(user); }
當(dāng)外部方法直接調(diào)用 insertMale(user)
的時候,事務(wù)并不會生效。
6.2 原因分析
AOP使用的是動態(tài)代理的機(jī)制,它會給類生成一個代理類,事務(wù)的相關(guān)操作都在代理類上完成。內(nèi)部調(diào)用使用的是實例調(diào)用,并沒有通過代理類調(diào)用方法,所以會導(dǎo)致事務(wù)失效。
6.2.1 偽代碼
- 代理類
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //代理之前做增強(qiáng) System.out.println("代理之前..."); //根據(jù)需要添加事務(wù)處理邏輯 ... //調(diào)用原有方法 insertMale Object obj = method.invoke(object, args); //做增強(qiáng) System.out.println("代理之后..."); //根據(jù)需要添加事務(wù)處理邏輯 ... return obj; }
當(dāng)執(zhí)行 insertMale() 方法時,因為沒有事務(wù)注解,所以沒有添加事務(wù)處理邏輯,所以直接調(diào)用了目標(biāo)類的 insertMale() 方法。
- 目標(biāo)類執(zhí)行情況
public void insertMale(User user) { user.setGender("male"); //這里的 this 指向了目標(biāo)類而不是代理類 //所以及時下面的方法添加了事務(wù)注解,但是并沒有除法增強(qiáng)實現(xiàn),事務(wù)也還是不生效的 this.insertUser(user); }
6.3 解決方案
6.3.1 注入自身
利用Spring可以循環(huán)依賴來解決問題
@Service public class TestService { @Autowired private TestService testService; @Transactional(rollbackFor = RuntimeException.class) public void insertUser(User user) { userMapper.insertUser(user); throw new RuntimeException(""); } /** * 內(nèi)部調(diào)用新增方法 */ public void insertMale(User user) { user.setGender("male"); //這里使用 字段 testService 調(diào)用事務(wù)方法 testService.insertUser(user); } }
6.3.2 使用 ApplicationContext 獲取目標(biāo)類
注入 Spring 上下文 ApplicationContex
, 然后獲取到 目標(biāo) bean, 再調(diào)用事務(wù)方法
@Service public class TestService { @Autowired private ApplicationContext applicationContext; @Transactional(rollbackFor = RuntimeException.class) public void insertUser(User user) { userMapper.insertUser(user); throw new RuntimeException(""); } /** * 內(nèi)部調(diào)用新增方法 */ public void insertMale(User user) { user.setGender("male"); //這里使用上下文獲取目標(biāo)類實例 TestService testService = applicationContext.getBean(TestService.class); testService.insertUser(user); } }
6.3.3 使用 AopContext
Aop 上下文采用 ThreadLocal
保存了代理對象,可以使用 Aop 上下文來進(jìn)行目標(biāo)方法的調(diào)用。
使用時候要在啟動類上添加 exposeProxy = true
配置
- 配置
@SpringBootApplication //配置:導(dǎo)出代理對象到AOP上下文 @EnableAspectJAutoProxy(exposeProxy = true) public class DemoApplication { }
- 使用
public class TestService { @Transactional(rollbackFor = RuntimeException.class) public void insertUser(User user) { userMapper.insertUser(user); throw new RuntimeException(""); } /** * 內(nèi)部調(diào)用新增方法 */ public void insertMale(User user) { user.setGender("male"); //使用AOP上下文獲取目標(biāo)代理類 TestService testService = (TestService) AopContext.currentProxy(); testService.insertUser(user); } }
到此這篇關(guān)于Spring事物基礎(chǔ)知識及AOP相關(guān)陷阱分析的文章就介紹到這了,更多相關(guān)Spring事物基礎(chǔ) AOP陷阱內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring使用Jackson實現(xiàn)轉(zhuǎn)換XML與Java對象
這篇文章主要為大家詳細(xì)介紹了Spring如何使用Jackson實現(xiàn)轉(zhuǎn)換XML與Java對象,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-02-02jackson 如何將實體轉(zhuǎn)json json字符串轉(zhuǎn)實體
這篇文章主要介紹了jackson 實現(xiàn)將實體轉(zhuǎn)json json字符串轉(zhuǎn)實體,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10Apache Dubbo的SPI機(jī)制是如何實現(xiàn)的
SPI全稱為Service Provider Interface,對應(yīng)中文為服務(wù)發(fā)現(xiàn)機(jī)制。SPI類似一種可插拔機(jī)制,首先需要定義一個接口或一個約定,然后不同的場景可以對其進(jìn)行實現(xiàn),調(diào)用方在使用的時候無需過多關(guān)注具體的實現(xiàn)細(xì)節(jié)。在Java中,SPI體現(xiàn)了面向接口編程的思想,滿足開閉設(shè)計原則。2021-06-06