Hibernate持久層框架使用【八】效能優化與快取
一級快取:
在hibernate中一級快取是預設開啟的,它與session相關,例如當你對資料庫中的資料進行查詢後,它會將查詢到的物件儲存到記憶體中,再次查詢時便直接從記憶體中讀取,從記憶體中讀取的速度顯然比從資料庫中讀取資料要快得多。
為了證明,可以寫一個測試類對快取進行測試
public class Test { public static void main(String[] args) { // TODO Auto-generated method stub Configuration configuration = new Configuration().configure(); SessionFactory sessionFactory = configuration.buildSessionFactory(); Session session = sessionFactory.openSession(); Transaction transaction = session.beginTransaction(); //記錄開始時的時間 Long startTime = System.currentTimeMillis(); //查詢id為1的資料 Student student = (Student) session.get(Student.class, 1); //記錄結束時的時間 Long endTime = System.currentTimeMillis(); //輸出所用的時間 System.out.println("所用時間:"+(endTime-startTime)); System.out.println(student.getName()); //再次查詢這條資料 //記錄開始時的時間 Long startTime2 = System.currentTimeMillis(); //查詢id為1的資料 Student student2 = (Student) session.get(Student.class, 1); //記錄結束時的時間 Long endTime2 = System.currentTimeMillis(); //輸出所用的時間 System.out.println("所用時間:"+(endTime2-startTime2)); System.out.println(student2.getName()); transaction.commit(); session.close(); sessionFactory.close(); } }
上面的程式碼首先記錄了開始查詢前的時間,並且查詢了id為1的一條資料,記錄查詢結束時的時間(此時這個查詢到的Student物件已經被儲存到記憶體中了)
最後,輸出所用的時間
同樣複製上面這段程式碼,查詢同樣一條id為1的資料,記錄下所用的時間並輸出
執行程式
Hibernate: select student0_.sid as sid1_1_0_, student0_.age as age2_1_0_, student0_.name as name3_1_0_ from student student0_ where student0_.sid=? 所用時間:172 關羽 所用時間:1 關羽
可以看到,在第一次查詢資料時生成了查詢語句,所用時間172ms
再次查詢時不僅沒有生成查詢語句,所用的時間也僅為1ms
可見,從記憶體中讀取的速度要快得多
既然一級快取是通過session儲存在記憶體中的,那麼只要將儲存在session記憶體中的所有物件清空,再次查詢時就會重寫生成查詢語句進行查詢了,同樣使用上面的程式碼,在第一次查詢與第二次查詢之間加入這一行程式碼:
//清空session中儲存的所有物件
session.clear();
再次執行程式
Hibernate: select student0_.sid as sid1_1_0_, student0_.age as age2_1_0_, student0_.name as name3_1_0_ from student student0_ where student0_.sid=? 所用時間:184 關羽 Hibernate: select student0_.sid as sid1_1_0_, student0_.age as age2_1_0_, student0_.name as name3_1_0_ from student student0_ where student0_.sid=? 所用時間:4 關羽
控制檯確實輸出了兩次查詢語句,同樣是往資料庫查詢資料,這裡第一條查詢所用的時間為184ms,而第二條雖然沒有像前面從記憶體中讀取那麼快,但也只用了4ms,這是因為第一次查詢時hibernate需要與資料庫進行通訊,而接下來的操作則不再需要
二級快取:
二級快取同樣會將物件快取到記憶體中,但是如果當記憶體存滿時,它會將資料存放到磁碟中
它是於SessionFactory相關的,所以即使Session被清空,查詢出的資料依舊會使用二級快取通過SessionFactory儲存在記憶體中,直到SessionFactory被關閉或快取到達時效
在hibernate中二級快取是預設關閉的,在使用二級快取時需要加入第三方的jar包才能使用
首先開啟從官網下載好的hibernate,解壓(詳細參考第一篇部落格【配置hibernate】)
解壓後進入lib資料夾下的optional資料夾,這裡有一些可選的第三方jar包資料夾,選擇ehcache,將資料夾下的三個jar包拷貝到專案中引入(我的分別是slf4j-api-1.6.1.jar、hibernate-ehcache-4.3.8.Final.jar、ehcache-core-2.4.3.jar)
接著開啟hibernate-release-4.3.8.Final\project\etc這個資料夾,將ehcache.xml這個快取配置檔案拷貝到專案的src路徑下
開啟這個配置檔案,大概是這樣的:
<ehcache>
<diskStore path="D:\\ehcache"/>
<defaultCache
maxElementsInMemory="100"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
/>
<cache name="sampleCache1"
maxElementsInMemory="3"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="true"
/>
</ehcache>
這個配置檔案儲存了使用這個快取的一些配置
其中,<diskStore path="D:\\ehcache"/>表示當你記憶體存滿時,暫存的磁碟路徑
另外還有兩個類似的配置
<defaultCache
maxElementsInMemory="100"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
/>
這個配置表示預設的快取配置,即當你不宣告使用哪個快取配置時,預設使用這個配置
在預設配置中:
maxElementsInMemory="100" 表示最大的快取數量
eternal="false" 表示是否永久儲存
timeToIdleSeconds="120" 表示閒置時間,120s
timeToLiveSeconds="120" 表示存活時間,120s(時間結束時快取清空)
overflowToDisk="true" 表示當超過快取數量時是否儲存到磁碟,這裡寫了true,(儲存的路徑就是上面宣告的diskStore)
還有一個自定義的配置
<cache name="sampleCache1"
maxElementsInMemory="3"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="true"
/>
和前面的預設配置一樣,只不過加了一個name,在後面使用快取時可以通過這個name來宣告使用這個配置,為了方便測試,這裡的快取數量我改為3
快取配置完成後再開啟你的hibernate配置檔案hibernate.cfg.xml
加入下面這兩個配置來啟用快取,以及啟用哪個快取
<!-- 配置開啟二級快取 -->
<property name="hibernate.cache.use_second_level_cache">true</property>
<!-- 配置二級快取的提供商 -->
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
這兩個配置開啟hibernate-release-4.3.8.Final\project\etc下的hibernate.properties按ctrl+f搜尋cache就能找到了
最後,還需要進行一個配置,來對持久化類使用快取
<!-- 對持久化類Student使用快取 -->
<class-cache usage="read-write" class="domain.Student" region="sampleCache1"/>
這個配置要加在持久化類的mapping對映下面
測試二級快取:
public class EhCache {
public static void main(String[] args) {
// TODO Auto-generated method stub
Configuration configuration = new Configuration().configure();
SessionFactory sessionFactory = configuration.buildSessionFactory();
query(sessionFactory);
query(sessionFactory);
//sessionFactory.close();
}
public static void query(SessionFactory sessionFactory){
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
//記錄開始時的時間
Long startTime = System.currentTimeMillis();
//查詢id為1的資料
List<Object> students = session.createSQLQuery("select * from Student").list();
//記錄結束時的時間
Long endTime = System.currentTimeMillis();
//輸出所用的時間
System.out.println("所用時間:"+(endTime-startTime));
transaction.commit();
session.close();
}
}
因為這個二級快取是與SessionFactory相關的,為了排除一級快取的影響,這裡寫了一個方法,傳入SessionFactory,在查詢完成後關閉Session,在main方法中重複執行兩次,因為每次呼叫這個方法執行完成後都會將session關閉,所以排除了一級快取的影響。
執行程式,看到控制檯列印的資料
Hibernate:
select
*
from
Student
所用時間:95
Hibernate:
select
*
from
Student
所用時間:0
前後兩次查詢,每次查詢完成後都關閉了session,但是查詢所用的時間卻截然不同,第一次使用了95ms,第二次則顯示0ms
在前面對持久化類進行快取配置時,使用了自定義的快取配置,即最大快取數為3,但這裡,我資料庫查詢出來的結果卻不只3條,那麼剩下的物件應該被儲存到了前面配置的磁碟路徑中了,開啟D:\ehcache,果然看到了兩個data檔案,這些檔案就是二級快取超出記憶體時暫存到磁碟的資料了
main方法中還有一行程式碼被註釋掉了,即//sessionFactory.close();
因為二級快取是和sessionFactory相關的,如果開啟這行程式碼,在執行完畢後將sessionFactory關閉,那麼在關閉時,暫存到磁碟的資料也會被清空
效能優化:
我們在對資料表中插入資料時,一般這些插入的物件會被儲存到session中,而session會使用JVM分配到的記憶體進行儲存,直到transaction進行commit,再一次性將session中的物件提交到資料庫中,當session中儲存的資料量超過記憶體時,就會發生記憶體溢位,導致資料無法成功儲存
例如下面插入1億條資料
public class Test01 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Configuration configuration = new Configuration().configure();
SessionFactory sessionFactory = configuration.buildSessionFactory();
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
for(int i=0; i<100000000; i++)
{
Student student = new Student();
student.setName("馬超"+i);
student.setAge(i);
session.save(student);
}
transaction.commit();
session.close();
sessionFactory.close();
}
}
如果不斷地執行下去,最後將會發生記憶體溢位的錯誤
為了解決這個問題,我們可以對此進行一些優化,如下,在for迴圈中加入一個if語句,判斷每10條資料進行一次同步(將資料同步到資料庫中)
public class Test01 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Configuration configuration = new Configuration().configure();
SessionFactory sessionFactory = configuration.buildSessionFactory();
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
for(int i=0; i<100000000; i++)
{
Student student = new Student();
student.setName("馬超"+i);
student.setAge(i);
session.save(student);
if (i % 10 == 0) {
session.flush();
}
}
transaction.commit();
session.close();
sessionFactory.close();
}
}