hibernate一級快取和二級快取的區別
快取是介於應用程式和實體資料來源之間,其作用是為了降低應用程式對實體資料來源存取的頻次,從而提高了應用程式的運作效能。快取內的資料是對實體資料來源中的資料的複製,應用程式在運行時從快取讀寫數據,在特定的時刻或事件會同步快取和實體資料來源的資料。
快取的媒體一般是內存,所以讀寫速度很快。但如果快取中存放的資料量非常大時,也會用硬碟作為快取媒體。快取的實作不僅要考慮儲存的介質,還要考慮到管理快取的並發存取和快取資料的生命週期。
Hibernate的快取包括Session的快取和SessionFactory的緩存,其中SessionFactory的快取又可以分為兩類:內建快取和外部快取。 Session的快取是內建的,不能被卸載,也被稱為Hibernate的第一級快取。 SessionFactory的內建快取和Session的快取在實作方式上比較相似,前者是SessionFactory物件的一些集合屬性所包含的數據,後者是指Session的一些集合屬性所包含的資料。 SessionFactory的內建快取中存放了映射元資料和預定義SQL語句,映射元資料是映射檔案中資料的拷貝,而預定義SQL語句是在Hibernate初始化階段根據映射元資料推導出來,SessionFactory的內建快取是只讀取的,應用程式不能修改快取中的映射元資料和預定義SQL語句,因此SessionFactory不需要進行內建快取與映射檔案的同步。 SessionFactory的外接快取是一個可設定的插件。在預設情況下,SessionFactory不會啟用這個外掛程式。外接快取的資料是資料庫資料的拷貝,外接快取的媒體可以是記憶體或硬碟。 SessionFactory的外置快取也被稱為Hibernate的第二級快取。
Hibernate的這兩層快取都位於持久化層,存放的都是資料庫資料的拷貝,那麼它們之間的差別是什麼呢?為了理解二者的區別,需要深入理解持久化層的快取的兩個特性:快取的範圍和快取的並發存取策略。
持久化層的快取的範圍
快取的範圍決定了快取的生命週期以及可以被誰存取。緩存的範圍分為三類。
1 事務範圍:快取只能被目前事務存取。快取的生命週期依賴事務的生命週期,當事務結束時,快取也就結束生命週期。在此範圍下,快取的媒體是記憶體。事務可以是資料庫事務或是應用事務,每個事務都有獨自的緩存,快取內的資料通常採用相互關聯的的物件形式。
2 進程範圍:快取被進程內的所有事務共用。這些事務有可能是並發存取快取,因此必須對快取採取必要的交易隔離機制。快取的生命週期依賴進程的生命週期,在進程結束時,快取也就結束了生命週期。進程範圍的快取可能會存放大量的數據,所以存放的媒體可以是記憶體或硬碟。快取內的資料既可以是相互關聯的物件形式也可以是物件的鬆散資料形式。鬆散的物件資料形式有點類似於物件的序列化數據,但是物件分解為鬆散的演算法比物件序列化的演算法要求更快。
3 叢集範圍:在叢集環境中,快取被一個機器或多個機器的進程共享。快取中的資料被複製到叢集環境中的每個進程節點,進程間透過遠端通訊來保證快取中的資料的一致性,快取中的資料通常採用物件的鬆散資料形式。
對大多數應用來說,應該慎重地考慮是否需要使用叢集範圍的緩存,因為存取的速度不一定會比直接存取資料庫資料的速度快多少。
持久化層可以提供多種範圍的快取。如果在交易範圍的快取中沒有查到相應的數據,也可以到進程範圍或叢集範圍的快取內查詢,如果還是沒有查到,那麼只有到資料庫中查詢。事務範圍的快取是持久化層的第一級緩存,通常它是必需的;進程範圍或叢集範圍的快取是持久化層的第二級緩存,通常是可選的。
持久化層的快取的並發存取策略
當多個並發的事務同時存取持久化層的快取的相同資料時,會造成並發問題,必須採用必要的事務隔離措施。
在進程範圍或叢集範圍的緩存,即第二層緩存,會出現並發問題。因此可以設定以下四種類型的並發存取策略,每種策略對應一種交易隔離等級。
事務型:僅在受管理環境中適用。它提供了Repeatable Read事務隔離等級。對於經常被讀但很少修改的數據,可以採用這種隔離類型,因為它可以防止髒讀和不可重複讀取這類的並發問題。
讀寫型:提供了Read Committed交易隔離等級。僅在非叢集的環境中適用。對於經常被讀但很少修改的數據,可以採用這種隔離類型,因為它可以防止髒讀這類的並發問題。
非嚴格讀寫型:不保證快取與資料庫中資料的一致性。如果存在兩個事務同時存取快取中相同資料的可能,必須為該資料配置一個很短的資料過期時間,從而盡量避免髒讀。對於極少被修改,並且允許偶爾髒讀的數據,可以採用這種並發存取策略。 只讀型:對於從來不會修改的數據,如參考資料,可以使用這種並發存取策略。
事務型並發存取策略是事務隔離等級最高,且只讀型的隔離等級最低。事務隔離等級越高,並發效能就越低。
什麼樣的資料適合存放到第二層快取?
1、很少被修改的數據
2、不是很重要的數據,允許出現偶爾並發的數據
3、不會被並發存取的數據
4、參考資料數據?
1、經常被修改的數據
2、財務數據,絕對不允許出現並發
3、與其他應用共享的數據。
Hibernate的二級緩存
如前所述,Hibernate提供了兩級緩存,第一級是Session的緩存。由於Session物件的生命週期通常對應一個資料庫事務或一個應用事務,因此它的快取是交易範圍的快取。第一層快取是必要的,不允許而且事實上也無法比卸除。在第一級快取中,持久化類別的每個實例都具有唯一的OID。
第二級快取是一個可插拔的的快取插件,它是由SessionFactory負責管理。由於SessionFactory物件的生命週期和應用程式的整個過程對應,因此第二級快取是進程範圍或叢集範圍的快取。這個快取中存放的物件的鬆散資料。第二級物件有可能出現並發問題,因此需要採用適當的並發存取策略,該策略為被快取的資料提供了事務隔離等級。快取適配器用於把具體的快取實現軟體與Hibernate整合。第二級快取是可選的,可以在每個類別或每個集合的粒度上配置第二級快取。
Hibernate的二級快取策略的一般過程如下:
1) 條件查詢的時候,總是發出一個select * from table_name where …. (選擇所有欄位)這樣的SQL語句查詢資料庫,一次取得所有的資料物件。
2) 把所獲得的所有資料物件依照ID放入第二級快取。
3) 當Hibernate根據ID存取資料物件的時候,先從Session一級快取中查;查不到,如果配置了二級緩存,那麼從二級快取中查;查不到,再查詢資料庫,把結果按照ID放入到快取。
4) 刪除、更新、增加資料的時候,同時更新快取。
Hibernate的二級快取策略,是針對於ID查詢的快取策略,對於條件查詢則毫無作用。為此,Hibernate提供了針對條件查詢的Query快取。
Hibernate的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都要從快取中清空。
延遲載入:
<hibernate-mapping> <class name=”com.neusoft.entity.User” table=”user” lazy=”true”> …… </class> </hibernate-mapping>
User user=(User)session.load(User.class,”1”);
System.out.println(user.getName());
当运行到(1)处时,Hibernate并没有发起对数据的查询,如果我们此时通过一些调试工具(比如JBuilder2005的Debug工具),观察此时user对象的内存快照,我们会惊奇的发现,此时返回的可能是User$EnhancerByCGLIB$$bede8986类型的对象,而且其属性为null,这是怎么回事?还记得前面我曾讲过session.load()方法,会返回实体对象的代理类对象,这里所返回的对象类型就是User对象的代理类对象。在Hibernate中通过使用CGLIB,来实现动态构造一个目标对象的代理类对象,并且在代理类对象中包含目标对象的所有属性和方法,而且所有属性均被赋值为null。通过调试器显示的内存快照,我们可以看出此时真正的User对象,是包含在代理对象的CGLIB$CALBACK_0.target属性中,当代码运行到(2)处时,此时调用user.getName()方法,这时通过CGLIB赋予的回调机制,实际上调用CGLIB$CALBACK_0.getName()方法,当调用该方法时,Hibernate会首先检查CGLIB$CALBACK_0.target属性是否为null,如果不为空,则调用目标对象的getName方法,如果为空,则会发起数据库查询,生成类似这样的SQL语句:select * from user where id='1';来查询数据,并构造目标对象,并且将它赋值到CGLIB$CALBACK_0.target属性中。
这样,通过一个中间代理对象,Hibernate实现了实体的延迟加载,只有当用户真正发起获得实体对象属性的动作时,才真正会发起数据库查询操作。所以实体的延迟加载是用通过中间代理类完成的,所以只有session.load()方法才会利用实体延迟加载,因为只有session.load()方法才会返回实体类的代理类对象。
B、 集合类型的延迟加载:
在Hibernate的延迟加载机制中,针对集合类型的应用,意义是最为重大的,因为这有可能使性能得到大幅度的提高,为此Hibernate进行了大量的努力,其中包括对JDK Collection的独立实现,我们在一对多关联中,定义的用来容纳关联对象的Set集合,并不是java.util.Set类型或其子类型,而是net.sf.hibernate.collection.Set类型,通过使用自定义集合类的实现,Hibernate实现了集合类型的延迟加载。为了对集合类型使用延迟加载,我们必须如下配置我们的实体类的关于关联的部分:
<hibernate-mapping> <class name=”com.neusoft.entity.User” table=”user”> ….. <set name=”addresses” table=”address” lazy=”true” inverse=”true”> <key column=”user_id”/> <one-to-many class=”com.neusoft.entity.Arrderss”/> </set> </class> </hibernate-mapping>
通过将
User user=(User)session.load(User.class,”1”); Collection addset=user.getAddresses();
(1)
Iterator it=addset.iterator();
(2)
while(it.hasNext()){ Address address=(Address)it.next(); System.out.println(address.getAddress()); }
当程序执行到(1)处时,这时并不会发起对关联数据的查询来加载关联数据,只有运行到(2)处时,真正的数据读取操作才会开始,这时Hibernate会根据缓存中符合条件的数据索引,来查找符合条件的实体对象。
这里我们引入了一个全新的概念——数据索引,下面我们首先将接一下什么是数据索引。在Hibernate中对集合类型进行缓存时,是分两部分进行缓存的,首先缓存集合中所有实体的id列表,然后缓存实体对象,这些实体对象的id列表,就是所谓的数据索引。当查找数据索引时,如果没有找到对应的数据索引,这时就会一条select SQL的执行,获得符合条件的数据,并构造实体对象集合和数据索引,然后返回实体对象的集合,并且将实体对象和数据索引纳入Hibernate的缓存之中。另一方面,如果找到对应的数据索引,则从数据索引中取出id列表,然后根据id在缓存中查找对应的实体,如果找到就从缓存中返回,如果没有找到,在发起select SQL查询。在这里我们看出了另外一个问题,这个问题可能会对性能产生影响,这就是集合类型的缓存策略。如果我们如下配置集合类型:
<hibernate-mapping> <class name=”com.neusoft.entity.User” table=”user”> ….. <set name=”addresses” table=”address” lazy=”true” inverse=”true”> <cache usage=”read-only”/> <key column=”user_id”/> <one-to-many class=”com.neusoft.entity.Arrderss”/> </set> </class> </hibernate-mapping>
这里我们应用了
User user=(User)session.load(User.class,”1”); Collection addset=user.getAddresses(); Iterator it=addset.iterator(); while(it.hasNext()){ Address address=(Address)it.next(); System.out.println(address.getAddress()); } System.out.println(“Second query……”); User user2=(User)session.load(User.class,”1”); Collection it2=user2.getAddresses(); while(it2.hasNext()){ Address address2=(Address)it2.next(); System.out.println(address2.getAddress()); }
运行这段代码,会得到类似下面的输出:
Select * from user where id='1'; Select * from address where user_id='1'; Tianjin Dalian Second query…… Select * from address where id='1'; Select * from address where id='2'; Tianjin Dalian
我们看到,当第二次执行查询时,执行了两条对address表的查询操作,为什么会这样?这是因为当第一次加载实体后,根据集合类型缓存策略的配置,只对集合数据索引进行了缓存,而并没有对集合中的实体对象进行缓存,所以在第二次再次加载实体时,Hibernate找到了对应实体的数据索引,但是根据数据索引,却无法在缓存中找到对应的实体,所以Hibernate根据找到的数据索引发起了两条select SQL的查询操作,这里造成了对性能的浪费,怎样才能避免这种情况呢?我们必须对集合类型中的实体也指定缓存策略,所以我们要如下对集合类型进行配置:
<hibernate-mapping> <class name=”com.neusoft.entity.User” table=”user”> ….. <set name=”addresses” table=”address” lazy=”true” inverse=”true”> <cache usage=”read-write”/> <key column=”user_id”/> <one-to-many class=”com.neusoft.entity.Arrderss”/> </set> </class> </hibernate-mapping>
此时Hibernate会对集合类型中的实体也进行缓存,如果根据这个配置再次运行上面的代码,将会得到类似如下的输出:
Select * from user where id='1'; Select * from address where user_id='1'; Tianjin Dalian Second query…… Tianjin Dalian
这时将不会再有根据数据索引进行查询的SQL语句,因为此时可以直接从缓存中获得集合类型中存放的实体对象。
C、 属性延迟加载:
在Hibernate3中,引入了一种新的特性——属性的延迟加载,这个机制又为获取高性能查询提供了有力的工具。在前面我们讲大数据对象读取时,在User对象中有一个resume字段,该字段是一个java.sql.Clob类型,包含了用户的简历信息,当我们加载该对象时,我们不得不每一次都要加载这个字段,而不论我们是否真的需要它,而且这种大数据对象的读取本身会带来很大的性能开销。在Hibernate2中,我们只有通过我们前面讲过的面性能的粒度细分,来分解User类,来解决这个问题(请参照那一节的论述),但是在Hibernate3中,我们可以通过属性延迟加载机制,来使我们获得只有当我们真正需要操作这个字段时,才去读取这个字段数据的能力,为此我们必须如下配置我们的实体类:
<hibernate-mapping> <class name=”com.neusoft.entity.User” table=”user”> …… <property name=”resume” type=”java.sql.Clob” column=”resume” lazy=”true”/> </class> </hibernate-mapping>
通过对
String sql=”from User user where user.name='zx' ”; Query query=session.createQuery(sql);
(1)
List list=query.list(); for(int i=0;i<list.size();i++){ User user=(User)list.get(i); System.out.println(user.getName()); System.out.println(user.getResume()); }
(2)
当执行到(1)处时,会生成类似如下的SQL语句:
Select id,age,name from user where name='zx';
这时Hibernate会检索User实体中所有非延迟加载属性对应的字段数据,当执行到(2)处时,会生成类似如下的SQL语句:
Select resume from user where id='1';
这时会发起对resume字段数据真正的读取操作。
更多浅析Java的Hibernate框架中的缓存和延迟加载机制相关文章请关注PHP中文网!