Hibernate 缓存机制
一级缓存和二级缓存
Hibernate 的缓存包括2个级别的缓存:
- 一级缓存:Session 级别,默认开启;
- 二级缓存:SessionFactory 级别,可选;
1)一级缓存
Session 级别的缓存总是开启的,在开发过程中一般不需要进行控制,当应用保存持久化实体时,Session 不会立即把这种改变 flush 到数据库,而是缓存在 Session 一级缓存中,直到一级缓存区放满 ,或者调用Session.close() 或 Session.flush()时,缓存区中的数据才会一次性地 flush 到数据库。
Session 这种缓存方式方式可以减少程序对数据库的连接次数,已减少数据库连接资源的占用,提高数据库访问性能;
Session 还提供了以下的一系列方法用于一级缓存操作:
void | flush() | 将 Session 缓存中的对象强制 flush 到底层数据库 |
void | evict(Object obj) | 将对象 obj 从 Session 缓存中剔除 |
boolean | contains(Object obj) | 返回 Session 缓存中是否含有某个对象 |
void | clear() | 清除 Session 缓存中的所有对象 |
更多方法参见 API;
在一些特殊情况下,比如处理一个大对象(占用很大内存)时,可以调用 Session.evict (Object obj)方法,将对象从一级缓存中剔除,如以下示例:
BigObject bigObjList = session.createQuery("from Atricle");
while(bigObjList.next()){
BigObject obj = (BigObject)bigObjList.get(0);
//use obj to do something
.....
session.evit(obj); //将obj从Session一级缓存中剔除
}
2)二级缓存
SessionFactory 级别的缓存是全局性的,所有的 Session 共享该缓存,默认关闭,需要显式开启;
一旦开启二级缓存,程序中 Session 要查询数据时,会先查找一级缓存,再查找二级缓存,只有但一级、二级缓存中都不存在需要的数据时,再查找底层数据库;
配置二级缓存
这里以EhCache为例,其他的还用如Radis等第三方实现;
① 将该第三方缓存 jar 包添加到项目依赖中;
② 开启缓存,配置缓存种类;
在 hibernate.cfg.xml 文件中添加以下属性:
<property name="hibernate.cache.use_second_level_cache">true</property>
配置第三方的缓存类,这里使用 EhCache:
<property name="hibernate.cache.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
③ 完成缓存实现库的自身配置
对于 Ehcache 缓存库,需要在src目录下添加一个xml配置文件,示例如下:
src/ehcahe.xml
<?xml version='1.0' encoding='UTF-8'?>
<ehcache>
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxElementsInMemory="10240"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskPersistent="false"
/>
</ehcache>
以上一些属性的对照:
maxElementsInMemory="10000" :缓存中放置对象的最大数量
eternal="false" :缓存是否永久有效
timeToIdleSeconds="120":对象缓存在多少s没有被使用会被清除
timeToLiveSeconds="120":缓存对象在过期前可被缓存多少s
diskPersistent="false":设置缓存是否持久化到硬盘中,由<diskStore>设置路径
Cache 类用于对二级缓存进行管理,该对象由 SessionFactory.getChache() 方法获取;
evictEntity(Class classType, Serializable id) | 在二级缓存中 消除指定的实体的指定id实例 evictEntity("User.class",id) |
evictEntityRegion(Class classType) | 在二级缓存中 消除指定实体的所有实例 |
evictCollection(String role, Serializable ownerIdentifier) | 在二级缓存中 消除指定id的 实体关联实体实例,如: evictEntity("User.class") |
evictCollection(String role) | 在二级缓存中 消除所有的指定的 实体关联实体实例,如: evictCollection("User.Article") |
查询缓存
一级、二级缓存是针对整个实体进行缓存的,它不会缓存普通属性,如果需要对普通属性进行缓存,可以使用查询缓存;
默认查询缓存是关闭的,开启查询缓存,需要在 hibernate.cfg.xml 中添加以下配置:
<property name="hibernate.cache.use_query_cache">true</property>
在程序中调用查询缓存的代码如下:
Session session = HibernateUtil.currentSession();
Transaction tran = session.beginTransaction();
List list = session.createQuery("select u.name,u.age from User u")
.setCacheable(true) //对本次查询开启查询缓存
.list();
Iterator iterator = session.createQuery("select u.name,u.age from User u")
.setCacheable(true) //对本次查询开启查询缓存
.iterate();
批量处理的内存溢出问题
当在一次事务操作中进行大量的实体插入、更新时(如一次Session事务中向数据库提交1000000个实体的插入),很有可能程序会抛出 OutOfMemoryException 栈溢出异常,如以下程序:
public class test {
public static void main(String[] args){
Session session = HibernateUtil.currentSession();
Transaction tran = session.beginTransaction();
for(int i=0;i<1000000;i++){
Users user = new Users();
.....
session.save(user);
}
HibernateUtil.closeSession();
}
}
这是由于这些实体实例在事务提交之前,会缓存在Session的一级缓存中,这个一级缓存是有限的;
对于这种情况,一种解决思路是可以在Session保存的一定量的实体后,将Session一级缓存强制flush到数据库,同时清空Session缓存,如下:
public class Test {
public static void main(String[] args){
Session session = HibernateUtil.currentSession();
Transaction tran = session.beginTransaction();
for(int i=0;i<1000000;i++){
User user = new User();
user.setName("assad");
user.setAge(new Random().nextInt(80));
session.save(user); //Session 保存实体
if(i % 30 == 0){ //每隔保存30个实体,Session缓存强制写入,同时清空
session.flush();
session.clear();
}
}
HibernateUtil.closeSession();
}
}
同时还需要手动关闭 SessionFactory 的二级缓存,防止实体缓存到二级缓存造成二级缓存溢出;
对于批量更新,如果返回多行数据,可以使用 ScrollableResults 来储存返回结果,利用游标所在带来的新能优势,如下:
public class Test {
public static void main(String[] args){
Session session = HibernateUtil.currentSession();
Transaction tran = session.beginTransaction();
//使用ScrollableResults储存结果集
ScrollableResults users = session.createQuery("from User")
.setCacheMode(CacheMode.IGNORE)
.scroll(ScrollMode.FORWARD_ONLY);
int count = 0;
while( users.next()){
Users user = (Users)users.get(0);
user.setName("new name"+ count++);
if( count % 30 == 0){
session.flush();
session.clear();
}
}
HibernateUtil.closeSession();
}
}