Spring 事務、循環依賴八股文
Spring 事務實現方式有哪些?
事務就是一系列的操作原子執行。Spring事務機制主要包括聲明式事務和編程式事務。
- 編程式事務:通過編程的方式管理事務,這種方式帶來了很大的靈活性,但很難維護。
- 聲明式事務:將事務管理代碼從業務方法中分離出來,通過aop進行封裝。Spring聲明式事務使得我們無需要去處理獲得連接、關閉連接、事務提交和回滾等這些操作。使用
@Transactional注解開啟聲明式事務。
@Transactional相關屬性如下:
屬性 | 類型 | 描述 |
value | String | 可選的限定描述符,指定使用的事務管理器 |
propagation | enum: Propagation | 可選的事務傳播行為設置 |
isolation | enum: Isolation | 可選的事務隔離級別設置 |
readOnly | boolean | 讀寫或只讀事務,默認讀寫 |
timeout | int (in seconds granularity) | 事務超時時間設置 |
rollbackFor | Class對象數組,必須繼承自Throwable | 導致事務回滾的異常類數組 |
rollbackForClassName | 類名數組,必須繼承自Throwable | 導致事務回滾的異常類名字數組 |
noRollbackFor | Class對象數組,必須繼承自Throwable | 不會導致事務回滾的異常類數組 |
noRollbackForClassName | 類名數組,必須繼承自Throwable | 不會導致事務回滾的異常類名字數組 |
說一下 spring 的事務隔離級別?
Spring的事務隔離級別是指在并發環境下,事務之間相互隔離的程度。Spring框架支持多種事務隔離級別,可以根據具體的業務需求來選擇適合的隔離級別。以下是常見的事務隔離級別:
- DEFAULT(默認):使用數據庫默認的事務隔離級別。通常為數據庫的默認隔離級別,如Oracle為READ COMMITTED,MySQL為REPEATABLE READ。
- READ_UNCOMMITTED:最低的隔離級別,允許讀取未提交的數據。事務可以讀取其他事務未提交的數據,可能會導致臟讀、不可重復讀和幻讀的問題。
- READ_COMMITTED:保證一個事務只能讀取到已提交的數據。事務讀取的數據是其他事務已經提交的數據,避免了臟讀的問題。但可能會出現不可重復讀和幻讀的問題。
- REPEATABLE_READ:保證一個事務在同一個查詢中多次讀取的數據是一致的。事務期間,其他事務對數據的修改不可見,避免了臟讀和不可重復讀的問題。但可能會出現幻讀的問題。
- SERIALIZABLE:最高的隔離級別,保證事務串行執行,避免了臟讀、不可重復讀和幻讀的問題。但會降低并發性能,因為事務需要串行執行。
通過@Transactional注解的isolation屬性來指定事務隔離級別。
有哪些事務傳播行為?
在TransactionDefinition接口中定義了七個事務傳播行為:
PROPAGATION_REQUIRED如果存在一個事務,則支持當前事務。如果沒有事務則開啟一個新的事務。如果嵌套調用的兩個方法都加了事務注解,并且運行在相同線程中,則這兩個方法使用相同的事務中。如果運行在不同線程中,則會開啟新的事務。PROPAGATION_SUPPORTS如果存在一個事務,支持當前事務。如果沒有事務,則非事務的執行。PROPAGATION_MANDATORY如果已經存在一個事務,支持當前事務。如果不存在事務,則拋出異常IllegalTransactionStateException。PROPAGATION_REQUIRES_NEW總是開啟一個新的事務。需要使用JtaTransactionManager作為事務管理器。PROPAGATION_NOT_SUPPORTED總是非事務地執行,并掛起任何存在的事務。需要使用JtaTransactionManager作為事務管理器。PROPAGATION_NEVER總是非事務地執行,如果存在一個活動事務,則拋出異常。PROPAGATION_NESTED如果一個活動的事務存在,則運行在一個嵌套的事務中。如果沒有活動事務, 則按PROPAGATION_REQUIRED 屬性執行。
PROPAGATION_NESTED 與PROPAGATION_REQUIRES_NEW的區別:
使用PROPAGATION_REQUIRES_NEW時,內層事務與外層事務是兩個獨立的事務。一旦內層事務進行了提交后,外層事務不能對其進行回滾。兩個事務互不影響。
使用PROPAGATION_NESTED時,外層事務的回滾可以引起內層事務的回滾。而內層事務的異常并不會導致外層事務的回滾,它是一個真正的嵌套事務。
Spring 事務傳播行為有什么用?
主要作用是定義和管理事務邊界,尤其是一個事務方法調用另一個事務方法時,事務如何傳播的問題。它解決了多個事務方法嵌套執行時,是否要開啟新事務、復用現有事務或者掛起事務等復雜情況。
總結用途:
- 控制事務的傳播和嵌套:根據具體業務需求,可以指定是否使用現有事務或開啟新的事務,解決事務的傳播問題。
- 確保獨立操作的事務隔離:某些操作(如日志記錄、發送通知)應當獨立于主事務執行,即使主事務失敗,這些操作也可以成功完成。
- 控制事務的邊界和一致性:不同的業務場景可能需要不同的事務邊界,例如強制某個方法必須在事務中執行,或者確保某個方法永遠不在事務中運行。
談談對Spring事務和AOP底層實現原理的區別
Spring的聲明式事務其實也是通過AOP的這一套底層實現原理實現的,都是通過同一個bean的后置處理器來完成的動態代理創建的,只是:
- 創建動態代理的匹配方式不一樣: 區別就是AOP的增強通常是通過切面+切點+通知來完成的, 在創建bean的時候發現bean和切點表達式匹配就會創建動態代理。 而事務內置一個增強類, 在創建bean的時候, 一旦發現你的類加了@Transactional注解 就會創建動態代理。
- 執行動態代理的增強不一樣: 在執行AOP的bean時會先執行動態代理的增強類, 通過責任鏈分別按順序執行通知。
在執行事務的bean的時候會先執行動態代理的增強類, 在執行目標方法前進行異常捕捉,出現異常回滾事務, 無異常提交事務。
Spring事務在什么情況下會失效?
1.應用在非 public 修飾的方法上
之所以會失效是因為@Transactional 注解依賴于Spring AOP切面來增強事務行為,這個 AOP 是通過代理來實現的。
而無論是JDK動態代理還是CGLIB代理,Spring AOP的默認行為都是只代理public方法。
2.被用 final 、static 修飾方法
和上邊的原因類似,被用 final 、static 修飾的方法上加 @Transactional 也不會生效。
static 靜態方法屬于類本身的而非實例,因此代理機制是無法對靜態方法進行代理或攔截的。
final 修飾的方法不能被子類重寫,事務相關的邏輯無法插入到 final 方法中,代理機制無法對 final 方法進行攔截或增強。
3.同一個類中方法調用
比如有一個類Test,它的一個方法A,A再調用本類的方法B(不論方法B是用public還是private修飾),但方法A沒有聲明注解事務,而B方法有。則外部調用方法A之后,方法B的事務是不會起作用的。
那為啥會出現這種情況?其實這還是由于使用Spring AOP代理造成的,因為只有當事務方法被當前類以外的代碼調用時,才會由Spring生成的代理對象來管理。
但是如果是A聲明了事務,A的事務是會生效的。
4.Bean 未被 spring 管理
上邊我們知道 @Transactional 注解通過 AOP 來管理事務,而 AOP 依賴于代理機制。因此,Bean 必須由Spring管理實例! 要確保為類加上如 @Controller、@Service 或 @Component注解,讓其被Spring所管理,這很容易忽視。
5.異步線程調用
如果我們在 testMerge() 方法中使用異步線程執行事務操作,通常也是無法成功回滾的,來個具體的例子。
假設testMerge() 方法在事務中調用了 testA(),testA() 方法中開啟了事務。接著,在 testMerge() 方法中,我們通過一個新線程調用了 testB(),testB() 中也開啟了事務,并且在 testB() 中拋出了異常。此時,testA() 不會回滾 和 testB() 回滾。
testA() 無法回滾是因為沒有捕獲到新線程中 testB()拋出的異常;testB()方法正常回滾。
在多線程環境下,Spring 的事務管理器不會跨線程傳播事務,事務的狀態(如事務是否已開啟)是存儲在線程本地的 ThreadLocal 來存儲和管理事務上下文信息。這意味著每個線程都有一個獨立的事務上下文,事務信息在不同線程之間不會共享。
6.數據庫引擎不支持事務
事務能否生效數據庫引擎是否支持事務是關鍵。常用的MySQL數據庫默認使用支持事務的innodb引擎。一旦數據庫引擎切換成不支持事務的myisam,那事務就從根本上失效了。
7.RollbackFor 沒設置對,比如默認沒有任何(設置 RuntimeException 或者 Error 才能捕獲),則方法內拋出 IOException 則不會回滾,需要配置 @Transactional(rollbackFor=Exception.class)。
8.異常被捕獲了,比如代碼拋錯,但是被 catch 了,僅打了 log 沒有拋出異常,這樣事務無法正常獲取到錯誤,因此不會回滾。
Spring多線程事務 能否保證事務的一致性
在多線程環境下,Spring事務管理默認情況下無法保證全局事務的一致性。這是因為Spring的本地事務管理是基于線程的,每個線程都有自己的獨立事務。
- Spring的事務管理通常將事務信息存儲在ThreadLocal中,這意味著每個線程只能擁有一個事務。這確保了在單個線程內的數據庫操作處于同一個事務中,保證了原子性。
- 可以通過如下方案進行解決:
編程式事務: 為了在多線程環境中實現事務一致性,您可以使用編程式事務管理。這意味著您需要在代碼中顯式控制事務的邊界和操作,確保在適當的時機提交或回滾事務。
分布式事務: 如果您的應用程序需要跨多個資源(例如多個數據庫)的全局事務一致性,那么您可能需要使用分布式事務管理(如2PC/3PC TCC等)來管理全局事務。這將確保所有參與的資源都處于相同的全局事務中,以保證一致性。
總之,在多線程環境中,Spring的本地事務管理需要額外的協調和管理才能實現事務一致性。這可以通過編程式事務、分布式事務管理器或二階段提交等方式來實現,具體取決于您的應用程序需求和復雜性。
但在 Seata 框架中,事務一致性是通過分布式事務協調器(TC)來保證的。TC 負責協調分布式事務的各個參與者(RM),確保它們按照相同的順序執行事務操作,從而保證事務的一致性。 具體來說,當一個事務開始時,TC 會生成一個全局事務 ID(XID),并將其傳播給所有的 RM。每個 RM 在執行事務操作時,都會將自己的操作記錄到本地事務日志中,并將 XID 和操作記錄發送給 TC。TC 會根據 XID 和操作記錄,協調各個 RM 的執行順序,確保它們按照相同的順序執行事務操作。如果在執行過程中出現異常,TC 會根據事務回滾策略,決定是否回滾事務。 通過這種方式,Seata 框架可以保證分布式事務的一致性,即使在多個節點之間進行事務操作,也可以確保數據的一致性和可靠性。(了解)
@Transactional(rollbackFor = Exception.class)注解了解嗎?
Exception 分為運行時異常 RuntimeException 和非運行時異常。事務管理對于企業應用來說是至關重要的,即使出現異常情況,它也可以保證數據的一致性。
當 @Transactional 注解作用于類上時,該類的所有 public 方法將都具有該類型的事務屬性,同時,我們也可以在方法級別使用該標注來覆蓋類級別的定義。
@Transactional 注解默認回滾策略是只有在遇到RuntimeException(運行時異常) 或者 Error 時才會回滾事務,而不會回滾 Checked Exception(受檢查異常)。這是因為 Spring 認為RuntimeException和 Error 是不可預期的錯誤,而受檢異常是可預期的錯誤,可以通過業務邏輯來處理。
循環依賴
什么是循環依賴?
循環依賴(Circular Dependency)是指兩個或多個模塊,組件之間相互依賴形成一個閉環。簡而言之, 模塊A依賴模塊B,而模塊B又依賴于模塊A。這會導依賴鏈的循環,無法確定加載或初始化的順序。
Spring怎么解決循環依賴的問題?
解決步驟:
- Spring 首先創建 Bean 實例,并將其加入三級緩存中(Factory)。
- 當一個 Bean 依賴另一個未初始化的 Bean 時,Spring 會從三級緩存中獲取 Bean 的工廠,并生成該 Bean 的對象(若有代理則是代理對象)代理對象存入二級緩存,解決循環依賴。
- 一旦所有依賴 Bean 被完全初始化,Bean 將轉移到一級緩存中。
詳細內容如下:
首先,有兩種Bean注入的方式。
構造器注入和屬性注入。
- 對于構造器注入的循環依賴,Spring處理不了,會直接拋出
BeanCurrentlylnCreationException異常。 - 對于屬性注入的循環依賴(單例模式下),是通過三級緩存處理來循環依賴的。
而非單例對象的循環依賴,則無法處理。
下面分析單例模式下屬性注入的循環依賴是怎么處理的:
首先,Spring單例對象的初始化大略分為三步:
createBeanInstance:實例化bean,使用構造方法創建對象,為對象分配內存。populateBean:進行依賴注入。initializeBean:初始化bean。
Spring為了解決單例的循環依賴問題,使用了三級緩存:
- 一級緩存
singletonObjects:完成了初始化的單例對象map,bean name --> bean instance,存完整單例bean。 - 二級緩存
earlySingletonObjects:完成實例化未初始化的單例對象map,bean name --> bean instance,存放的是早期的bean,即半成品,此時還無法使用(只用于循環依賴提供的臨時bean對象)。 - 三級緩存
singletonFactories(循環依賴的出口,解決了循環依賴): 單例對象工廠map,bean name --> ObjectFactory,單例對象實例化完成之后會加入singletonFactories。它存的是一個對象工廠,用于創建對象并放入二級緩存中。同時,如果對象有Aop代理,則對象工廠返回代理對象。
這三個 map 是如何配合的呢?
- 首先,獲取單例 Bean 的時候會通過 BeanName 先去 singletonObjects(一級緩存)查找完整的 Bean,如果找到則直接返回,否則進行步驟 2。
- 看對應的 Bean 是否在創建中,如果不在直接返回找不到(返回null),如果是,則會去 earlySingletonObjects(二級緩存) 查找 Bean,如果找到則返回,否則進行步驟 3。
- 去 singletonfactores(三級緩存)通過 BeanName查找到對應的工廠,如果存著工廠則通過工廠創建 Bean,并目放置到earlySingletonObjects 中。
- 如果三個緩存都沒找到,則返回 null。
從上面的步驟我們可以得知,如果查詢發現 Bean 還未創建,到第二步就直接返回 null,不會繼續查二級和三級緩存。返回 null 之后,說明這個Bean 還未創建,這個時候會標記這個 Bean 正在創建中,然后再調用 createBean 來創建 Bean,而實際創建是調用方法 doCreateBean。
在調用createBeanInstance進行實例化之后,會調用addSingletonFactory,將單例對象放到singletonFactories中。
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}假如A依賴了B的實例對象,同時B也依賴A的實例對象。
- A首先完成了實例化,并且將自己添加到singletonFactories中
- 接著進行依賴注入,發現自己依賴對象B,此時就嘗試去get(B)
- 發現B還沒有被實例化,對B進行實例化
- 然后B在初始化的時候發現自己依賴了對象A,于是嘗試get(A),嘗試一級緩存singletonObjects和二級緩存earlySingletonObjects沒找到,嘗試三級緩存singletonFactories,由于A初始化時將自己添加到了singletonFactories,所以B可以拿到A對象,然后將A從三級緩存中移到二級緩存中
- B拿到A對象后順利完成了初始化,然后將自己放入到一級緩存singletonObjects中
- 此時返回A中,A此時能拿到B的對象順利完成自己的初始化
由此看出,屬性注入的循環依賴主要是通過將實例化完成的bean添加到singletonFactories來實現的。而使用構造器依賴注入的bean在實例化的時候會進行依賴注入,不會被添加到singletonFactories中。比如A和B都是通過構造器依賴注入,A在調用構造器進行實例化的時候,發現自己依賴B,B沒有被實例化,就會對B進行實例化,此時A未實例化完成,不會被添加到singtonFactories。而B依賴于A,B會去三級緩存尋找A對象,發現不存在,于是又會實例化A,A實例化了兩次,從而導致拋異常。
總結:1、利用緩存識別已經遍歷過的節點; 2、利用Java引用,先提前設置對象地址,后完善對象。
Spring有沒有解決多例Bean的循環依賴?
- 多例不會使用緩存進行存儲(多例Bean每次使用都需要重新創建)
- 不緩存早期對象就無法解決循環
Spring有沒有解決構造函數參數Bean的循環依賴?
- 構造函數的循環依賴會報錯
- 可以通過人工進行解決:@Lazy
就不會立即創建依賴的bean了
而是等到用到才通過動態代理進行創建
為什么必須都是單例
如果從源碼來看的話,循環依賴的 Bean 是原型模式,會直接拋錯:
圖片
所以 Spring 只支持單例的循環依賴,但是為什么呢?
按照理解,如果兩個Bean都是原型模式的話,那么創建A1需要創建一個B1,創建B1的時候要創建一個A2,創建 A2又要創建一個B2,創建 B2又要創建一個A3,創建 A3 又要創建一個 B3.就又卡 BUG 了,是吧,因為原型模式都需要創建新的對象,不能跟用以前的對象。
如果是單例的話,創建 A 需要創建 B,而創建的 B 需要的是之前的個 A,不然就不叫單例了,對吧? 也是基于這點, Spring 就能操作操作了。
具體做法就是:先創建A,此時的A是不完整的(沒有注入B),用個 map 保存這個不完整的A,再創建B,B需要A,所以從那個map 得到“不完整”的A,此時的B就完整了,然后A就可以注入B,然后A就完整了,B也完整了,且它們是相互依賴的。
圖片
為什么不能全是構造器注入?一個set注入,一個構造器注入一定能成功?
為什么不能全是構造器注入?
在 Spring 中創建 Bean 分三步:
- 實例化,createBeanlnstance,就是 new 了個對象
- 屬性注入,populateBean, 就是 set 一些屬性值
- 初始化,initializeBean,執行一些 aware 接口中的方法,initMethod,AOP代理等
明確了上面這三點,再結合上面說的“不完整的”,我們來理一下。
如果全是構造器注入,比如A(B b),那表明在 new的時候,就需要得到B,此時需要 new B,但是B也是要在構造的時候注入A,即B(A a),這時候B需要在一個 map 中找到不完整的A,發現找不到。
為什么找不到?因為A 還沒 new 完呢,所以找不到完整的 A,因此如果全是構造器注入的話,那么 Spring 無法處理循環依賴。
一個set注入,一個構造器注入一定能成功?
假設我們 A 是通過 set 注入 B,B 通過構造函數注入 A,此時是成功的。
我們來分析下:實例化A之后,此時可以在 map中存入A,開始為A進行屬性注入,發現需要B,此時 new B,發現構造器需要A,此時從 map中得到A,B構造完畢,B進行屬性注入,初始化,然后A注入B完成屬性注入,然后初始化 A。
整個過程很順利,沒毛病。
圖片
假設 A 是通過構造器注入 B,B 通過 set 注入 A,此時是失敗的。
我們來分析下:實例化A,發現構造函數需要B,此時去實例化B,然后進行B 的屬性注入,從 map 里面找不到A,因為 A 還沒 new 成功,所以B也卡住了,然后就 循環了。
圖片
看到這里,仔細思考的小伙伴可能會說,可以先實例化 B,往 map 里面塞入不完整的 B,這樣就能成功實例化 A 了。確實,思路沒錯但是 Spring 容器是按照字母序創建 Bean 的,A 的創建永遠排在 B 前面。
現在我們總結一下:
- 如果循環依賴都是構造器注入,則失敗
- 如果循環依賴不完全是構造器注入,則可能成功,可能失敗,具體跟BeanName的字母序有關系,
二級緩存能不能解決循環依賴?
Spring 之所以需要三級緩存而不是簡單的二級緩存,主要原因在于AOP代理和Bean的早期引用問題。
- 如果只是循環依賴導致的死循環的問題: 一級緩存就可以解決 ,但是無法解決在并發下獲取不完整的Bean。
- 二級緩存雖然可以解決循環依賴的問題,但在涉及到動態代理(OP)時,直接使用二級緩存不做任問處理會導致我們拿到的 Bean 是未代理的原始對象。如果二級緩存內存放的都是代理對象,則違反了 Bean 的生命周期
Spring一二級緩存和MyBatis一、二級緩存有什么關系?
沒有關系!
- MyBatis一、二級緩存是用來存儲查詢結果的, 一級緩存會在同一個SqlSession中的重復查詢結果進行緩存, 二級緩存則是全局應用下的重復查詢結果進行緩存。
- 而Spring的一、二級緩存是用來存儲Bean的! 一級緩存用來存儲完整最終使用的Bean,二級緩存用來存儲早期臨時bean。 當然還有個三級緩存用來解決循環依賴的。



























