eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存
1、前言
这个我在操作的过程中遇到了很多知识点,不只是一篇文章了,我都看了一下然后外加自己研究操作做了一个实践纪录。
2、项目实践
2.1 jar包拉取
2.2 数据库结构
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for teacher
-- ----------------------------
DROP TABLE IF EXISTS `teacher`;
CREATE TABLE `teacher` (
`id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
`subject` varchar(255) NOT NULL,
`favorite_food` varchar(255) NOT NULL,
`height` double(30,0) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of teacher
-- ----------------------------
INSERT INTO `teacher` VALUES ('2', 'li', 'chinese', 'apple', '168');
INSERT INTO `teacher` VALUES ('3', '移动', '电子科技', '钢铁', '1000000');
INSERT INTO `teacher` VALUES ('10', '老王', '英文', '桃子', '175');
2.3 创建配置文件hibernate.cfg.xml
hibernate.cfg.xml,这里主要看缓存那里即可。这里的mapping我把class注释是为了先不使用注释的方法,如果使用注释的方法实现2级缓存也可以,不过到时候类上要加入@Cache注解
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- mysql的驱动包 -->
<property name="connection.driver_class">
com.mysql.cj.jdbc.Driver
</property>
<!-- 数据库连接地址,这里数据库以参数形式传入,以;分隔不同参数 -->
<property name="connection.url">
jdbc:mysql://localhost:3306/student?serverTimezone=UTC
</property>
<!-- 数据库账号 -->
<property name="connection.username">
root
</property>
<!-- 数据库密码 -->
<property name="connection.password">
root
</property>
<!-- 数据库连接池 -->
<property name="connection.pool_size">
10
</property>
<!-- 日志文件中是否展示sql -->
<property name="show_sql">
true
</property>
<!-- 设置Hibernate SQL方言 -->
<property name="dialect">
org.hibernate.dialect.MySQLDialect
</property>
<!-- 其实这个hibernate.hbm2ddl.auto参数的作用主要用于:自动创建|更新|验证数据库表结构。如果不是此方面的需求建议set value="none"。 -->
<!-- create:每次加载hibernate时都会删除上一次的生成的表,然后根据你的model类再重新来生成新表,哪怕两次没有任何改变也要这样执行,这就是导致数据库表数据丢失的一个重要原因。 -->
<!-- create-drop : 每次加载hibernate时根据model类生成表,但是sessionFactory一关闭,表就自动删除。-->
<!-- update:最常用的属性,第一次加载hibernate时根据model类会自动建立起表的结构(前提是先建立好数据库),以后加载hibernate时根据 model类自动更新表结构,即使表结构改变了但表中的行仍然存在不会删除以前的行。要注意的是当部署到服务器后,表结构是不会被马上建立起来的,是要等 应用第一次运行起来后才会。 -->
<!-- validate : 每次加载hibernate时,验证创建数据库表结构,只会和数据库中的表进行比较,不会创建新表,但是会插入新值。-->
<property name="hbm2ddl.auto">
update
</property>
<!-- Enable Hibernate's automatic session context management -->
<property name="current_session_context_class">thread</property>
<!-- 是否启用查询缓存 -->
<property name="cache.use_query_cache">true</property>
<!-- 是否开启2级缓存 -->
<property name="cache.use_second_level_cache">true</property>
<!-- 二级缓存中的数据的存储格式,默认为false 设置为true,则便于人工查看二级缓存,但是有性能影响 -->
<property name="cache.use_structured_entries">true</property>
<!-- 缓存工厂用哪个 -->
<property name="cache.region.factory_class">ehcache</property>
<mapping resource="com/czx/cache/pojo/Teacher.hbm.xml"/>
<!-- <mapping class="com.czx.cache.pojo.Teacher"/> -->
</session-factory>
</hibernate-configuration>
2.4 创建ehcache.xml
ehcache.xml,这个文件可以不写,不写就是使用我这里的default缓存。
这里可能会有人不理解为什么空闲时间跟生存时间不一致,为什么要设置这两个?因为你很有可能生存了9分钟,然后被访问了一次,空闲时间重置了,这个时候就不需要挂掉被删掉,还是要缓存。同时成立才要被删除这种设计挺不错的。
<ehcache>
<!-- 设置创建cache.data文件的目录的路径。如果路径是Java系统属性,则替换为它在运行的VM中的值。 -->
<!-- 将转换以下属性: -->
<!-- user.home-用户的主目录 -->
<!-- user.dir-用户当前的工作目录 -->
<!-- java.io.tmpdir-默认临时文件路径 -->
<diskStore path="./target/tmp"/>
<!--默认缓存配置。这些将应用于通过编程方式创建的缓存缓存管理器。
defaultCache需要以下属性:
maxInMemory-设置将在内存中创建的最大对象数
eternal-设置元素是否永恒。如果是永恒的,则忽略超时,元素永不过期。
timeToIdleSeconds—设置元素在过期之前的空闲时间。仅用于如果元素不是永恒的。空闲时间现在是-上次访问时间
timeToLiveSeconds-设置元素过期前的生存时间。仅用于如果元素不是永恒的。TTL现在是创建时间
overflowToDisk—设置在内存缓存中执行操作时元素是否可以溢出到磁盘已达到最大内存限制。-->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
/>
<!--预定义缓存。在此处添加缓存配置设置。
如果没有缓存配置,则在CacheManager启动
defaultCache需要以下属性:
name-设置缓存的名称。这用于标识缓存。它必须是独一无二的。
maxInMemory-设置将在内存中创建的最大对象数
eternal-设置元素是否永恒。如果是永恒的,则忽略超时,元素永不过期。
timeToIdleSeconds—设置元素在过期之前的空闲时间。仅用于如果元素不是永恒的。空闲时间现在是-上次访问时间
timeToLiveSeconds-设置元素过期前的生存时间。仅用于如果元素不是永恒的。TTL现在是创建时间
overflowToDisk—设置在内存缓存中执行操作时元素是否可以溢出到磁盘已达到最大内存限制。-->
<!-- 名为sampleCache1的示例缓存,此缓存在内存中最多包含10000个元素。如果元素空闲时间超过10秒且寿命超过20秒将过期。
如果有超过10000个元素,它将溢出到磁盘缓存,在这种配置中,它将到达java.io.tmp所在的任何位置在您的系统上定义。在标准Linux系统上,这将是/tmp-->
<cache name="sampleCache1"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="10"
timeToLiveSeconds="20"
overflowToDisk="true"
/>
<!-- 名为sampleCache2的示例缓存,这个缓存包含1000个元素。元素将永远保存在内存中。它们没有过期。-->
<cache name="sampleCache2"
maxElementsInMemory="1000"
eternal="true"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
overflowToDisk="false"
/> -->
<!-- 将缓存的配置放置在以下位置 -->
</ehcache>
2.5 创建Teacher类
Teacher,
@org.hibernate.annotations.Cache标注的3个属性
usage,设置二级缓存的并发策略,并发策略后面单拎出来说
region,设置二级缓存的区域,自定义的cache名称
include,设置是否缓存lazy的数据,all表示缓存lazy的数据,non-lazy表示只缓存非lazy的数据
package com.czx.cache.pojo;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
@Entity
@Table(name = "teacher")
//@Cache( usage=CacheConcurrencyStrategy.READ_WRITE,region="sampleCache1")
public class Teacher {
@Id
@Column(name = "id")
private Integer id;
@Column(name = "name")
private String name;
@Column(name = "subject")
private String subject;
@Column(name = "favorite_food")
private String favoriteFood;
@Column(name = "height")
private Double height;
public Teacher(Integer id, String name, String subject, String favoriteFood, Double height) {
super();
this.id = id;
this.name = name;
this.subject = subject;
this.favoriteFood = favoriteFood;
this.height = height;
}
public Teacher() {
super();
}
@Override
public String toString() {
return "Teacher [id=" + id + ", name=" + name + ", subject=" + subject + ", favoriteFood=" + favoriteFood
+ ", height=" + height + "]";
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getFavoriteFood() {
return favoriteFood;
}
public void setFavoriteFood(String favoriteFood) {
this.favoriteFood = favoriteFood;
}
public Double getHeight() {
return height;
}
public void setHeight(Double height) {
this.height = height;
}
}
2.6 Teacher.hbm.xml
Teacher.hbm.xml,
cache 的3个属性
usage,设置二级缓存的并发策略,并发策略后面单拎出来说
region,设置二级缓存的区域,自定义的cache名称
include,设置是否缓存lazy的数据,all表示缓存lazy的数据,non-lazy表示只缓存非lazy的数据
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.czx.cache.pojo.Teacher" table="teacher">
<meta attribute="class-description">
一个teacher类的描述处理
</meta>
<!-- 设置缓存策略 -->
<cache usage="read-write" region="sampleCache1"/>
<id name="id" column="id" type="integer">
<generator class="native"></generator>
</id>
<property name="name" column="name" type="string"></property>
<property name="subject" column="subject" type="string"></property>
<property name="favoriteFood" column="favorite_food" type="string"></property>
<property name="height" column="height" type="double"></property>
</class>
</hibernate-mapping>
2.7 创建测试CacheLearn
CacheLearn,这里我加入线程休眠是为了测试2级缓存是否过期,没啥问题的,自己注释打开就能得到不一样的结果了
package com.czx.cache;
import java.util.Date;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import com.czx.cache.pojo.Teacher;
public class CacheLearn {
private static SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
public static void main(String[] args) {
CacheLearn cacheLearn = new CacheLearn();
cacheLearn.cache007();
}
public void cache007() {
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
Teacher t = session.load(Teacher.class, 3);
System.out.println(t.getFavoriteFood());
Thread tt =new Thread();
transaction.commit();
// System.out.println(new Date());
//弄个线程处理休眠下,这里毫秒单位来的
// try {
// tt.sleep(60000L);
// }catch (Exception e) {
// // TODO: handle exception
// }
// System.out.println(new Date());
Session session2 = sessionFactory.openSession();
Transaction transaction2 = session2.beginTransaction();
Teacher t2 = session2.load(Teacher.class, 3);
System.out.println(t2);
transaction2.commit();
session.close();
}
}
3、1级2级缓存相关概念以及缓存策略
下面有个图我们客户端通过session进行查询操作,这个时候我们会先去找一级缓存有没有,如果有就直接返回,如果没有就去找2级缓存有没有,如果有也可以直接返回给客户端。都没有的情况下去找数据库做相关操作,操作完成数据存到session1级缓存中去,然后在存到2级缓存去,最后返回给客户端。
3.1一级缓存
第一级缓存是 Session 缓存并且是一种强制性的缓存,所有的要求都必须通过它。Session 对象在它自己的权利之下,在将它提交给数据库之前保存一个对象。
如果你对一个对象发出多个更新,Hibernate 会尝试尽可能长地延迟更新来减少发出的 SQL 更新语句的数目。如果你关闭 session,所有缓存的对象丢失,或是存留,或是在数据库中被更新。
3.2 二级缓存
第二级缓存是一种可选择的缓存并且第一级缓存在任何想要在第二级缓存中找到一个对象前将总是被询问。第二级缓存可以在每一个类和每一个集合的基础上被安装,并且它主要负责跨会话缓存对象。
3.3 缓存的并发策略
并发策略
一个并发策略是一个中介,它负责保存缓存中的数据项和从缓存中检索它们。如果你将使用一个二级缓存,你必须决定,对于每一个持久类和集合,使用哪一个并发策略。
缓存并发策略,可以得知这里面的级别是从高到低的,Transactional最高最严谨,阻止脏读不可重复读,遇到下面就越不严谨,根据项目的具体情况去选择合适的。
名称 | 含义 |
---|---|
Transactional(事务型) | 仅在受管理环境下适用.它提供了RepeatableRead事务隔离级别.对于经常读但是很少被修改的数据,可以采用这种隔离类型,因为它可以防止脏读和不可重复读 |
Read-write(读写型) | 提供ReadCommited数据隔离级别.对于经常读但是很少被修改的数据,可以采用这种隔离类型,因为它可以防止脏读 |
Nonstrict-read-write(非严格读写) | 不保证缓存与数据库中数据的一致性.提供ReadUncommited事务隔离级别,对于极少被修改,而且允许脏读的数据,可以采用这种策略 |
Read-only(只读型) | 提供Serializable数据隔离级别,对于从来不会被修改的数据,可以采用这种访问策略 |
3.4 缓存提供者
下面这个其实主要看他能否集群,支持什么集群就够了。
缓存名称 | 描述 |
---|---|
EHCache | 它能在内存或硬盘上缓存并且集群缓存,而且它支持可选的 Hibernate 查询结果缓存。 |
OSCache | 支持在一个单独的 JVM 中缓存到内存和硬盘,同时有丰富的过期策略和查询缓存支持。 |
warmCache | 一个基于 JGroups 的聚集缓存。它使用集群失效但是不支持 Hibernate 查询缓存。 |
JBoss Cache | 一个也基于 JGroups 多播库的完全事务性的复制集群缓存。它支持复制或者失效,同步或异步通信,乐观和悲观锁定。Hibernate 查询缓存被支持。 |
下面这个的话是不同的缓存提供者支持什么类型的缓存策略。每一个缓存提供者都不和每个并发策略兼容。以下的兼容性矩阵将帮助你选择一个合适的组合。
策略/提供者 | Read-only | Nonstrictread-write | Read-write | Transactional |
---|---|---|---|---|
EHCache | X | X | X | |
OSCache | X | X | X | |
SwarmCache | X | X | ||
JBoss Cache | X | X |
4、说到2级缓存,那就要说1+n问题(这个了解一下就好了)
我先说一下1+n是什么问题吧。
网络上有的说是如果你查询一个集合,但是呢,由于你查询的只是一个集合,返回的是一个id集合并没有真的去细查,到了真正要使用的时候,又会再去查一次数据库。
举例说明:
如果你使用的是iterate这个方法,那么就会出现这个问题,结果如下,你可以看看。(这个iterate已经过期了,了解一下看看吧)
Iterator<Teacher> iterator = session.createQuery("from Teacher").iterate();
for(;iterator.hasNext();) {
System.out.println(iterator.next());
}
但是,当你使用查询的list方法不会有1+n问题,所以我感觉没啥用,而且iterate方法也已经过期。这个1+n问题也可以通过2级缓存解决。
4.1 list方法的缓存
list() 默认情况只会放入缓存,不会从一级缓存中取!
使用查询缓存,可以让list()查询从二级缓存中取!
1、开启查询缓存后查询的list()可以从二级缓存中拿数据(list()不能从一级缓存[session缓存]中拿数据),但是不主动去拿,需要设置setCacheable(true)
5、说到缓存,就不得不说get和load方法的区别
get和load都是通过id获取到对应的数据。
关于这个get说法众说纷纭,各有各的看法,我自己操作看看才能知道结果。百度有的说get可以查2级,有人说不行,我自己测试不就知道了。
在我配置好了2级缓存的情况下,执行以下代码(以下是方法,调用一下即可)
这里我一开始使用get获取数据,关闭session,在获取一次数据看看,结果如下,显而易见,这个get是可以查2级缓存的。
public void cacheTest() {
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
Teacher t = session.get(Teacher.class, 3);
System.out.println(t);
transaction.commit();
session.close();
Session session2 = sessionFactory.openSession();
Transaction transaction2 = session2.beginTransaction();
Teacher t2 = session2.get(Teacher.class, 3);
System.out.println(t2);
transaction2.commit();
session2.close();
}
5.1 对于Hibernate的get方法。
这里说下其实本质就是如果要查数据,先查看一下session一级缓存有没有数据,如果没有就去2级缓存中找,都没有再去查找数据库。其中每一个session是独立开来的,如果你关闭了session,那么里面的缓存也会丢失,但是2级缓存的介质不一样所以还能存起来,所以如果加入了2级缓存那么只要查一次就算清空session之后也还能查到数据。
这里有一个特例,如果数据库没有数据,那么就没法缓存,每一次去查都要去到数据库查为null的数据,这样就会造成缓存穿透。
看到这里如果你对上面概念还是不了解在建议看看下面我举得例子。
第一次查数据的情况:
首先启动session---->
然后去session缓存(一级缓存)中找。第一次是没有的---->
所以去更下级缓存中找,2级缓存有没有呢?其实还能继续往下找更高级的缓存,但是这里以2级缓存为例就够了。---->
缓存中找不到了,那么查数据库去,如果没有对应的数据,那么也会返回null,正常有数据就能返回对应的对象。然后将数据缓存到session缓存中。
连续二次查数据的情况下(没有关闭session,在一个session中查数据,这里2级缓存用不到):
第一次查询结束---->
接着查相同的hql/sql语句---->
去session缓存中找,找到了---->
返回对应的对象即可
连读二次查数据的情况(在不同的session)
第一次查询数据结束---->
关闭session---->
新建一个新的sessionA---->
使用sessionA查数据---->
sessionA缓存中没有数据,去查找数据库---->
将数据库数据返回
连读二次查数据的情况(在不同的session,启用了2级缓存)
第一次查询数据结束---->
关闭session–>
新建一个新的sessionA---->
使用sessionA查数据----->
sessionA中没有数据,去2级缓存中找,找到了---->
将2级缓存中内容返回
5.2 对于Hibernate的load方法。
如果你使用了get方法返回数据那么必定会有是数据库的查询语句,但是如果你使用的是load方法,那么就未必是了。
当我们使用session.load()方法来加载一个对象时,此时并不会发出SQL语句,当前得到的这个对象其实是一个代理对象,这个代理对象只保存了实体对象的id值,只有当我们要使用这个对象(例如要打印这个数据),得到其它属性时,这个时候才会发出SQL语句,从数据库中去查询对象。如果你只是要获取id值,那么也并不会访问数据库。
实际上工作流程是这样的,这类似懒加载,我们一开始加载数据都去缓存中找,先一级后2级,找不到了,那么也先会返回一个代理对象,里面只带有对象id。到了我们要使用该对象(不是id的其他属性)的时候,那么才会真正去向数据库发请求,然后将数据缓存到session中去。第二次查找如果缓存没了那么跟第一次查找一样。缓存还在进行第二次查找,加载数据去缓存中找,返回一个对象的引用,到了要使用该对象的时候可以直接从缓存中获取了
第一次查找数据
使用load方法获取数据----->
去缓存中找数据,没有就返回一个代理对象---->
该代理对象带id,如果你要使用该对象除了id以外的属性那么就要去查询数据库------>
数据库获取到了数据,返回并将数据存到session缓存中去
第二次查找数据,缓存在
使用load方法获取数据------>
去缓存中找数据,发现有数据,于是就返回该对象的引用----->
然后获取属性可以去缓存中拿了。
5.3 说完它们的不同工作流程,那么来说一下它们如果出错会报什么异常吧
上面流程说过了,get是会从数据库获取对象的,那么如果数据库没有这个对象,我还要调用这个对象的属性,那么会报什么异常呢?java.lang.NullPointerException,空指针异常,是null的。
load方法会返回一个代理对象,那么如果数据库没有,我还要调用这个对象的属性会报什么异常?org.hibernate.ObjectNotFoundException,因为我们的代理对象只有id,其他属性都是没有的呀。
项目地址
这篇不是很建议看项目,因为我在里面不停的做不同情况下的测试,不过如果你想要看,那也行吧。
https://gitee.com/mrchen13427566118/ssh_hibernate_learn.git里面的ssh_hibernate_cache。
打开方式可以看这篇
https://blog.csdn.net/weixin_43987277/article/details/116936221里面的第三点。