大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。
当前系列: JDBC&Hibernate 修改讲义
#信不信?当个的SQL操作,你用JDBC可能会比Hibernate性能更高跑得更快;但项目越来越复杂,人家用Hibernate可能比你用JDBC跑得更快。

一级缓存

其实就是我们前面讲的Unit Of Work的必然结果。除非StatelessSession,否则就自然存在。

已经加载到entityManager或session(以下简称context)里面的entity,再次获取的时候不需要查询数据库。

先获取一个id=x的Student(所有关联对象都被设置成eager load)

em.find(Student.class, 5);
然后,log演示:

-Resolved object in session cache: [Student#5]

  • 无论entity自己,还是关联的entity(包括集合)都会被缓存
  • 如果是context中没有的entity,还是会继续查询
//不会hit DB,因为已经load
em.find(Student.class, 5);
//不会hit DB,因为已经load
em.find(Bed.class, 3);
//会hit DB,因为当前em中没有id=4的Bed
em.find(Bed.class, 4);
//id=5的学生的scoresId为9和10
em.find(Score.class, 9);    //不会hit
em.find(Score.class, 11);   //会hit

但是,如果我们使用criteria查询呢?

//再次使用criteria用name查询Teacher
CriteriaQuery<Teacher> queryOnTeacher = builder.createQuery(Teacher.class);
Root<Teacher> rootOnTeacher = queryOnTeacher.from(Teacher.class);
//fg正是id=5的Student的老师
queryOnTeacher.where(builder.equal(rootOnTeacher.get(Teacher_.name), "fg"));

//不会hit DB
em.createQuery(queryOnTeacher).getFirstResult();
结果就是SELECT * FROM Teacher WHERE name = "fg"的SQL语句依然会执行,但是

对比name="xy"演示,没有:

Initializing object from ResultSet: [Teacher#4]

等log

@想一想@:为什么?因为:

  • find()方法是根据id查找entity,无论是在context内部,还是整个数据库,id和entity直接有唯一的对应关系
  • criteria是根据where条件查entity,context内部找不到,不保证整个数据库就找不到
所以,criteria的SELECT是一定要执行的。但是,一旦获取到行数据的id,就可以先在context内部比对:如果已有该id的entity,就可以直接拿来用而不用再次生成……


二级缓存

一级缓存的“范围”是基于context的,也就是说,不同的context之间无法共享entity(演示)

EntityManager em2 = entityManagerFactory.createEntityManager();
//因为是“新”的em2,所以一级缓存无法生效
em2.find(Student.class, 5);

但二级缓存就可以(在不同的context)全局共享。

开启

二级缓存需要显式开启,指定cache provider,我们这里用Ehcache:

PS:其他还有官方文档推荐的Infinispan,已经非常流行的分布式缓存redis

首先maven下载包:

<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>2.10.8</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-ehcache</artifactId>
    <version>5.1.17.Final</version>
</dependency>

然后在persistence.xml中指定:

<!-- 开启二级缓存,默认false -->
<property name="use_second_level_cache" value="true"/>
<property name="hibernate.cache.region.factory_class"
	value="org.hibernate.cache.ehcache.EhCacheRegionFactory" />
还需要用@Cacheable标记哪些entity需要被缓存,用@Cache指定具体的缓存方式:
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
public class Student

Hibernate默认的策略是不会缓存任何entity,除非显式声明。该策略可以通过javax.persistence.sharedCache.mode修改(略)

为了演示方便,我们设置格式化的缓存log:

<property name="hibernate.cache.use_structured_entries" value="true"/>
log演示:
  • -Converting second-level cache entry [CacheEntry(Student)[atai,17,true,[Ljava.lang.Object;@22fcaf0b,null,我是阿泰,泰山的泰,^_^,2011-05-01,MONDAY,5,5,null]] into entity : [Student#5]
  • 跨em查询缓存仍然生效

dehydrated

如前所示,我们就Student设置了缓存,再次根据Id获取某Student时,不会SELECT该Student,但还是会SELECT该Student关联的Bed,为什么呢?

其一,Bed上面没有没有@Cacheable!但即使加了@Cacheable:还是不行。

@想一想@:为什么?因为:

  1. Student中没有缓存bedId(Student是inverse端)
  2. 而所有缓存数据以键值对(Map)的形式存放,键就是Id。

单个entity:dehydrated(脱水),只保留基本的数据(排除方法等)信息,如上所示。或者,用图表示

集合

Hibernate默认不缓存entity的关联集合。

如果要缓存的话,在需要缓存的集合上添加@Cache注解:

@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
@ManyToMany(mappedBy = "students", fetch = FetchType.EAGER)
private Set<Teacher> teachers = new HashSet<>();

entity的集合中只缓存每个元素的id,每个具体的元素按单个entity脱水(dehydrated)后缓存:

这是org.hibernate.annotations包下的注解(@Cacheable是javax.persistence下的),它包含一个

CacheConcurrencyStrategy

主要的作用是指定缓存并发策略(CacheConcurrencyStrategy),即当多个context同时访问某缓存entity并对其进行读写操作时,如何处理可能产生的冲突。其原理和我们之前学习的事务隔离级别(复习)非常类似。

PS:Hibernate通过大量精妙的设计来保证缓存数据和数据库数据的一致性,但是如果第三方在Hibernate之外修改数据库,Hibernate无从监控

可选的策略包括:(具体实现依赖于provider,此处简单粗略说明)



说明
性能
数据完整性
READ_ONLY:只读
一旦被缓存,就不能被修改。
  • 如果只是在内存(context)中修改,不提交到数据库,就不会影响其他context
  • 试图同步修改后的缓存数据,就会抛异常

抛异常
NONSTRICT_READ_WRITE:非严格模式的读写
修改就加独占锁,直到更改完成
较高

READ_WRITE:可读可写
修改只加写锁
一般
一般
TRANSACTIONAL:事务级
对可能涉及到的数据都加独占锁



演示代码:

Student atai = em.find(Student.class, 5);
atai.setAge(20);
//transaction.commit();

EntityManager em2 = entityManagerFactory.createEntityManager();		
Student atai2 = em2.find(Student.class, 5);

region和include

region和缓存设置相关,比如:缓存过期时间,最多缓存多少条数据,缓存满了之后如何处理……,这些配置都可以写在ehCache的配置文件(ehcache.xml)里,一个region一个设置。

  1. 我们现在没有指定region,Hibernate默认用该entity类的全名(或集合名)做region。
  2. elCache里也没有entity类的全名对应的region设置,所以使用的就是ehCache的全局默认设置。
<cache name="nh.17bang.Student"
<cache name="normal"
@Cache(region = "normal"
public class Student
elCache以后还会使用,我们后面详解讲。

include指示是否缓存lazy-load的元素。默认是要缓存的,如果不想缓存:

@Cache(include = "non-lazy",
	usage = CacheConcurrencyStrategy.READ_ONLY)


查询缓存

在二级缓存开启的基础上显式开启

<property name="hibernate.cache.use_query_cache" value="true" />
然后一样要在查询上设置
em.createQuery(queryOnTeacher)
	//设置在createQuery()之后
	.setHint("org.hibernate.cacheable", "true");

使用字符串"org.hibernate.cacheable"和"true"是非常难受的一种写法……

建议专门建一个类,存储相关的字符串常量:

public class QueryCacheStrings {
	public final static String CACHABLE = "org.hibernate.cacheable";
	public final static String TRUE = "true";

log演示:

  • 缓存使用的key:
    key: sql: select teacher0_.id as id1_3_, teacher0_.name as name2_3_, teacher0_.age as age3_3_
  • 生成了查询缓存:
    Caching query results in region: org.hibernate.cache.internal.StandardQueryCache; timestamp=6698896483094528注意这个时间戳(timestamp)

和二级缓存的关系

没有开启二级缓存,查询缓存也不会启动。

但是,查询缓存的结果直接独立存储,是像集合那样只存元素id。@想一想@:为什么?

因为查询的结果不一定就是entity的集合啊!可以是multiselect之类的投影呀……

原理

Hibernate仍然会保证缓存数据的正确性(和数据库一致),为了做到这一点,Hibernate需要

  • 记录查询缓存生成时间(以下简称“缓存时间”)和所有相关表最后更改的时间(以下简称“更改时间”)
  • 一旦相关表有更改,就更新其更改时间
  • 将缓存时间和更改时间相比较:
    • 如果缓存生成时间在更改时间之后,缓存仍然有效,可以使用;否则
    • 缓存失效,删除之,重新查询生成新的缓存

所以,如果查询所涉及的任何一张表的更改,都会导致缓存失效;如果表的更改较为频繁,使用查询缓存是得不偿失的。


连接池

Hibernate自带了一个开发用(即能用于生成环境)连接池。log演示:

-HHH10001002: Using Hibernate built-in connection pool (not for production use!)maven下载依赖:

<dependency>
	<groupId>org.hibernate</groupId>
	<artifactId>hibernate-hikaricp</artifactId>
	<version>5.1.17.Final</version>
</dependency>
这会直接下载两个jar包:
  • hikaricp本身

  • hibernate:hibernate对hikaricp的封装

然后在persistence.xml中配置:

  • 让hibernate使用hikaricp:
    <property name="hibernate.connection.provider_class"
    	value="org.hibernate.hikaricp.internal.HikariCPConnectionProvider" />
  • 对hikaricp进行配置:
    <property name="hibernate.hikari.poolName" value="hh.17bang" />
    <property name="hibernate.hikari.minimumIdle" value="2"></property>
    <property name="hibernate.hikari.maximumPoolSize" value="5"></property>

演示:

  • log中没有了build-in Connection pool的提示,反而是hikaricp相关内容,比如:
    -HHH000130: Instantiating explicit connection provider: org.hibernate.hikaricp.internal.HikariCPConnectionProvider
  • 控制台显示n条(由hakari配置决定)连接信息
  • eclipse上显示hikari守护线程:


作业

  1. 结合一起帮的功能,配置缓存并说明为什么这样配置或不配置:
    1. 为entity和集合配置二级缓存,
    2. 查询缓存
  2. 配置hikari连接池


学习笔记
源栈学历
今天学习不努力,明天努力找工作

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 JDBC&Hibernate 中继续学习:

下一课: 已经是最后一课了……

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码