二級緩存spring循環
1. spring一級緩存和二級緩存的區別是什麼
一級緩存:x0dx0a就是Session級別的緩存。一個Session做了一個查詢操作,它會把這個操作的結果放在一級緩存中。x0dx0a如果短時間內這個session(一定要同一個session)又做了同一個操作,那麼hibernate直接從一級緩存中拿,而不會再去連資料庫,取數據。x0dx0a它是內置的事務范圍的緩存,不能被卸載。x0dx0a二級緩存:x0dx0a就是SessionFactory級別的緩存。顧名思義,就是查詢的時候會把查詢結果緩存到二級緩存中。x0dx0a如果同一個sessionFactory創建的某個session執行了相同的操作,hibernate就會從二級緩存中拿結果,而不會再去連接資料庫。x0dx0a這是可選的插件式的緩存,在默認情況下,SessionFactory不會啟用這個插件。x0dx0a可以在每個類或每個集合的粒度上配置。緩存適配器用於把具體的緩存實現軟體與Hibernate集成。x0dx0a嚴格意義上說,SessionFactory緩存分為兩類:內置緩存和外置緩存。我們通常意義上說的二級緩存是指外置緩存。x0dx0a內置緩存與session級別緩存實現方式相似。前者是SessionFactory對象的一些集合屬性包含的數據,後者是指Session的一些集合屬性包含的數據x0dx0aSessionFactory的內置緩存中存放了映射元數據和預定義sql語句。x0dx0a映射元數據是映射文件中數據的拷貝;x0dx0a而預定義SQL語句是在Hibernate初始化階段根據映射元數據推導出來。x0dx0aSessionFactory的內置緩存是只讀的,應用程序不能修改緩存中的映射元數據和預定義SQL語句,因此SessionFactory不需要進行內置緩存與映射文件的同步。x0dx0aHibernate的這兩級緩存都位於持久化層,存放的都是資料庫數據的拷貝。x0dx0a緩存的兩個特性:x0dx0a緩存的范圍x0dx0a緩存的並發訪問策略x0dx0a1、緩存的范圍x0dx0a決定了緩存的生命周期以及可以被誰訪問。緩存的范圍分為三類。x0dx0a事務范圍x0dx0a進程范圍x0dx0a集群范圍x0dx0a註:x0dx0a對大多數應用來說,應該慎重地考慮是否需要使用集群范圍的緩存,因為訪問的速度不一定會比直接訪問資料庫數據的速度快多少。x0dx0a事務范圍的緩存是持久化層的第一級緩存,通常它是必需的;進程范圍或集群范圍的緩存是持久化層的第二級緩存,通常是可選的。x0dx0a2、緩存的並發訪問策略x0dx0a當多個並發的事務同時訪問持久化層的緩存的相同數據時,會引起並發問題,必須採用必要的事務隔離措施。x0dx0a在進程范圍或集群范圍的緩存,即第二級緩存,會出現並發問題。x0dx0a因此可以設定以下四種類型的並發訪問策略,每一種策略對應一種事務隔離級別。x0dx0a事務型並發訪問策略是事務隔離級別最高,只讀型的隔離級別最低。事務隔離級別越高,並發性能就越低。x0dx0aA 事務型:僅僅在受管理環境中適用。它提供了Repeatable Read事務隔離級別。x0dx0a對於經常被讀但很少修改的數據,可以採用這種隔離類型,因為它可以防止臟讀和不可重復讀這類的並發問題。x0dx0aB 讀寫型:提供了Read Committed事務隔離級別。僅僅在非集群的環境中適用。x0dx0a對於經常被讀但很少修改的數據,可以採用這種隔離類型,因為它可以防止臟讀這類的並發問題。x0dx0aC 非嚴格讀寫型:不保證緩存與資料庫中數據的一致性。x0dx0a如果存在兩個事務同時訪問緩存中相同數據的可能,必須為該數據配置一個很短的數據過期時間,從而盡量避免臟讀。x0dx0a對於極少被修改,並且允許偶爾臟讀的數據,可以採用這種並發訪問策略。x0dx0aD 只讀型:對於從來不會修改的數據,如參考數據,可以使用這種並發訪問策略。x0dx0a什麼樣的數據適合存放到第二級緩存中?x0dx0a1、很少被修改的數據x0dx0a2、不是很重要的數據,允許出現偶爾並發的數據x0dx0a3、不會被並發訪問的數據x0dx0a4、參考數據x0dx0a不適合存放到第二級緩存的數據?x0dx0a1、經常被修改的數據x0dx0a2、財務數據,絕對不允許出現並發x0dx0a3、與其他應用共享的數據。x0dx0aHibernate的二級緩存策略的一般過程如下:x0dx0a1) 條件查詢的時候,總是發出一條select * from table_name where ?. (選擇所有欄位)這樣的SQL語句查詢資料庫,一次獲得所有的數據對象。x0dx0a2) 把獲得的所有數據對象根據ID放入到第二級緩存中。x0dx0a3) 當Hibernate根據ID訪問數據對象的時候,首先從Session一級緩存中查;查不到,如果配置了二級緩存,那麼從二級緩存中查;查不到,再查詢資料庫,把結果按照ID放入到緩存。x0dx0a4) 刪除、更新、增加數據的時候,同時更新緩存。x0dx0a註:x0dx0aHibernate的二級緩存策略,是針對於ID查詢的緩存策略,對於條件查詢則毫無作用。為此,Hibernate提供了針對條件查詢的Query緩存。x0dx0aQuery緩存策略的過程如下:x0dx0a1) Hibernate首先根據這些信息組成一個Query Key,Query Key包括條件查詢的請求一般信息:SQL, SQL需要的參數,記錄范圍(起始位置rowStart,最大記錄個數maxRows),等。x0dx0a2) Hibernate根據這個Query Key到Query緩存中查找對應的結果列表。如果存在,那麼返回這個結果列表;如果不存在,查詢資料庫,獲取結果列表,把整個結果列表根據Query Key放入到Query緩存中。x0dx0a3) Query Key中的SQL涉及到一些表名,如果這些表的任何數據發生修改、刪除、增加等操作,這些相關的Query Key都要從緩存中清空。
2. 徹底理解Spring如何解決循環依賴
可以簡化為以下5步。
1、構建BeanDefinition
2、實例化 Instantiation
3、屬性賦值 Populate
4、初始化 Initialization(BeanPostprocessor -> Aware,init)
5、銷毀 Destruction
用來保存實例化、初始化都完成的bean對象。
用來保存實例化完成,但是未初始化完成的對象(這個對象不一定是原始對象,也有可能是經過AOP生成的代理對象)。
用來保存一個對象工廠(ObjectFactory),提供一個匿名內部類,用於創建二級緩存中的對象。
三級緩存中提到的ObjectFactory即 () -> getEarlyBeanReference(beanName,mbd,bean),其中bean就是原始對象。
其中 getEarlyBeanReference 方法是 介面中定義的,AbstractAutoProxyCreator(Spring AOP proxy creator)實現了該方法。
分別按照一級緩存、二級緩存、三級緩存順序載入。如果存在循環依賴(比如beanName:B依賴beanName:A),而且三級緩存中存在beanName:A的引用,則從三級緩存中拿到beanName:A對應的提早曝光的對象(可能是原始對象,也可能是代理對象)並放入二級緩存。比如又有beanName:C依賴beanName:A,會直接從二級緩存中獲取到。
直接添加到一級緩存
org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(beanName:A, allowEarlyReference:false)
@Transactional使用的是自動代理創建器AbstractAutoProxyCreator,它實現了getEarlyBeanReference()方法從而很好的對循環依賴提供了支持。
@Async的代理創建使用的是單獨的後置處理器實現的,它只在一處()實現了對代理對象的創建,因此若出現它被循環依賴了,就會報。
具體原因分析參考: https://blog.csdn.net/f641385712/article/details/92797058
1、Spring 解決循環依賴有兩個前提條件:不全是構造器方式的循環依賴,必須是單例。
2、如果沒有出現循環依賴,第三級緩存(singletonFactories)將不會使用到,對象會按照Spring創建bean的生命周期流程,最後將bean直接放到第一級緩存(singletonObjects)中。
3、一定要三級緩存嘛,二級緩存不能解決循環依賴?不能,主要是為了生成代理對象。
因為三級緩存中放的是生成具體對象的匿名內部類(ObjectFactory),它可能生成代理對象,也可能是普通的實例對象。使用三級緩存主要是為了保證不管什麼時候使用的都是一個對象。假設只有二級緩存的情況,往二級緩存中放的顯示一個普通的Bean對象, BeanPostProcessor 去生成代理對象之後,覆蓋掉二級緩存中的普通Bean對象,無法保證程多線程環境下獲取到bean對象一致性。
3. hibernate二級緩存 和 spring整合的緩存(就是用哪個Cacheable註解的)有什麼區別么
二級緩存配置(spring+hibernate)
說明:本人不建議使用查詢緩存,因為查詢緩存要求完全相同的查詢sql語句才會起作用,所說的查詢緩存是針對第二次查詢時 sql語句與第一次sql語句完全相同 那麼就可以從緩存中取數據而不去資料庫中取數據了,在不啟用查詢緩存的情況下 每次的查詢數據也會緩存到二級緩存的 只不過每次查詢都會去查詢資料庫(不包括根據ID查詢),啟用查詢緩存很麻煩 需要每次查詢時 調用Query.setCacheable(true)方法才可以,如:List<OrgiData> orgiDatas = (List<OrgiData>) s.createQuery("from OrgiData").setCacheable(true).list();
因此建議將查詢緩存設置為如下:
hibernate.cache.use_query_cache=false
還有就是最重要的一點:對於經常修改或重要的數據不宜進行緩存,因為多並發時會造成數據不同步的情況。
首先增加ehcache-1.4.1.jar和backport-util-concurrent-3.1.jar或oscache-2.1.jar
一、spring配置
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mappingResources">
<list>
<value>com/handpay/core/merchant/bean/MerchGroupBuy.hbm.xml
</value>
</list>
</property>
<property name="hibernateProperties">
<value>
hibernate.dialect=org.hibernate.dialect.SQLServerDialect
hibernate.show_sql=true
hibernate.format_sql=true
hibernate.hbm2ddl.auto=update
hibernate.cache.use_second_level_cache=true
hibernate.cache.use_query_cache=false
hibernate.cache.provider_class=org.hibernate.cache.EhCacheProvider </value>
</property>
</bean>
<!---紅色字體是二級緩存相關的設置->
二、hbm.xml文件示例
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.handpay.core.merchant.bean">
<class name="MerchGroupBuy" table="merch_group_buy">
<cache usage="read-write" region="com.handpay.core.merchant.bean.MerchGroupBuy"/>
<id name="id">
<generator class="native" />
</id>
<property name="code" />
<property name="createTime"/>
<property name="minNum"/>
<property name="status">
</property>
<property name="title"/>
<property name="typeCode"/>
<property name="updateTime"/>
</class>
</hibernate-mapping>
三、註解示例
@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
@Table(name = "alcor_t_countries", catalog = "alcorweb")
public class AlcorTCountries implements java.io.Serializable{。。。。}
四、配置文件參數詳解
ehcache.xml是ehcache的配置文件,並且存放在應用的classpath中。下面是對該XML文件中的一些元素及其屬性的相關說明:
<diskStore>元素:指定一個文件目錄,當EHCache把數據寫到硬碟上時,將把數據寫到這個文件目錄下。 下面的參數這樣解釋:
user.home – 用戶主目錄
user.dir – 用戶當前工作目錄
java.io.tmpdir – 默認臨時文件路徑
<defaultCache>元素:設定緩存的默認數據過期策略。
<cache>元素:設定具體的命名緩存的數據過期策略。
<cache>元素的屬性
name:緩存名稱。通常為緩存對象的類名(非嚴格標准)。
maxElementsInMemory:設置基於內存的緩存可存放對象的最大數目。
maxElementsOnDisk:設置基於硬碟的緩存可存放對象的最大數目。
eternal:如果為true,表示對象永遠不會過期,此時會忽略timeToIdleSeconds和timeToLiveSeconds屬性,默認為false;
timeToIdleSeconds: 設定允許對象處於空閑狀態的最長時間,以秒為單位。當對象自從最近一次被訪問後,如果處於空閑狀態的時間超過了timeToIdleSeconds屬性值,這個對象就會過期。當對象過期,EHCache將把它從緩存中清空。只有當eternal屬性為false,該屬性才有效。如果該屬性值為0,則表示對象可以無限期地處於空閑狀態。
timeToLiveSeconds:設定對象允許存在於緩存中的最長時間,以秒為單位。當對象自從被存放到緩存中後,如果處於緩存中的時間超過了 timeToLiveSeconds屬性值,這個對象就會過期。當對象過期,EHCache將把它從緩存中清除。只有當eternal屬性為false,該屬性才有效。如果該屬性值為0,則表示對象可以無限期地存在於緩存中。timeToLiveSeconds必須大於timeToIdleSeconds屬性,才有意義。
overflowToDisk:如果為true,表示當基於內存的緩存中的對象數目達到了maxElementsInMemory界限後,會把益出的對象寫到基於硬碟的緩存中。注意:如果緩存的對象要寫入到硬碟中的話,則該對象必須實現了Serializable介面才行。
memoryStoreEvictionPolicy:緩存對象清除策略。有三種:
1 FIFO ,first in first out ,這個是大家最熟的,先進先出,不多講了
2 LFU , Less Frequently Used ,就是上面例子中使用的策略,直白一點就是講一直以來最少被使用的。如上面所講,緩存的元素有一個hit 屬性,hit 值最小的將會被清出緩存。
2 LRU ,Least Recently Used ,最近最少使用的,緩存的元素有一個時間戳,當緩存容量滿了,而又需要騰出地方來緩存新的元素的時候,那麼現有緩存元素中時間戳離當前時間最遠的元素將被清出緩存。
五 、查看 二級緩存數據
1、使用sessionFactory直接獲取
Map cacheEntries = sessionFactory().getStatistics()
.getSecondLevelCacheStatistics("cacheRegionName")
.getEntries();
其中 cacheRegionName 既是 ehcache.xml配置中的<cache 標簽的name屬性值
2、讓log4j列印緩存信息(生成環境下請注釋掉,以免影響性能)
log4j.logger.org.hibernate.cache=debug
4. spring為什麼要使用三級緩存解決循環依賴
首先清楚spring中bean 的載入過程:
1 解析需要spring管理的類為beanDefinition
2 通過反射實例化對象
3 反射設置屬性
4初始化,調用initMethod等。(postConstruct也是在這執行)
循環依賴的問題: a依賴b,b依賴a。
在a實例化之後會先將a放入到緩存中,然後給a設置屬性,去緩存中查到b。此時找不到就開始b的創建。b實例化之後,放入到緩存中,需要給a設置屬性,此時去緩存中查到a設置成功。然後初始化。成功後將b放入一級緩存。這個時候a在給自己屬性b設置值的時候就找到了b,然後設置b。完成屬性設置,再初始化,初始化後a放入一級緩存。
解決代理對象(如aop)循環依賴的問題。
例: a依賴b,b依賴a,同時a,b都被aop增強。
首先明確aop的實現是通過 postBeanProcess後置處理器,在初始化之後做代理操作的。
為什麼使用三級緩存原因:
1 只使用二級緩存,且二級緩存緩存的是一個不完整的bean
如果只使用二級緩存,且二級緩存緩存的是一個不完整的bean,這個時候a在設置屬性的過程中去獲取b(這個時候a還沒有被aop的後置處理器增強),創建b的過程中,b依賴a,b去緩存中拿a拿到的是沒有經過代理的a。就有問題。
2 使用二級緩存,且二級緩存是一個工廠方法的緩存
如果二級緩存是一個工廠的緩存,在從緩存中獲取的時候獲取到經過aop增強的對象。可以看到從工廠緩存中獲取的邏輯。
protected ObjectgetEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && ()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bpinstanceof ) {
ibp = () bp;
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
return exposedObject;
}
a依賴b,b依賴a,c。c又依賴a。a,b,c均aop增強。
載入開始: a實例化,放入工廠緩存,設置b,b實例化,設置屬性,拿到a,此時從工廠緩存中拿到代理後的a。由於a沒載入完畢,不會放入一級緩存。這個時候b開始設置c,c實例化,設置屬性a,又去工廠緩存中拿對象a。這個時候拿到的a和b從工廠緩存不是一個對象。出現問題。
3 使用二級緩存,二級緩存緩存的是增強後的bean。這個與spring載入流程不符合。spring載入流程是:實例化,設置屬性,初始化,增強。在有循環引用的時候,之前的bean並不會增強後放入到二級緩存。
綜上1,2,3 可知二級緩存解決不了有aop的循環依賴。spring採用了三級緩存。
一級緩存 singletonObjects 緩存載入完成的bean。
二級緩存 earlySingletonObjects 緩存從三級緩存中獲取到的bean,此時裡面的bean沒有載入完畢。
三級緩存 singletonFactories 。緩存一個objectFactory工廠。
場景:a依賴b,b依賴a和c,c依賴a。並且a,b,c都aop增強。
載入過程:
a實例化,放入三級工廠緩存,設置屬性b,b實例化放入三級緩存。b設置屬性a,從三級工廠緩存中獲取代理後的對象a,同時,代理後的a放入二級緩存,然後設置屬性c,c實例化放入三級緩存,設置屬性a,此時從二級緩存中獲取到的代理後的a跟b中的a是一個對象,屬性a設置成功。c初始化,然後執行後置處理器。進行aop的增強。增強後將代理的c放入到一級緩存,同時刪除三級緩存中的c。c載入完成,b得到c,b設置c成功。b初始化,然後執行後置處理器,進行aop增強,將增強後的代理對象b放入到一級緩存。刪除三級緩存中的b。此時 a拿到b,設置屬性b成功,開始初始化,初始化後執行後置處理器。在aop的後置處理器中有一個以beanName為key,經過aop增強的代理對象為value的map earlyProxyReferences。
這個時候 後置處理器處理對象a的時候,
public (@Nullable Object bean, String beanName) {
if (bean !=null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
也就是 發現這個beanName已經被代理後就不在代理。這個時候執行後置處理器後,a還是未經代理的對象a。此時a再通過getSingleton 重新從緩存中獲取一下a。
Object earlySingletonReference = getSingleton(beanName, false);
false 表示不從三級緩存中取,只從一級,二級緩存中獲取。
這個時候能拿到二級緩存中的a。二級緩存中的a也是經過代理後的a。
然後將代理後的a放入到一級緩存中。a載入完畢。
放入一級緩存的過程 :
addSingleton(beanName, singletonObject);
從三級工廠緩存中獲取對象:
protected ObjectgetEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && ()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bpinstanceof ) {
ibp = () bp;
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
return exposedObject;
}
其中 AbstractAutoProxyCreator實現該介面。
public ObjectgetEarlyBeanReference(Object bean, String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
this.earlyProxyReferences.put(cacheKey, bean);
return wrapIfNecessary(bean, beanName, cacheKey);
}
wrapIfNecessary()就是真正執行代理的。
bean初始化之後執行的後置處理器:
其中AbstractAutoProxyCreator 實現了該介面。
5. Spring是如何解決循環依賴的
你需要我,我需要你就是循環依賴
在Spring中使用的三級緩存來解決循環依賴問題,這里的緩存其實就是Map對象
當獲取一個Bean時會先從緩存中查找是否有相應的Bean。
1 創建A實例
2 將A實例(半初始化,屬性沒有填充)暴露放入緩存中
3 填充A實例的屬性
4 A實例屬性依賴B對象
5 創建B對象實例
6 填充B實例屬性
7 B實例屬性依賴A對象
8 將上面已經暴露到三級緩存中的A對象注入給B實例
在獲取A對象的時候執行上面27.1中的getSingleton方法,會將三級緩存中A這個半初始化狀態的對象移除,將其存入到二級緩存中。
9 B實例Bean的創建工作繼續執行初始化方法
B如果需要AOP代理?最終B對象是個代理對象。B到此就完全的初始化完了,B的依賴對象A此時是個半初始化狀態的對象
10 B實例對象保存到一級緩存
最終B實例創建,初始化都執行完後會將自身加入到一級緩存同時清除二級,三級緩存
11 A實例Bean創建繼續執行
如果B是被AOP代理的那麼此時的A實例注入的B對象就是一個代理對象。
12 A實例Bean執行初始化方法
13 A繼續執行上面的10步驟
三級緩存解決問題:循環依賴+AOP問題
只用一,二級緩存:
從上面羅列的步驟看似乎很是完美解決了循環依賴問題,接下來我們看看加入AOP的場景
假如A,B兩個對象最終都是要被AOP代理的
執行到這里,A中依賴的B是代理對象沒有問題,但是B中依賴的A對象是原始對象;這就不正確了應該依賴的A也必須是代理對象才是。
引入三級緩存:
三級緩存引入了ObjectFactory對象,在獲取對象的時候,是調用ObjectFactory#getObject方法。
而這個getObject方法的實現實際執行的是getEarlyBeanReference方法,再來回顧下:
在創建實例時先將其存入三級緩存中:
getEarlyBeanReference方法就是提前創建代理對象
如果開啟了AOP代理後
通過getEarlyBeanReference方法提前創建代理對象。這樣就解決了循環依賴時AOP代理問題。保證獲取的都是同一個對象。
其實引入三級緩存還解決了一個問題就是延遲代理對象的創建,如果不應用ObjectFactory方式那麼我們需要不管需不需要都要先創建代理對象,而引入ObjectFactory可以在注入的時候先暴露的是ObjectFactory只有在調用getObject方法的時候才去創建真正的代理對象(避免了所有Bean都強制創建代理對象)。當沒有被代理時可以直接返回原始對象,如果被代理會提前創建代理對象。
不用二級直接是用一,三級緩存?
假設場景:A 依賴 B,B 依賴 A、C,C 依賴 A
如果這樣會出現不同的代理對象,每次調用getObject都會創建不同的代理對象(在上面的場景中如果只用一,三級緩存那麼 B 依賴 A會通過getObject獲取一個代理對象Proxy$1,接著注入C的時候 C中又依賴A,那這時候又從getObject獲取對象那麼返回的將又會是一個新的代理對象Proxy$2;在這個過程中A對象就出現了2個不一樣的對象了,這肯定是錯誤的)。而引入二級緩存也就解決了這個問題。只有二級緩存沒有的時候才從三級緩存匯總獲取(如果需要則創建代理對象,然後保存到二級緩存中,二級緩存中已經是提前創建了代理對象(如果需要代理))。
當一個Bean完全的創建完以後放入一級緩存中,此時會吧二級三級中的緩存清除。
完畢!!!!
SpringMVC參數統一驗證方法
SpringBoot多數據源配置詳解
SpringCloud Nacos 整合feign
Spring AOP動態代理失效的解決方法@Transactional為何會失效
SpringBoot配置文件你了解多少?
SpringBoot郵件發送示例 Springboot面試題整理附答案
SpringBoot項目查看線上日誌
在Spring Cloud 中你還在使用Ribbon快來試試Load-Balancer
SpringBoot分庫分表sharding-sphere3
6. Spring如何處理循環引用
以上就是典型的循環引用場景。
熟悉Spring的人都知道Spring是通過三級緩存來處理循環依賴:
下面是本次跟蹤的大概流程,主要是關注了三級緩存的變化情況
用大白話來描述一下流程(假定beanA先創建):
Code1:
從Spring真正開始創建對象的 doGetBean 方法開始
Code2:
此時,由於beanA第一次被創建,因此 一級緩存 獲取不到對象,返回NULL,再次返回Code1創建beanA
Code3:(注意這里重載了Code2的方法)
首先從一級緩存中嘗試獲取beanA,如果獲取不到則標記當前beanA處於創建狀態,通過 ObjectFactory 開始創建beanA
Code4:
首先通過推斷出來的構造器創建beanA,接著將beanA放入三級緩存,開始為beanA填充屬性,此時會發現beanB尚未創建,進入beanB的創建流程
beanA填充beanB屬性完畢,從一、二級緩存中嘗試獲取緩存中的beanA
Code5:
創建beanB的流程其實類似上面創建beanA的流程,首先在一、二、三級緩存中沒有找到beanB,則開始創建beanB
Code6:
這個其實就上面創建beanA的時候執行過得Code2,先從一級緩存獲取beanB,沒有獲取到則標記beanB正在創建,進入創建beanB的流程
beanB經歷 實例化、屬性填充、初始化 操作之後,清除beanB正在創建標記,並將beanB從 三級緩存 挪到 一級緩存
Code7:
這里與創建beanA過程的Code4一樣,創建beanB完畢之後,開始為beanB填充屬性beanA
為beanB填充完畢beanA,beanB的創建過程主要步驟( 實例化、屬性填充、初始化 )結束
Code8:
為beanB填充屬性beanA的時候,Spring再次嘗試創建beanA,此時會走到Code1的流程,但是此時Spring已經可以從三級緩存中獲取到beanA,因此不會再往下執行beanA的創建,同時會將beanA從 三級緩存 移動到 二級緩存
PS.
經過beanA和beanB的創建,我們發現Spring每次創建一個bean都會嘗試先去 一、二、三級緩存 獲取,這里其實就是Code2中 getSingleton ,如果一級緩存沒有獲取到,但是bean正在被創建則嘗試去二級緩存獲取,二級緩存沒有獲取到,但是bean允許提前引用則再次嘗試從三級緩存獲取
Code9:
beanA經歷 創建、屬性填充、初始化 完畢,重新嘗試從緩存中獲取
從上面解決循環引用的問題,我們可以總結出,Spring創建對象、裝配屬性的時候都嘗試先從一、二、三級緩存載入創建好的對象,在裝配屬性的時候如果從三級緩存獲取到對象,會移動到二級緩存,此後獲取這個對象就會先從二級緩存中獲取。
如果採用二級緩存,單純從解決循環引用的角度來說確實可以實現,就以上面的beanA來說,假設beanB通過二級緩存裝配了beanA屬性,後續beanA被某些操作重新包裝了一次再次放入二級緩存,就會導致同一個beanA屬性,在不同的bean中引用了不同的對象。
7. Spring的三級緩存
例如:A依賴B , B 依賴 C , C 依賴 A 這樣就是一個簡單的循環依賴。
創建bean的流程為:
一級緩存:singletonObjects
二級緩存:earlySingletonObjects
三級緩存:singletonFactories
1、創建A的時候 去一級緩存中查詢A對象 為 null,這時會繼續將A的實例創建出來(調用構造方法),未進行初始化,創建半成品A,將半成品A 放入到 三級緩存中。
2、初始化A的時候發現需要B,但是B未創建,這時走創建B的流程,這時會繼續將B的實例創建出來(調用構造方法),未進行初始化,創建半成品B,將半成品B放入到三級緩存中。
3、初始化B的時候發現需要C,但是C未創建,這時走創建C的流程,這時會繼續將C的實例創建出來(調用構造方法),未進行初始化,創建半成品C,將半成品C放入到三級緩沖中。
4、接下來初始化C發現需要A,從一級緩存和二級緩存中取不到A,但是從三級緩沖中取到A,將A放入到二級緩存並從三級緩存中清除。
5、C對象創建成功放入到一級緩存,C對象創建好了以後B對象也可以初始化成功,再將B對象放入一級緩存。
6、最後初始化A對象,放入一級緩存。
二級緩存可以解決循環依賴的問題,但是不能解決aop場景下的循環依賴,這樣注入到其他bean的都是原始對象,而不是最終的代理對象。
8. Spring boot + Mybatis plus + Redis實現二級緩存
1.1 通過application.yml配置redis的連接信息,springboot默認redis用的lecttuce客戶端,如果想用jedis的話,只需要在pom.xml中引入redis的時候排除在lecttuce,然後再導入jedis的jar包就好了,
1.2 打開mybatis plus的二級緩存,為true的時候是開啟的,false是關閉二級緩存
1.3 編寫緩存類繼承Cache類,實現Cache中的方法
1.4 早*.xml中加上<cache>標簽,type寫你所編寫二級緩存類的路徑
9. spring一級緩存和二級緩存的區別是什麼
一級緩存:
就是Session級別的緩存。一個Session做了一個查詢操作,它會把這個操作的結果放在一級緩存中。
如果短時間內這個session(一定要同一個session)又做了同一個操作,那麼hibernate直接從一級緩存中拿,而不會再去連資料庫,取數據。
它是內置的事務范圍的緩存,不能被卸載。
二級緩存:
就是SessionFactory級別的緩存。顧名思義,就是查詢的時候會把查詢結果緩存到二級緩存中。
如果同一個sessionFactory創建的某個session執行了相同的操作,hibernate就會從二級緩存中拿結果,而不會再去連接資料庫。
這是可選的插件式的緩存,在默認情況下,SessionFactory不會啟用這個插件。
可以在每個類或每個集合的粒度上配置。緩存適配器用於把具體的緩存實現軟體與Hibernate集成。
嚴格意義上說,SessionFactory緩存分為兩類:內置緩存和外置緩存。我們通常意義上說的二級緩存是指外置緩存。
內置緩存與session級別緩存實現方式相似。前者是SessionFactory對象的一些集合屬性包含的數據,後者是指Session的一些集合屬性包含的數據
SessionFactory的內置緩存中存放了映射元數據和預定義SQL語句。
映射元數據是映射文件中數據的拷貝;
而預定義SQL語句是在Hibernate初始化階段根據映射元數據推導出來。
SessionFactory的內置緩存是只讀的,應用程序不能修改緩存中的映射元數據和預定義SQL語句,因此SessionFactory不需要進行內置緩存與映射文件的同步。
Hibernate的這兩級緩存都位於持久化層,存放的都是資料庫數據的拷貝。
緩存的兩個特性:
緩存的范圍
緩存的並發訪問策略
1、緩存的范圍
決定了緩存的生命周期以及可以被誰訪問。緩存的范圍分為三類。
事務范圍
進程范圍
集群范圍
註:
對大多數應用來說,應該慎重地考慮是否需要使用集群范圍的緩存,因為訪問的速度不一定會比直接訪問資料庫數據的速度快多少。
事務范圍的緩存是持久化層的第一級緩存,通常它是必需的;進程范圍或集群范圍的緩存是持久化層的第二級緩存,通常是可選的。
2、緩存的並發訪問策略
當多個並發的事務同時訪問持久化層的緩存的相同數據時,會引起並發問題,必須採用必要的事務隔離措施。
在進程范圍或集群范圍的緩存,即第二級緩存,會出現並發問題。
因此可以設定以下四種類型的並發訪問策略,每一種策略對應一種事務隔離級別。
事務型並發訪問策略是事務隔離級別最高,只讀型的隔離級別最低。事務隔離級別越高,並發性能就越低。
A 事務型:僅僅在受管理環境中適用。它提供了Repeatable Read事務隔離級別。
對於經常被讀但很少修改的數據,可以採用這種隔離類型,因為它可以防止臟讀和不可重復讀這類的並發問題。
B 讀寫型:提供了Read Committed事務隔離級別。僅僅在非集群的環境中適用。
對於經常被讀但很少修改的數據,可以採用這種隔離類型,因為它可以防止臟讀這類的並發問題。
C 非嚴格讀寫型:不保證緩存與資料庫中數據的一致性。
如果存在兩個事務同時訪問緩存中相同數據的可能,必須為該數據配置一個很短的數據過期時間,從而盡量避免臟讀。
對於極少被修改,並且允許偶爾臟讀的數據,可以採用這種並發訪問策略。
D 只讀型:對於從來不會修改的數據,如參考數據,可以使用這種並發訪問策略。
什麼樣的數據適合存放到第二級緩存中?
1、很少被修改的數據
2、不是很重要的數據,允許出現偶爾並發的數據
3、不會被並發訪問的數據
4、參考數據
不適合存放到第二級緩存的數據?
1、經常被修改的數據
2、財務數據,絕對不允許出現並發
3、與其他應用共享的數據。
Hibernate的二級緩存策略的一般過程如下:
1) 條件查詢的時候,總是發出一條select * from table_name where …. (選擇所有欄位)這樣的SQL語句查詢資料庫,一次獲得所有的數據對象。
2) 把獲得的所有數據對象根據ID放入到第二級緩存中。
3) 當Hibernate根據ID訪問數據對象的時候,首先從Session一級緩存中查;查不到,如果配置了二級緩存,那麼從二級緩存中查;查不到,再查詢資料庫,把結果按照ID放入到緩存。
4) 刪除、更新、增加數據的時候,同時更新緩存。
註:
Hibernate的二級緩存策略,是針對於ID查詢的緩存策略,對於條件查詢則毫無作用。為此,Hibernate提供了針對條件查詢的Query緩存。
Query緩存策略的過程如下:
1) Hibernate首先根據這些信息組成一個Query Key,Query Key包括條件查詢的請求一般信息:SQL, SQL需要的參數,記錄范圍(起始位置rowStart,最大記錄個數maxRows),等。
2) Hibernate根據這個Query Key到Query緩存中查找對應的結果列表。如果存在,那麼返回這個結果列表;如果不存在,查詢資料庫,獲取結果列表,把整個結果列表根據Query Key放入到Query緩存中。
3) Query Key中的SQL涉及到一些表名,如果這些表的任何數據發生修改、刪除、增加等操作,這些相關的Query Key都要從緩存中清空。
10. Spring循環依賴引出的問題(轉)
源起
在開發過程中,遇到需要把方法調用改為非同步的情況,本來以為簡單得加個@Asyn在方法上就行了,沒想到項目啟動的時候報了如下的錯誤:
Caused by: org.springframework.beans.factory.:
Error creating bean with name 'customerServiceImpl':
Bean with name 'customerServiceImpl' has been injected into other beans [customerServiceImpl,followServiceImpl,cupidService] 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.
看了下好像報的是循環依賴的錯誤,但是Spring單例是支持循環依賴的,當時一臉懵逼。
拿著報錯去網路了下,說是多個動態代理導致的循環依賴報錯,也找到了報錯的地點,但是還是不明白為什麼會這樣,所以打算深入源碼探個究竟,順便回顧下Bean的獲取流程和循環依賴的內容。
模擬場景
用SpringBoot新建一個demo項目,因為原項目是有定義切面的,這里也定義一個切面:
@Aspect
@Component
public class TestAspect {
@Pointcut("execution(public * com.example.demo.service.CyclicDependencyService.sameClassMethod(..))")
private void testPointcut() {}
@AfterReturning("testPointcut()")
public void after(JoinPoint point) {
System.out.println("在" + point.getSignature() + "之後干點事情");
}
}
然後新建一個注入自己的Service構成循環依賴,然後提供一個方法滿足切點要求,並且加上@Async註解:
@Service
public class CyclicDependencyService {
@Autowired
private CyclicDependencyService cyclicDependencyService;
public void test() {
System.out.println("調用同類方法");
cyclicDependencyService.sameClassMethod();
}
@Async
public void sameClassMethod() {
System.out.println("循環依賴中的非同步方法");
System.out.println("方法線程:" + Thread.currentThread().getName());
}
}
還有別忘了給Application啟動類加上@EnableAsync和@EnableAspectJAutoProxy:
@EnableAsync
@EnableAspectJAutoProxy
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
最後打好斷點,開始debug。
debug
從Bean創建的的起點--AbstractBeanFactory#getBean開始
// Eagerly check singleton cache for manually registered singletons.
Object sharedInstance = getSingleton(beanName);
首先會在緩存中查找,DefaultSingletonBeanRegistry#getSingleton(String beanName, boolean allowEarlyReference):
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && (beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
這里一共有三級緩存:
singletonObjects,保存初始化完成的單例bean實例;
earlySingletonObjects,保存提前曝光的單例bean實例;
singletonFactories,保存單例bean的工廠函數對象;
後面兩級都是為了解決循環依賴設置的,具體查找邏輯在後續其他情況下調用會說明。
緩存中找不到,就要創建單例:
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
});
調用DefaultSingletonBeanRegistry#getSingleton(String beanName, ObjectFactory<?> singletonFactory):
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
...
beforeSingletonCreation(beanName);
...
singletonObject = singletonFactory.getObject();
...
afterSingletonCreation(beanName);
...
addSingleton(beanName, singletonObject);
...
}
創建前後分別做了這幾件事:
前,beanName放入singletonsCurrentlyInCreation,表示單例正在創建中
後,從singletonsCurrentlyInCreation中移除beanName
後,將創建好的bean放入singletonObjects,移除在singletonFactories和earlySingletonObjects的對象
創建單例調用getSingleton時傳入的工廠函數對象的getObject方法,實際上就是createBean方法,主要邏輯在#doCreateBean中:
...
instanceWrapper = createBeanInstance(beanName, mbd, args);
final Object bean = instanceWrapper.getWrappedInstance();
...
// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
(beanName));
if (earlySingletonExposure) {
if (logger.isTraceEnabled()) {
logger.trace("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
// Initialize the bean instance.
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
...
if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
else if (!this. && hasDependentBean(beanName)) {
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
for (String dependentBean : dependentBeans) {
if (!(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}
if (!actualDependentBeans.isEmpty()) {
throw new (beanName,
"Bean with name '" + beanName + "' has been injected into other beans [" +
StringUtils.(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.");
}
}
}
}
可以看到報錯就是在這個方法里拋出的,那麼這個方法就是重點中的重點。
首先實例化單例,instantiate,只是實例化獲取對象引用,還沒有注入依賴。我debug時記錄的bean對象是 CyclicDependencyService@4509 ;
然後判斷bean是否需要提前暴露,需要滿足三個條件:1、是單例;2、支持循環依賴;3、bean正在創建中,也就是到前面提到的singletonsCurrentlyInCreation中能查找到,全滿足的話就會調用DefaultSingletonBeanRegistry#addSingletonFactory把beanName和單例工廠函數對象(匿名實現調用#getEarlyBeanReference方法)放入singletonFactories;
接著就是注入依賴,填充屬性,具體怎麼注入這里就不展開了,最後會為屬性cyclicDependencyService調用DefaultSingletonBeanRegistry.getSingleton(beanName, true),注意這里和最開始的那次調用不一樣,為true,就會在singletonFactories中找到bean的單例工廠函數對象,也就是在上一步提前暴露時放入的,然後調用它的匿名實現#getEarlyBeanReference:
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && ()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof ) {
ibp = () bp;
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
return exposedObject;
}
方法邏輯就是挨個調用實現了介面的後置處理器(以下簡稱BBP)的getEarlyBeanReference方法。一個一個debug下來,其他都是原樣返回bean,只有會把原bean( CyclicDependencyService@4509 )存在earlyProxyReferences,然後將bean的代理返回(debug時記錄的返回對象是 CyclicDependencyService$$EnhancerBySpringCGLIB$$6ed9e2db@4740 )並放入earlySingletonObjects,再賦給屬性cyclicDependencyService。
public Object getEarlyBeanReference(Object bean, String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
this.earlyProxyReferences.put(cacheKey, bean);
return wrapIfNecessary(bean, beanName, cacheKey);
}
屬性填充完成後就是調用初始化方法#initializeBean:
...
invokeAwareMethods(beanName, bean);
...
wrappedBean = (wrappedBean, beanName);
...
invokeInitMethods(beanName, wrappedBean, mbd);
...
wrappedBean = (wrappedBean, beanName);
...
初始化主要分為這幾步:
如果bean實現了BeanNameAware、BeanClassLoaderAware或BeanFactoryAware,把相應的資源放入bean;
順序執行BBP的方法;
如果實現了InitializingBean就執行afterPropertiesSet方法,然後執行自己的init-method;
順序執行BBP的。
debug的時候發現是第4步改變了bean,先執行#:
public Object (@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
這里會獲取並移除之前存在earlyProxyReferences的bean( CyclicDependencyService@4509 ),因為和當前bean是同一個對象,所以什麼都沒做直接返回。隨後會執行#:
if (isEligible(bean, beanName)) {
ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
if (!proxyFactory.isProxyTargetClass()) {
evaluateProxyInterfaces(bean.getClass(), proxyFactory);
}
proxyFactory.addAdvisor(this.advisor);
customizeProxyFactory(proxyFactory);
return proxyFactory.getProxy(getProxyClassLoader());
}
先判斷bean是否有需要代理,因為CyclicDependencyService有方法帶有@Async註解就需要代理,返回代理對象是 CyclicDependencyService$$EnhancerBySpringCGLIB$$e66d8f6e@5273 。
返回的代理對象賦值給#doCreateBean方法內的exposedObject,接下來就到了檢查循環依賴的地方了:
if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
else if (!this. && hasDependentBean(beanName)) {
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
for (String dependentBean : dependentBeans) {
if (!(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}
if (!actualDependentBeans.isEmpty()) {
throw new (beanName,
"Bean with name '" + beanName + "' has been injected into other beans [" +
StringUtils.(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.");
}
}
}
}
首先從earlySingletonObjects里拿到前面屬性填充時放入的bean代理( CyclicDependencyService$$EnhancerBySpringCGLIB$$6ed9e2db@4740 ),不為空的話就比較bean和exposedObject,分別是 CyclicDependencyService@4509 和 CyclicDependencyService$$EnhancerBySpringCGLIB$$e66d8f6e@5273 ,很明顯不是同一個對象,然後會判斷屬性和是否有依賴的bean,然後判斷這些bean是否是真實依賴的,一旦存在真實依賴的bean,就會拋出。
總結
總結下Spring解決循環依賴的思路:在創建bean時,對於滿足提前曝光條件的單例,會把該單例的工廠函數對象放入三級緩存中的singletonFactories中;然後在填充屬性時,如果存在循環依賴,必然會嘗試獲取該單例,也就是執行之前放入的工廠函數的匿名實現,這時候拿到的有可能是原bean對象,也有可能是被某些BBP處理過返回的代理對象,會放入三級緩存中的earlySingletonObjects中;接著bean開始初始化,結果返回的有可能是原bean對象,也有可能是代理對象;最後對於滿足提前曝光的單例,如果真的有提前曝光的動作,就會去檢查初始化後的bean對象是不是原bean對象是同一個對象,只有不是的情況下才可能拋出異常。重點就在於 存在循環依賴的情況下,初始化過的bean對象是不是跟原bean是同一個對象 。
從以上的debug過程可以看出,是這個BBP在初始化過程中改變了bean,使得結果bean和原bean不是一個對象,而則是在填充屬性獲取提前曝光的對象時把原始bean緩存起來,返回代理的bean。然後在初始化時執行它的方法時如果傳入的bean是之前緩存的原始bean,就直接返回,不進行代理。如果其他BBP也都沒有改變bean的話,初始化過後的bean就是跟原始bean是同一個對象,這時就會把提前曝光的對象(代理過的)作為最終生成的bean。
https://segmentfault.com/a/1190000018835760
