hibernate提供的一級緩存
hibernate是一個線程對應一個session,一個線程可以看成一個用戶。也就是說session級緩存(一級緩存)只能給一個線程用,別的線程用不了,一級緩存就是和線程綁定了。
hibernate一級緩存生命周期很短,和session生命周期一樣,一級緩存也稱session級的緩存或事務級緩存。如果tb事務提交或回滾了,我們稱session就關閉了,生命周期結束了。
緩存和連接池的區別:緩存和池都是放在內存里,實現是一樣的,都是為了提高性能的。但有細微的差別,池是重量級的,里面的數據是一樣的,比如一個池里放100個Connection連接對象,這個100個都是一樣的。緩存里的數據,每個都不一樣。比如讀取100條數據庫記錄放到緩存里,這100條記錄都不一樣。
緩存主要是用于查詢
//同一個session中,發出兩次load方法查詢
Student student = (Student)session.load(Student.class, 1);
System.out.println("student.name=" + student.getName());
//不會發出查詢語句,load使用緩存
student = (Student)session.load(Student.class, 1);
System.out.println("student.name=" + student.getName()); |
第二次查詢第一次相同的數據,第二次load方法就是從緩存里取數據,不會發出sql語句到數據庫里查詢。
//同一個session,發出兩次get方法查詢
Student student = (Student)session.get(Student.class, 1);
System.out.println("student.name=" + student.getName());
//不會發出查詢語句,get使用緩存
student = (Student)session.get(Student.class, 1);
System.out.println("student.name=" + student.getName()); |
第二次查詢第一次相同的數據,第二次不會發出sql語句查詢數據庫,而是到緩存里取數據。
//同一個session,發出兩次iterate查詢實體對象
Iterator iter = session.createQuery
("from Student s where s.id<5").iterate();
while (iter.hasNext()) {
Student student = (Student)iter.next();
System.out.println(student.getName());
}
System.out.println("--------------------------------------");
//它會發出查詢id的語句,但不會發出根據id查詢學生的語句,因為iterate使用緩存
iter = session.createQuery("from Student s where s.id<5").iterate();
while (iter.hasNext()) {
Student student = (Student)iter.next();
System.out.println(student.getName());
} |
一說到iterater查詢就要立刻想起:iterater查詢在沒有緩存的情況下會有N+1的問題。
執行上面代碼查看控制臺的sql語句,第一次iterate查詢會發出N+1條sql語句,第一條sql語句查詢所有的id,然后根據id查詢實體對象,有N個id就發出N條語句查詢實體。
第二次iterate查詢,卻只發一條sql語句,查詢所有的id,然后根據id到緩存里取實體對象,不再發sql語句到數據庫里查詢了。
//同一個session,發出兩次iterate查詢,查詢普通屬性
Iterator iter = session.createQuery(
"select s.name from Student s where s.id<5").iterate();
while (iter.hasNext()) {
String name = (String)iter.next();
System.out.println(name);
}
System.out.println("--------------------------------------");
//iterate查詢普通屬性,一級緩存不會緩存,所以發出查詢語句
//一級緩存是緩存實體對象的
iter = session.createQuery
("select s.name from Student s where s.id<5").iterate();
while (iter.hasNext()) {
String name = (String)iter.next();
System.out.println(name);
} |
執行代碼看控制臺sql語句,第一次發出N+1條sql語句,第二次還是發出了N+1條sql語句。因為一級緩存只緩存實體對象,tb不會緩存普通屬性,所以第二次還是發出sql查詢語句。
//兩個session,每個session發出一個load方法查詢實體對象
try {
session = HibernateUtils.getSession();
session.beginTransaction();
Student student = (Student)session.load(Student.class, 1);
System.out.println("student.name=" + student.getName());
session.getTransaction().commit();
}catch(Exception e) {
e.printStackTrace();
session.getTransaction().rollback();
}finally {
HibernateUtils.closeSession(session);
}
第二個session調用load方法
try {
session = HibernateUtils.getSession();
session.beginTransaction();
Student student = (Student)session.load(Student.class, 1);
//會發出查詢語句,session間不能共享一級緩存數據
//因為他會伴隨著session的消亡而消亡
System.out.println("student.name=" + student.getName());
session.getTransaction().commit();
}catch(Exception e) {
e.printStackTrace();
session.getTransaction().rollback();
}finally {
HibernateUtils.closeSession(session);
} |
第一個session的load方法會發出sql語句查詢實體對象,第二個session的load方法也會發出sql語句查詢實體對象。因為session間不能共享一級緩存的數據,所以第二個session的load方法查詢相同的數據還是要到數據庫中查詢,因為它找不到第一個session里緩存的數據。
//同一個session,先調用save方法再調用load方法查詢剛剛save的數據
Student student = new Student();
student.setName("張三");
//save方法返回實體對象的id
Serializable id = session.save(student);
student = (Student)session.load(Student.class, id);
//不會發出查詢語句,因為save支持緩存
System.out.println("student.name=" + student.getName()); |
先save保存實體對象,再用load方法查詢剛剛save的實體對象,則load方法不會發出sql語句到數據庫查詢的,而是到緩存里取數據,因為save方法也支持緩存。當然前提是同一個session。
//大批量的數據添加
for (int i=0; i<100; i++) {
Student student = new Student();
student.setName("張三" + i);
session.save(student);
//每20條更新一次
if (i % 20 == 0) {
session.flush();
//清除緩存的內容
session.clear();
}
} |
大批量數據添加時,會造成內存溢出的,因為save方法支持緩存,每save一個對象就往緩存里放,如果對象足夠多內存肯定要溢出。一般的做法是先判斷一下save了多少個對象,如果save了20個對象就對緩存手動的清理緩存,這樣就不會造成內存溢出。
注意:清理緩存前,要手動調用flush方法同步到數據庫,否則save的對象就沒有保存到數據庫里。
注意:大批量數據的添加還是不要使用hibernate,這是hibernate弱項。可以使用jdbc(速度也不會太快,只是比hibernate好一點),或者使用工具產品來實現,比如oracle的Oracle SQL Loader,導入數據特別快。
Hibernate 二級緩存
二級緩存需要sessionFactory來管理,它是進初級的緩存,所有人都可以使用,它是共享的。
二級緩存比較復雜,一般用第三方產品。hibernate提供了一個簡單實現,用Hashtable做的,只能作為我們的測試使用,商用還是需要第三方產品。
使用緩存,肯定是長時間不改變的數據,如果經常變化的數據放到緩存里就沒有太大意義了。因為經常變化,還是需要經常到數據庫里查詢,那就沒有必要用緩存了。
hibernate做了一些優化,和一些第三方的緩存產品做了集成。老師采用EHCache緩存產品。
和EHCache二級緩存產品集成:EHCache的jar文件在hibernate的lib里,我們還需要設置一系列的緩存使用策略,需要一個配置文件ehcache.xml來配置。這個文件放在類路徑下。
//默認配置,所有的類都遵循這個配置
<defaultCache
//緩存里可以放10000個對象
maxElementsInMemory="10000"
//過不過期,如果是true就是永遠不過期
eternal="false"
//一個對象被訪問后多長時間還沒有訪問就失效(120秒還沒有再次訪問就失效)
timeToIdleSeconds="120"
//對象存活時間(120秒),如果設置永不過期,這個就沒有必要設了
timeToLiveSeconds="120"
//溢出的問題,如果設成true,緩存里超過10000個對象就保存到磁盤里
overflowToDisk="true"
/> |
我們也可以對某個對象單獨配置:
<cache name="com.bjpowernode.hibernate.Student"
maxElementsInMemory="100"
eternal="false"
timeToIdleSeconds="10000"
timeToLiveSeconds="10000"
overflowToDisk="true"
/> |
還需要在hibernate.cfg.xml配置文件配置緩存,讓hibernate知道我們使用的是那個二級緩存。
<!-- 配置緩存提供商 -->
<property name="hibernate.cache.provider_class">
org.hibernate.cache.EhCacheProvider</property>
<!-- 啟用二級緩存,這也是它的默認配置 -->
<property name="hibernate.cache.use_second_level_cache">
true</property> |
啟用二級緩存的配置可以不寫的,因為默認就是true開啟二級緩存。
必須還手動指定那些實體類的對象放到緩存里在hibernate.cfg.xml里:
//在<sessionfactory>標簽里,在<mapping>標簽后配置
<class-cache class="com.bjpowernode.hibernate.Student"
usage="read-only"/> |
或者在實體類映射文件里:
//在<class>標簽里,<id>標簽前配置
<cache usage="read-only"/> |
usage屬性表示使用緩存的策略,一般優先使用read-only,表示如果這個數據放到緩存里了,則不允許修改,如果修改就會報錯。這就要注意我們放入緩存的數據不允許修改。因為放緩存里的數據經常修改,也就沒有必要放到緩存里。
使用read-only策略效率好,因為不能改緩存。但是可能會出現臟數據的問題,這個問題解決方法只能依賴緩存的超時,比如上面我們設置了超時為120秒,120后就可以對緩存里對象進行修改,而在120秒之內訪問這個對象可能會查詢臟數據的問題,因為我們修改對象后數據庫里改變了,而緩存卻不能改變,這樣造成數據不同步,也就是臟數據的問題。
第二種緩存策略read-write,當持久對象發生變化,緩存里就會跟著變化,數據庫中也改變了。這種方式需要加解鎖,效率要比第一種慢。
還有兩種策略,請看hibernate文檔,最常用還是第一二種策略。
二級緩存測試代碼演示:注意上面我們講的兩個session分別調用load方法查詢相同的數據,第二個session的load方法還是發了sql語句到數據庫查詢數據,這是因為一級緩存只在當前session中共享,也就是說一級緩存不能跨session訪問。
//開啟二級緩存,二級緩存是進程級的緩存,可以共享
//兩個session分別調用load方法查詢相同的實體對象
try {
session = HibernateUtils.getSession();
session.beginTransaction();
Student student = (Student)session.load(Student.class, 1);
System.out.println("student.name=" + student.getName());
session.getTransaction().commit();
}catch(Exception e) {
e.printStackTrace();
session.getTransaction().rollback();
}finally {
HibernateUtils.closeSession(session);
}
try {
session = HibernateUtils.getSession();
session.beginTransaction();
Student student = (Student)session.load(Student.class, 1);
//不會發出查詢語句,因為配置二級緩存,session可以共享二級緩存中的數據
//二級緩存是進程級的緩存
System.out.println("student.name=" + student.getName());
session.getTransaction().commit();
}catch(Exception e) {
e.printStackTrace();
session.getTransaction().rollback();
}finally {
HibernateUtils.closeSession(session);
} |
如果開啟了二級緩存,那么第二個session調用的load方法查詢第一次查詢的數據,是不會發出sql語句查詢數據庫的,而是去二級緩存中取數據。
//開啟二級緩存
//兩個session分別調用get方法查詢相同的實體對象
try {
session = HibernateUtils.getSession();
session.beginTransaction();
Student student = (Student)session.get(Student.class, 1);
System.out.println("student.name=" + student.getName());
session.getTransaction().commit();
}catch(Exception e) {
e.printStackTrace();
session.getTransaction().rollback();
}finally {
HibernateUtils.closeSession(session);
}
try {
session = HibernateUtils.getSession();
session.beginTransaction();
Student student = (Student)session.get(Student.class, 1);
//不會發出查詢語句,因為配置二級緩存,session可以共享二級緩存中的數據
//二級緩存是進程級的緩存
System.out.println("student.name=" + student.getName());
session.getTransaction().commit();
}catch(Exception e) {
e.printStackTrace();
session.getTransaction().rollback();
}finally {
HibernateUtils.closeSession(session);
} |
注意:二級緩存必須讓sessionfactory管理,讓sessionfactory來清除二級緩存。sessionFactory.evict(Student.class);//清除二級緩存中所有student對象,sessionFactory.evict(Student.class,1);//清除二級緩存中id為1的student對象。
如果在第一個session調用load或get方法查詢數據后,把二級緩存清除了,那么第二個session調用load或get方法查詢相同的數據時,還是會發出sql語句查詢數據庫的,因為緩存里沒有數據只能到數據庫里查詢。
我們查詢數據后會默認自動的放到二級和一級緩存里,如果我們想查詢的數據不放到緩存里,也是可以的。也就是說我們可以控制一級緩存和二級緩存的交換。
session.setCacheMode(CacheMode.IGNORE);禁止將一級緩存中的數據往二級緩存里放。
還是用上面代碼測試,在第一個session調用load方法前,執行session.setCacheMode(CacheMode.IGNORE);這樣load方法查詢的數據不會放到二級緩存里。那么第二個session執行load方法查詢相同的數據,會發出sql語句到數據庫中查詢,因為二級緩存里沒有數據,一級緩存因為不同的session不能共享,所以只能到數據庫里查詢。
上面我們講過大批量的數據添加時可能會出現溢出,解決辦法是每當天就20個對象后就清理一次一級緩存。如果我們使用了二級緩存,光清理一級緩存是不夠的,還要禁止一二級緩存交互,在save方法前調用session.setCacheMode(CacheMode.IGNORE)。
二級緩存也不會存放普通屬性的查詢數據,這和一級緩存是一樣的,只存放實體對象。session級的緩存對性能的提高沒有太大的意義,因為生命周期太短了。
Hibernate 查詢緩存
一級緩存和二級緩存都只是存放實體對象的,如果查詢實體對象的普通屬性的數據,只能放到查詢緩存里,查詢緩存還存放查詢實體對象的id。
查詢緩存的生命周期不確定,當它關聯的表發生修改,查詢緩存的生命周期就結束。這里表的修改指的是通過hibernate修改,并不是通過數據庫客戶端軟件登陸到數據庫上修改。
hibernate的查詢緩存默認是關閉的,如果要使用就要到hibernate.cfg.xml文件里配置:
<property name="hibernate.cache.use_query_cache">true</property> |
并且必須在程序中手動啟用查詢緩存,在query接口中的setCacheable(true)方法來啟用。
//關閉二級緩存,沒有開啟查詢緩存,采用list方法查詢普通屬性
//同一個sessin,查詢兩次
List names = session.createQuery("select s.name from Student s")
.list();
for (int i=0; i<names.size(); i++) {
String name = (String)names.get(i);
System.out.println(name);
}
System.out.println("-----------------------------------------");
//會發出sql語句
names = session.createQuery("select s.name from Student s")
.setCacheable(true)
.list();
for (int i=0; i<names.size(); i++) {
String name = (String)names.get(i);
System.out.println(name);
} |
上面代碼運行,由于沒有使用查詢緩存,而一、二級緩存不會緩存普通屬性,所以第二次查詢還是會發出sql語句到數據庫中查詢。
現在開啟查詢緩存,關閉二級緩存,并且在第一次的list方法前調用setCacheable(true),并且第二次list查詢前也調用這句代碼,可以寫出下面這樣:
List names = session.createQuery("select s.name from Student s")
.setCacheable(true)
.list(); |
其它代碼不變,運行代碼后發現第二次list查詢普通屬性沒有發出sql語句,也就是說沒有到數據庫中查詢,而是到查詢緩存中取數據。
//開啟查詢緩存,關閉二級緩存,采用list方法查詢普通屬性
//在兩個session中調用list方法
try {
session = HibernateUtils.getSession();
session.beginTransaction();
List names = session.createQuery("select s.name from Student s")
.setCacheable(true)
.list();
for (int i=0; i<names.size(); i++) {
String name = (String)names.get(i);
System.out.println(name);
}
session.getTransaction().commit();
}catch(Exception e) {
e.printStackTrace();
session.getTransaction().rollback();
}finally {
HibernateUtils.closeSession(session);
}
System.out.println("----------------------------------------");
try {
session = HibernateUtils.getSession();
session.beginTransaction();
//不會發出查詢語句,因為查詢緩存和session的生命周期沒有關系
List names = session.createQuery("select s.name from Student s")
.setCacheable(true)
.list();
for (int i=0; i<names.size(); i++) {
String name = (String)names.get(i);
System.out.println(name);
}
session.getTransaction().commit();
}catch(Exception e) {
e.printStackTrace();
session.getTransaction().rollback();
}finally {
HibernateUtils.closeSession(session);
} |
運行結果是第二個session發出的list方法查詢普通屬性,沒有發出sql語句到數據庫中查詢,而是到查詢緩存里取數據,這說明查詢緩存和session生命周期沒有關系。
//開啟緩存,關閉二級緩存,采用iterate方法查詢普通屬性
//在兩個session中調用iterate方法查詢 |
運行結果是第二個session的iterate方法還是發出了sql語句查詢數據庫,這說明iterate迭代查詢普通屬性不支持查詢緩存。
//關閉查詢緩存,關閉二級緩存,采用list方法查詢實體對象
//在兩個session中調用list方法查詢 |
運行結果第一個session調用list方法查詢實體對象會發出sql語句查詢數據,因為關閉了二級緩存,所以第二個session調用list方法查詢實體對象,還是會發出sql語句到數據庫中查詢。
//開啟查詢緩存,關閉二級緩存
//在兩個session中調用list方法查詢實體對象 |
運行結果第一個session調用list方法查詢實體對象會發出sql語句查詢數據庫的。第二個session調用list方法查詢實體對象,卻發出了很多sql語句查詢數據庫,這跟N+1的問題是一樣的,發出了N+1條sql語句。為什么會出現這樣的情況呢?這是因為我們現在查詢的是實體對象,查詢緩存會把第一次查詢的實體對象的id放到緩存里,當第二個session再次調用list方法時,它會到查詢緩存里把id一個一個的拿出來,然后到相應的緩存里找(先找一級緩存找不到再找二級緩存),如果找到了就返回,如果還是沒有找到,則會根據一個一個的id到數據庫中查詢,所以一個id就會有一條sql語句。
注意:如果配置了二級緩存,則第一次查詢實體對象后,會往一級緩存和二級緩存里都存放。如果沒有二級緩存,則只在一級緩存里存放。(一級緩存不能跨session共享)
//開啟查詢緩存,開啟二級緩存
//在兩個session中調用list方法查詢實體對象 |
運行結果是第一個session調用list方法會發出sql語句到數據庫里查詢實體對象,因為配置了二級緩存,則實體對象會放到二級緩存里,因為配置了查詢緩存,則實體對象所有的id放到了查詢緩存里。第二個session調用list方法不會發出sql語句,而是到二級緩存里取數據。
查詢緩存意義不大,查詢緩存說白了就是存放由list方法或iterate方法查詢的數據。我們在查詢時很少出現完全相同條件的查詢,這也就是命中率低,這樣緩存里的數據總是變化的,所以說意義不大。除非是多次查詢都是查詢相同條件的數據,也就是說返回的結果總是一樣,這樣配置查詢緩存才有意義。