<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
啊,昨晚發版又出現了讓有頭大的迴圈依賴問題,按理說Spring會為我們解決迴圈依賴,但是為什麼還會出現這個問題呢?為什麼在本地、UAT以及PRE環境都沒有出現這個問題,但是到了PROD環境就出現了這個問題呢?本文將從事故時間線、及時止損、覆盤分析等幾個方面為大家帶來詳細的分析,乾貨滿滿!
本著"先止損、後覆盤分析"
的原則,我們來看一下這次發版事故的時間線。
2021年11月16日晚23點00分00秒開始發版,此時集團的devops有點慢
2021年11月16日晚23點03分01秒,收到發版失敗的訊息,登入伺服器發現發生了迴圈依賴,具體錯誤如下圖,從紀錄檔中可以看到是dataCollectionSendMessageService
這個bean出現了迴圈依賴
問題發現了就需要先解決,然後再去分析為什麼。看到這個報錯紀錄檔我心裡也大概知道是為什麼了,所以很快就解決了,解決方案如下:給DataCollectionSendMessageService
加上@Lazy
註解
2021年11月16日晚23點07分16秒,使用重新整合的程式碼開始發版,大概10分鐘後線上節點全部發版完成。從時間線來看從發現問題到解決問題,前後一共用了接近15分鐘(這期間程式碼整合和釋出用了過多的時間),也算是做到了及時止損,沒有讓問題繼續擴大。
我大膽的猜想是因為打了@Aysnc註解
的bean生成了物件的代理,導致Spring bean最終載入的不是一個原始物件導致了此次問題的發生,那麼對不對呢,接下來我們通過原始碼詳細分析一下。
所謂迴圈依賴就是Spring IOC容器
在載入bean時會按照順序載入,先去範例化 beanA。然後發現 beanA 依賴於 beanB,接在又去範例化 beanB。範例化 beanB 時,發現 beanB 又依賴於 beanA。如果容器不處理迴圈依賴的話,容器會無限執行上面的流程,直到記憶體溢位,程式崩潰,所以這個時候就會丟擲BeanCurrentlyInCreationException
異常,也就是我們常說的迴圈依賴,下面是兩種常見迴圈依賴的場景。
幾個Bean之間的迴圈依賴
@Component public class A { @Autowired private B b; } @Component public class B { @Autowired private C c; } @Component public class C { @Autowired private A a; }
效果圖如下:
自己依賴自己
@Component public class A { @Autowired private A a; }
效果圖如下:
Spring是如何解決迴圈依賴的
首先Spring維護了三個Map,也就是我們通常說的三級快取
singletonObjects
:俗稱單例池,快取建立完成的單例BeansingletonFactories
:對映建立Bean的原始工廠earlySingletonObjects
:對映Bean的早期參照,也就是說這個Map裡的Bean不是完整的,只是完成了範例化,但還沒有初始化Spring通過三級快取解決了迴圈依賴,其中一級快取為單例池(singletonObjects),二級快取為早期曝光物件earlySingletonObjects,三級快取為早期曝光物件工廠(singletonFactories)。
當A、B兩個類發生迴圈參照時,在A完成範例化後,就使用範例化後的物件去建立一個物件工廠,並新增到三級快取中,如果A被AOP代理,那麼通過這個工廠獲取到的就是A代理後的物件,如果A沒有被AOP代理,那麼這個工廠獲取到的就是A範例化的物件
。
當A進行屬性注入時,會去建立B,同時B又依賴了A,所以建立B的同時又會去呼叫getBean(a)來獲取需要的依賴,此時的getBean(a)會從快取中獲取,第一步,先獲取到三級快取中的工廠;第二步,呼叫物件工工廠的getObject方法來獲取到對應的物件,得到這個物件後將其注入到B中。
緊接著B會走完它的生命週期流程,包括初始化、後置處理器等。當B建立完後,會將B再注入到A中,此時A再完成它的整個生命週期。至此,迴圈依賴結束!
簡單一句話說:先去快取裡找Bean,沒有則範例化當前的Bean放到Map,如果有需要依賴當前Bean的,就能從Map取到。
@Async
註解是Spring為我們提供的非同步呼叫的註解,@Async
可以作用到類或者方法上,標記了@Async
註解的方法將會在獨立的執行緒中被執行,呼叫者無需等待它的完成,即可繼續其他的操作。從原始碼中可以看到標記了@Async
註解的方法會被提交到org.springframework.core.task.TaskExecutor
中非同步執行。
或者我們可以通過value
來指定使用哪個自定義執行緒池,比如這樣子:
@Async("asyncTaskExecutor")
被@Async標記的bean注入時機
我們從原始碼的角度來看一下被@Async
標記的bean是如何注入到Spring容器裡的。在我們開啟@EnableAsync
註解之後代表可以向Spring容器中注入AsyncAnnotationBeanPostProcessor
,它是一個後置處理器,我們看一下他的類圖。
真正建立代理物件的程式碼在AbstractAdvisingBeanPostProcessor
中的postProcessAfterInitialization
方法中,以下程式碼有所刪減,只保留核心邏輯程式碼
// 這個map用來快取所有被postProcessAfterInitialization這個方法處理的bean private final Map<Class<?>, Boolean> eligibleBeans = new ConcurrentHashMap<>(256); // 這個方法主要是為打了@Async註解的bean生成代理物件 @Override public Object postProcessAfterInitialization(Object bean, String beanName) { // 這裡是重點,這裡返回true if (isEligible(bean, beanName)) { // 工廠模式生成一個proxyFactory ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName); if (!proxyFactory.isProxyTargetClass()) { evaluateProxyInterfaces(bean.getClass(), proxyFactory); } // 切入切面並建立一個代理物件 proxyFactory.addAdvisor(this.advisor); customizeProxyFactory(proxyFactory); return proxyFactory.getProxy(getProxyClassLoader()); } // No proxy needed. return bean; }
protected boolean isEligible(Class<?> targetClass) { // 首次從eligibleBeans這個map中一定是拿不到的 Boolean eligible = this.eligibleBeans.get(targetClass); if (eligible != null) { return eligible; } // 如果沒有advisor,也就是切面,直接返回false if (this.advisor == null) { return false; } // 這裡判斷AsyncAnnotationAdvisor能否切入,因為我們的bean是打了@Aysnc註解,這裡是一定能切入的,最終會返回true eligible = AopUtils.canApply(this.advisor, targetClass); this.eligibleBeans.put(targetClass, eligible); return eligible; }
至此打了@Aysnc註解
的bean就建立完成了,結果是生成了一個代理物件
。
迴圈依賴到底是怎麼生成的
經過上面的原始碼分析,我們可以知道有@Aysnc註解
的bean最後生成了一個代理物件,我們結合Spring bean建立的流程來分析這次問題。
beanA
開始初始化,beanA
範例化完成後給beanA
的依賴屬性beanB
進行賦值beanB
開始初始化,beanB
範例化完成後給beanB
的依賴屬性beanA
進行賦值beanA
是支援迴圈依賴的,所以可以在earlySingletonObjects
中可以拿到beanA
的早期參照的,但是因為beanB
打了@Aysnc註解
並不能在earlySingletonObjects
中可以拿到早期參照initializeBean(Object existingBean, String beanName)
方法,這裡beanA
可以正常範例化完成,但是因為beanB
打了@Aysnc註解
,所以向Spring IOC容器中增加了一個代理物件,也就是說beanA
的beanB
並不是一個原始物件,而是一個代理物件doCreateBean
方法時對進行檢測,以下程式碼有所刪減,只保留核心邏輯程式碼protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException { if (earlySingletonExposure) { Object earlySingletonReference = getSingleton(beanName, false); if (earlySingletonReference != null) { if (exposedObject == bean) { exposedObject = earlySingletonReference; } else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { String[] dependentBeans = getDependentBeans(beanName); Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length); // 重點在這裡,這裡會遍歷所有依賴的bean,如果beanA依賴beanB和快取中的beanB不相等 // 也就是說beanA本來依賴的是一個原始物件beanB,但是這個時候發現beanB是一個代理物件,就會增加到actualDependentBeans for (String dependentBean : dependentBeans) { if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { actualDependentBeans.add(dependentBean); } } // 發現actualDependentBeans不為空,就發生了我們最開始截圖的錯誤 if (!actualDependentBeans.isEmpty()) { throw new BeanCurrentlyInCreationException(beanName, "Bean with name '" + beanName + "' has been injected into other beans [" + StringUtils.collectionToCommaDelimitedString(actualDependentBeans) + "] in its raw version as part of a circular reference, but has eventually been " + "wrapped. This means that said other beans do not use the final version of the " + "bean. This is often the result of over-eager type matching - consider using " + "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example."); } } } } // Register bean as disposable. try { registerDisposableBeanIfNecessary(beanName, bean, mbd); } catch (BeanDefinitionValidationException ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex); } return exposedObject; }
解決迴圈依賴的正確姿勢
@Lazy
註解@Async
的Bean參與迴圈依賴至此我們就知道為什麼發生了此次問題。
到此這篇關於深度解析SpringBoot中@Async引起的迴圈依賴的文章就介紹到這了,更多相關SpringBoot中@Async迴圈依賴內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45