概述
Hibernate Cache 对于提高应用的性能是非常有用的。缓存的目标就是减少数据库的查询,从而减少应用的吞吐时间。Hibernate提供如下几种缓存类型
- 一级缓存(First Level Cache): Hibernate一级缓存是和
Session
对象紧密关联的。Hibernate一级缓存是默认可用的,并且不能关闭它。但是Hibernate提供了可以删除指定object的方法。任何一个在一个session钟缓存的对象在其他session中是不可见的,当session关闭后所有缓存的对象也会丢失。
- 二级缓存(Second Level Cache): 一级缓存的作用域是
Session
,而二级缓存的作用域是SessionFactory
,这就意味着缓存可以被同一个Session Factory的不同 Session 是可以同时共享缓存实例的。当一个二级缓存被设定,将会发生如下:- 如果一个是你实例已经出现在了一级缓存中,则直接返回该实例
- 如果一个实例在一级缓存中并没有出现,但是相应的实例状态(instace state)被缓存在了二级缓存中,数据将从二级缓存中得到,实例将会被组装和返回。
- 查询缓存: 将常用的、变化不频繁的查询结果缓存起来。
一级缓存
Session session = sessionFactory.openSession();
//第一次查询数据库
session.get(Employee.class, 1L);
Thread.sleep(2000);
//第二次不查询数据库
session.get(Employee.class, 1L);
session.close();
在同一个session中当对象的实例存在时将会直接返回实例,并不会再次执行查询数据库操作。但是hibernate提供了清除缓对象实例的方法,它们是:
- clear() : 该方法会清除session一级缓存的所有对象实例
- evict(Object object) :该方法清除一级缓存的指定对象
Session session = sessionFactory.openSession();
//第一次查询数据库
session.get(Employee.class, 1L);
Thread.sleep(2000);
//清除一级缓存
session.clear();
// or session.evict(employee);
//第二次查询数据库,因为一级缓存已经被清除
session.get(Employee.class, 1L);
session.close();
二级缓存
二级缓存的作用域是 Session Factory
一个事务产生的二级缓存可以在两一个事务中直接访问到。现在做如下测试(设置缓存策略为只读):
Session session = sessionFactory.openSession();
Session otherSession = sessionFactory.openSession();
Statistics statistics = sessionFactory.getStatistics();
statistics.setStatisticsEnabled(true);
//开启第一个事务
Transaction transaction = session.beginTransaction();
//第一次查询数据库
Employee employee= session.get(Employee.class, 1L);
print("第一次从数据库中查询",statistics);
//第二次从一级缓冲中查询
session.get(Employee.class, 1L);
print("第二次从一级缓冲中查询",statistics);
//清除一级缓存
session.evict(employee);
/从二级缓存中查询
session.get(Employee.class, 1L);
print("从二级缓存中查询",statistics);
//提交第一个事务
transaction.commit();
session.close();
//第二个事务
Transaction otherTransaction = otherSession.beginTransaction();
otherSession.get(Employee.class, 1L);
print("另一个事务",statistics);
otherSession.get(Employee.class, 2L);
print("查询另一条记录",statistics);
//提交第二个事务
otherTransaction.commit();
otherSession.close();
最终结果为:
- 第一次get的时候缓存中没有,那么从数据库中读一条记录;
- 第二次get的时候是从一级中读取(不读数据库);
- 当调用evict释放查出的数据后在get会从二级缓存中查(不读数据库);
- 当开启另一个事务再get时依旧是从二级缓存中读取(二级缓存跨事务);
- 当查询另一条记录时一、二级缓存中都没有,所以直接在数据库中查询。
Region Factory
Hibernate的二级缓存是对真实的缓存提供者(cache provider)透明的,org.hibernate.cache.spi.RegionFactory
接口囊括了缓存提供者应该实现的各种细节,该接口其实是Hibernate和缓存提供者的一个桥梁。比较知名的缓存提供者有:
- Ehcache : 成熟、使用范围广,本地缓存,单节点
- Hazelcast :分布式、多节点
- Infinispan : 分布式、多节点
- Hibernate Redis : Redis实现的Hibernate二级缓存框架
Hibernate二级缓存策略
只读(READ_ONLY): 当实体数据从来不会改变的时候使用(当尝试更新该实体对象时会抛出异常),该策略比较简单,但性能最佳,适用于那些不需要改动的静态数据。
不严格读写(ONSTRICT_READ_WRITE): 当事务提交后受影响的数据发生变化后缓存才会被更新,因此策略无法保证强一致性,存在一个短暂的窗口期,在这个期间可能从缓存中获取的数据已经是脏数据了。如果应用知识偶尔需要更新数据,并且不需要严格的事务隔离,可以考虑使用该种策略。缓存实例没有被加锁,如果两个事务同时修改数据,我们不知道我们到底拿到了什么数据,这种策略不能保证缓存中的数据和数据库中的数据一致。最好设置缓存的有效期。
读写(READ_WRITE): 这种策略通过加锁保持了数据的强一致性,当一个被缓存的实体数据发生变化,锁将会被添加到缓存中,直到事务被提交才释放该锁,此时当事务访问加锁的实体数据时将会直接访问数据库。
事务(TRANSACTIONAL): 这种策略应用于JTA环境中
缓存管理
如果没有设置缓存过期(expiration)和缓存释放(eviction)策略,缓存将会一致增加直到耗尽所有内存。通常情况下Hibernate将缓存管理的职责转交给缓存提供者(Cache Provider)。
集合的缓存
默认情况下集合是不会被缓存的,只有明确地标注为可缓存,该集合对象才能被缓存。
缓存状态内部的内部表征
实体并没有缓存实体的对象实例到二级缓存中,而是拆散(disassembled)的状态,返回缓存的时候需要重新组装(assembly)。
- 二级缓存中没有保存ID(主键),主键被以cache key的身份保存。
- 被@Transient修饰的属性不会被缓存
- 集合没有被缓存
- 没有关联关系的属性会被以他原来的形式缓存
- 在多对一、 一对一的关联关系的只有外键会缓存
这种设计只是反映了潜在的对应关系,这样提高了空间利用率,同时也方便关联的实体类的协调。
集合缓存的内部表征
在一对多,多对多的集合需要显式地指出才会被缓存。其实Hibernate将每一个集合的缓存放在其他的缓存域中,而不是该实体对象所在的缓存域中,集合缓存域的名称由类名+
集合的属性名组成,通过域的名字可以查看到缓存的数据。
还有需要指出的是,只有集合中各个对象的主键会被缓存,然后通过这些主键再去查找对象的其他属性,因此最好把集合中的实体类也设置成可被缓存。
查询缓存
可以HQL 的查询结果也缓存起来,当要经常查询很少发生变大的数据集时做查询缓存是很有用的。要启动查询缓存只要设置hibernate.cache.use_query_cache
属性为true
即可。
查询缓存的最佳实践
进行查询缓存的时候只有实体的主键会被缓存,所以强烈推荐使用二级缓存。
Hibernate会为每一个查询参数设置一个缓存,因此如果一个查询有很多的查询参数组合,那么这个查询不适合做查询缓存。
如果实体对应的数据库的数据会经常发生变动,那么该查询不适合做查询缓存,因为当数据库中的数据发生变化,缓存将会无效,不管已经发生变化的实例是否已经缓存。
所有的查询缓存被缓存在
rg.hibernate.cache.internal.StandardQueryCache
缓存域下。当实体或者集合将要被缓存的时候,可以根据需要自定义缓存参数。可以为每个查询缓存设置域名称,以便为不同的查询缓存提供不同的设置。Hibernate在域
org.hibernate.cache.spi.UpdateTimestampsCache
保存着数据库的最后一个更新的时间戳。当在使用查询缓存的时候这个作用域非常重要,因为Hibernate通过这个来判断缓存的实体是否已经过期。在该缓存域中的属于不应该被释放或者过期处理,因为这里的缓存的数据是为了和在表中数据查询做协调的。最好为这个缓存域设置关闭自动释放和过期处理,因为它也消耗不了太多空间。
参考资料
http://www.baeldung.com/hibernate-second-level-cache
https://www.journaldev.com/2969/hibernate-caching-first-level-cache
https://www.journaldev.com/2980/hibernate-ehcache-hibernate-second-level-cache
最后
感谢阅读,写作能力有限,如有需要可以扫描下方二维码并关注公众账号与作者取得联系。