缓存夹在应用程序和物理数据源之间,用来降低对数据库的访问次数。缓存里是数据源的一份拷贝,运行时先读写缓存,再在特定时刻把两边同步。
介质一般是内存,数据量很大时也会落到硬盘。实现时还要管并发和生命周期,所以既要硬件也要软件。
Hibernate 这边分两层:Session 一级缓存默认开、卸不掉;SessionFactory 二级缓存是可选插件,默认关。查对象时先一级、再二级、最后才打 SQL。
一、缓存
1. Hibernate 缓存机制
Hibernate 缓存包括 Session 缓存和 SessionFactory 缓存。SessionFactory 缓存又分内置和外置。
Session 缓存是内置的,不能卸载,也叫第一级缓存。SessionFactory 内置缓存和 Session 缓存实现上很像:前者是 SessionFactory 一些集合属性里的数据,后者是 Session 集合属性里的数据。内置缓存放映射元数据和预定义 SQL——元数据是映射文件的拷贝,SQL 在初始化阶段根据元数据推出来。内置缓存只读,应用改不了,因此不必和映射文件同步。
SessionFactory 外置缓存是可配置插件,默认不启用。数据是库表拷贝,介质可以是内存或硬盘。外置缓存也叫第二级缓存。
2. 两种缓存
一级缓存默认开启,开发者配置不了、也卸不掉。范围是 Session:同一个 Session 里 get/load 过一次,短时间内再查同一条,直接从缓存拿,不再发 SQL。Session 关掉就没了。
二级缓存是 SessionFactory 级别。SessionFactory 比较重,内存里通常单例。查询结果可以放进二级缓存,即便不是同一个 Session,只要工厂还在,会先查二级,没有再发 SQL。
-
一级属于事务范围,Hibernate 自己管,程序员不用干预。
-
二级属于进程范围和集群范围,可以配置,也能动态卸载/加载。
二、Hibernate 缓存原理
1. 一级缓存:Session 的缓存
Session 内置、不能卸载。生命周期通常对应一个数据库事务或一个应用事务。一级缓存里,持久化类的每个实例都有唯一 OID。
2. 二级缓存:SessionFactory 的缓存
SessionFactory 生命周期覆盖整个应用,所以二级缓存是进程范围或集群范围,可能并发,需要并发访问策略来提供事务隔离级别。它是可选插件,默认不启用。
Hibernate 提供 org.hibernate.cache.CacheProvider,充当缓存插件和框架之间的适配器。
什么样的数据适合放二级缓存:
适合:
1) 很少被修改的数据
2) 不是很重要的数据,允许出现偶尔并发
3) 不会被并发访问的数据
4) 常量数据
不适合:
1) 经常被修改的数据
2) 绝对不允许并发访问的数据,如财务数据
3) 与其他应用共享的数据
3. 延迟加载要解决的两个问题
Hibernate Session 是 java.sql.Connection 的一层封装,一个 Session 对应一个 Connection。
-
HTTP 请求结束后正确关闭 Session(过滤器负责关)。
-
延迟加载必须保证是同一个 Session(Session 绑在 ThreadLocal 上)。
4. 按 ID 查找时怎么用缓存
-
-
-
删除、更新、增加时,同时更新缓存。
5. 一级缓存与二级缓存对比
| 对比项 |
一级缓存 |
二级缓存 |
| 存放数据的形式 |
相互关联的持久化对象 |
对象的散装数据 |
| 缓存的范围 |
事务范围,每个事务都有单独的一级缓存 |
进程范围或集群范围,同一进程或集群内所有事务共享 |
| 并发访问策略 |
每个事务单独一份,无并发,无须策略 |
多事务会同时访问相同数据,必须提供并发策略以保证隔离级别 |
| 数据过期策略 |
对象不会过期,除非应用显式清空 |
必须提供过期策略:内存最大数目、最长存活、最长空闲 |
| 物理介质 |
内存 |
内存和硬盘;内存数目达到 maxElementsInMemory 后其余写入硬盘缓存 |
| 软件实现 |
包含在 Hibernate Session 实现中 |
第三方提供,Hibernate 只给适配器 |
| 启用方式 |
通过 Session 做保存、更新、删除、加载、查询就会启用;批量操作不想走缓存可直接用 JDBC API |
可按单个类或集合配置;经常读、很少改的类才值得开;没配置就不会进二级缓存 |
| 用户管理方式 |
内存有限,要用检索策略限制加载数量;Session.evict() 可清空指定对象,但不推荐 |
选哪些类开二级缓存并设并发策略;选适配器并设过期策略。SessionFactory.evict() 也能清空,但不推荐 |
三、缓存机制应用
1. 一级缓存的管理
evict(Object obj) // 将指定持久化对象从一级缓存清除,释放内存,对象从持久化变为脱管
clear() // 清除一级缓存中所有持久化对象
contains(Object obj)// 判断指定对象是否在一级缓存中
flush() // 刷新一级缓存,使之与数据库同步
2. 一级缓存应用
save() 之后对象会进 Session 缓存;get() / load() 从库取出后也会进缓存。HQL、QBC 查出来的同样如此。
@Test
public void testQueryOneCache() {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
Query query = session.createQuery("FROM Student");
List<Student> list = query.list();
for (Student stu : list) {
System.out.println(stu);
// if (stu.getNumber() != 1) {
// session.evict(stu); // 清除指定缓存对象
// }
}
Student student = (Student) session.get(Student.class, 1);
System.out.println(student);
// 清除 session 缓存两种方式:clear()、关闭 session
session.clear();
System.out.println("--------------------");
Student stu2 = (Student) session.get(Student.class, 1);
System.out.println(stu2);
session.getTransaction().commit();
}
3. 二级缓存的管理
sessionFactory.evict(Customer.class, new Integer(1));
// evict(Class, Serializable) 将某个类指定 ID 的对象从二级缓存清除
sessionFactory.evict(Customer.class);
// evict(Class) 将指定类的所有持久化对象从二级缓存清除
sessionFactory.evictCollection("Customer.orders");
// evictCollection(String) 将指定类的指定集合从二级缓存清除
4. 二级缓存的配置
常用插件:
| 插件 |
Provider |
| EHCache |
org.hibernate.cache.EhCacheProvider |
| OSCache |
org.hibernate.cache.OSCacheProvider |
| SwarmCache |
org.hibernate.cache.SwarmCacheProvider |
| JBossCache |
org.hibernate.cache.TreeCacheProvider |
四、二级缓存以 EHCache 为例
1. 导入 ehcache 包
ehcache-core-2.4.3.jar
hibernate-ehcache-4.3.11.Final.jar
slf4j-api-1.6.1.jar
2. 创建 ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
<!-- 缓存对象超过指定数量后,落到磁盘的地址 -->
<diskStore path="E:/ehcache"/>
<!-- 默认 cache:没有对应特定区域时使用 -->
<defaultCache maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="false"/>
<!-- 指定区域 cache:name 对应 Hibernate 中的区域名 -->
<cache name="com.ittx.hibernate001.model.Student"
eternal="false"
maxElementsInMemory="100"
timeToIdleSeconds="1200"
timeToLiveSeconds="1200"
overflowToDisk="false">
</cache>
</ehcache>
参数含义:name 区域名;maxElementsInMemory 内存最大数目;maxElementsOnDisk 磁盘最大数目;eternal 是否永不过期;overflowToDisk 是否溢出到硬盘;timeToIdleSeconds 空闲多少秒后销毁;timeToLiveSeconds 存活多少秒后销毁;memoryStoreEvictionPolicy 缓存算法。
常见算法三种:
-
LRU(Least Recently Used):新对象替换掉最近很少使用的对象(默认)
-
LFU(Least Frequently Used):替换命中率较低的对象
-
FIFO(First In First Out):替换最早进入二级缓存的对象
3. hibernate.cfg.xml
<!-- 高速缓存提供程序 -->
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
<!-- 设置二级缓存插件 EHCache 的 Provider 类 -->
<property name="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider</property>
<!-- enable second level cache and query cache -->
<property name="hibernate.cache.use_second_level_cache">true</property>
<!-- 启动查询缓存:findall()、list()、Iterator()、createCriteria()、createQuery() 的结果集要缓存必须配这项 -->
<property name="hibernate.cache.use_query_cache">true</property>
<!-- 指定缓存配置文件位置 -->
<property name="hibernate.cache.provider_configuration_file_resource_path">ehcache.xml</property>
<!-- 强制 Hibernate 以更人性化的格式将数据存入二级缓存 -->
<property name="hibernate.cache.use_structured_entries">true</property>
<!-- Hibernate 将收集有助于性能调节的统计数据 -->
<property name="hibernate.generate_statistics">true</property>
4. 配置 model.hbm.xml
在 class 标签下添加:
<cache usage="read-write"/>
5. 测试代码
@Test
public void testQueryCacheTwo() {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
System.out.println("第一次查询Student");
Query q = session.createQuery("from Student");
q.setCacheable(true);
q.list();
System.out.println("放进二级缓存");
session.getTransaction().commit();
session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
System.out.println("第二次查询Student,将不会发出sql");
q = session.createQuery("from Student");
q.setCacheable(true);
q.list();
session.getTransaction().commit();
// 配置文件打开 generate_statistics 后,可以得到二级缓存命中次数等数据
Statistics s = HibernateUtil.getSessionFactory().getStatistics();
System.out.println(s);
System.out.println("put:" + s.getSecondLevelCachePutCount());
System.out.println("hit:" + s.getSecondLevelCacheHitCount());
System.out.println("miss:" + s.getSecondLevelCacheMissCount());
}
关于查询缓存:一级与二级只能缓存实体对象;查询缓存可以缓存结果集(含普通属性)。只有经常使用同样参数的查询才划算,生命周期直到属性被修改为止。对实体结果集,查询缓存只缓存 id。
<!-- 开启查询缓存,默认是关闭的 -->
<property name="hibernate.cache.use_query_cache">true</property>
// 程序中应开启:
Query query = session.createQuery("select s.name from Student s");
query.setCacheable(true); // 开启查询缓存
一句话总结:按 ID 查对象时先 Session 一级、再 SessionFactory 二级、最后才打 SQL;二级缓存用 EHCache 时要同时开 use_second_level_cache,查询缓存还要 setCacheable(true)。