• 缓存就是数据交换的缓冲区(称之为Cache),它先于内存与CPU交换数据,因此速率很快。当某一硬件要读取数据时,会首先从缓存中查找需要的数据,如果找到了则直接执行,找不到的话则从内存中找。由于缓存的运行速度比内存快得多,故缓存的作用就是帮助硬件更快地运行。

1 缓存范围分类

  • 应用程序中根据缓存的范围,可以将缓存分为三类:

1.1 事务范围缓存

  • 事务范围缓存,即一级缓存,是单Session缓存。其只能被当前事务访问,每个事务都有各自的缓存。缓存的生命周期依赖于事务的生命周期:当事务结束时,缓存的生命周期也会结束。事务范围的缓存使用内存作为存储介质。
  • Hibernate中的一级缓存就属于事务范围。

1.2 应用范围缓存

  • 应用范围缓存,即二级缓存,是单SessionFactory缓存。其可以被应用程序内的所有事务共享访问。缓存的生命周期依赖于应用的声明周期,当应用结束时,缓存的生命周期同时结束。应用范围的缓存可以使用内存或者硬盘作为存储介质。
  • Hibernate的二级缓存就属于应用范围。

1.3 集群范围缓存

  • 集群范围缓存,是多SessionFactory缓存。在集群环境中,缓存被一个机器或者多个机器的进程共享,缓存中的数据被复制到集群环境中的每个进程节点,进程间通过远程通信来保证缓存中的数据的一致性,缓存中的数据通常采用对象的松散数据形式。
  • 有些Hibernate的二级缓存第三方插件支持集群范围缓存。

2 一级缓存

2.1 什么是一级缓存?

  • 一级缓存,就是Session缓存,其实就是内存中的一块空间,在这个内存空间中存放了相互关联的Java对象。
  • Session缓存是事务级缓存,伴随着事务的开启而开启,伴随着事务的关闭而关闭。Session缓存由Hibernate进行管理。
  • Session缓存,是Hibernate内置的。是不能被程序员取消的。即,只要使用Hibernate,就一定要使用,更确切地说,就一定在使用Session缓存。
  • 当程序调用Session的load()方法时,get()方法、save()方法、saveOrUpdate()方法、update()方法或者是Query方法时,Hibernate会对实体对象进行缓存。
  • 当通过get()或者load()方法查询实体对象时,Hibernate会首先到缓存中查询,在找不到实体对象的情况下,Hibernate才会发出SQL语句到DB中查询。从而提高Hibernate的使用效率。

2.2 一级缓存管理相关方法

  • 与一级缓存相关的方法如下,它们均为Session实例的方法。 1、evict(Object o):从Session中删除指定对象。 2、clear():无参数,将Session缓存清空。 3、contains(Object o):判断指定对象是否在Session中存在。 4、flush():无参数,将Session中对象状态同步到DB中。

2.3 一级缓存的存在性证明

  • 在同一个Session中连续两次查询同一个对象,Hibernate只会发出一条select语句。

3 快照

3.1 什么是快照?

  • 快照,即副本。Hibernate中的快照,即数据库的副本。快照中的数据由Hibernate自己维护。快照中的数据始终保持和DB中数据一致,不过由代码对副本中的内容进行修改。其作用主要是为了在处理数据更新时,将session中的数据与DB中的数据始终保持一致),以此来判断是否真正执行update语句。
  • 当代码通过session的查询方法调用,将数据加载到内存后,框架会将此数据存于Session缓存中,当然快照中也有该数据的副本。session缓存中数据是可以修改的,但是快照中的数据时不能够修改的,始终保持和DB中数据一致。默认情况下,当事务在提交时,会首先对比Session缓存中数据和快照中的数据。若不一致,则说明数据发生更新,会将Session缓存中数据通过update语句更新至DB中。当然,快照中数据也会更新。若一致,则说明数据未发生改变,无需做数据同步。
  • 举例:snapshot
	//快照测试
	@Test
	public void testSnapshot() {
		try {
			session.beginTransaction();
			
			//执行查询,会将结果保存于session缓存中,并在快照中保存一个副本
			Student student = session.get(Student.class, 1);
			//修改数据
			student.setName("巴拉巴拉");
			//提交时会与快照中数量对比,数据不同,执行update的同时,也会在快照中保存一份副本
			session.getTransaction();
			
			session.getTransaction().commit();
		} catch (Exception e) {
			e.printStackTrace();
			session.getTransaction().rollback();
		}
	}

3.2 Session的刷新和同步

  • Session的刷新是指,Session缓存中数据的更新。Session的同步是指,将Session缓存中的数据同步更新到DB中。执行同步的时间点只有一个:事务的提交。
  • 当代码中执行对Session中现有数据的修改操作,即update()与delete()语句后,Session缓存并不会马上刷新,即不会马上执行update与delete的SQL语句,而是在某个时间点到来时,才会刷新缓存。更新缓存中数据。刷新时间点主要有三个: 1、执行Query查询; 2、执行session.flush(); 3、执行事务的提交;
  • 不过,需要注意的是,增删改操作,当刷新时间点带来时是否马上进行缓存更新,各自情况还是不同的。 1、删除操作:对于删除操作,当刷新时间点到来时,会马上执行缓存更新,即马上执行delete语句。 2、修改操作:对于修改操作,当刷新时间点到来时,是否会马上执行缓存更新,即update语句是否马上执行,还要看修改后的数据是否与快照中的数据一致。若一致,即数据实际上未被修改,则不执行update语句。否则,当到达刷新时间点时,会执行update语句。 3、插入操作:对于插入操作,由于不是修改Session中的现有数据,所以与刷新时间点没有关系。在执行完save()方法后,会马上执行insert()语句,更新Session缓存中的数据。
  • 当然,刷新过缓存后,即缓存中的数据被更新后,是否真正能够同步到数据库中,还要看最终事务是否被提交。若最终事务未提交,或者发生回滚,则刷新过的缓存内容是无法同步到DB中的。

3.3 修改缓存刷新模式

  • 缓存刷新模式,即缓存刷新时间点,。当这三个刷新时间点到来时,是否会刷新缓存数据,决定于缓存刷新模式的设置。通过Session的setFlushMode()方法,可以设置缓存刷新模式。
  • Hibernate中缓存刷新模式通过FlushMode的常量进行指定,其值主要由四种:

4 二级缓存

  • 二级缓存是SessionFactory级别的缓存,其生命周期与SessionFactory一致。SessionFactory缓存可以依据功能和目的不同而划分为内置缓存和外置缓存。
  • SessionFactory的内置缓存中存放了映射元数据和预定义SQL语句,映射元数据是映射文件中数据的副本,而预定义SQL语句是在Hibernate初始化阶段根据映射元数据推导出来的SQL。SessionFactory的内置缓存是只读的,应用程序不能够修改缓存中的映射元数据和预定义SQL语句,因此SessionFactory不需要进行内置缓存和映射文件的同步。
  • SessionFactory的外置缓存是一个可配置的插件。在默认情况下,SessionFactory不会启用这个插件。外置缓存的数据是数据库数据的副本,外置缓存的介质可以是内存或者硬盘。SessionFactory的外置缓存也称之为Hibernate的二级缓存。
  • HIbernate本身只提供了二级缓存的规范,但是并非实现,故需要第三方缓存产品的支持,常用的二级缓存第三方插件有:EHCache、Memcached、OSCache、SwarmCache、JBossCache 等。这些插件的功能各有 侧重,各有特点。

4.1 Hibernate缓存执行顺序

  • 当Hibernate根据ID访问数据对象时,首先会从一级缓存Session中查找。若查不到且配置了二级缓存,则会从二级缓存中查找;若还是没有查到,就会查询数据库,把结果按照ID放入到缓存中。
  • 执行增、删、改操作时,会同步更新缓存。

4.2 二级缓存内容分类

  • 根据缓存内容的不同,可以将Hibernate二级缓存分为三类: 1、类缓存:缓存对象为实体类对象; 2、集合缓存:缓存对象为集合类对象; 3、查询缓存:缓存对象为查询结果;

4.3 二级缓存的并发访问策略

1、事务型(transactional):隔离级别最高,对于经常被读但是很少被改的数据,可以采用此策略。因为它可以防止脏读和不可重复读的并发问题。发生异常的时候,缓存也能够回滚(系统开销大)。 2、读写型(read-write):对于经常被读但是很少被改的数据,可以采用此策略。因为它可以防止脏读的并发问题。更新缓存的时候会锁定缓存中的数据。在EHCache中,使用该策略,将无法从二级缓存中获取数据。 3、非严格读写型(nonstrict-read-write):不保证缓存和数据库中的数据的一致性。对于极少被改,并且允许偶尔脏读的数据,可采用此策略,不锁定缓存中的数据。 4、只读型(read-only):对于从来不会被修改的数据,可使用此策略。

4.4 第三方插件EHCache用法

  • 举例:ehcache

4.4.1 导入Jar包

  • 要导入EHCache的jar包。该jar包可以在Hibernate框架的 lib\optional\ehcache 目录中找到。

4.4.2 修改主配置文件

  • 在hibernate.cfg.xml文件的<session-factory/>元素中加入如下内容:
		<!-- 二级缓存 -->
		<!-- 开启二级缓存 -->
		<property name="hibernate.cache.use_second_level_cache">true</property>
		<!-- 注册二级缓存区工厂  -->
		<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
  • 该配置在hibernate框架解压目录/project/etc目录中的hibernate.properties文件中可以找到配置项。
  • 二级缓存区工厂的Class在hibernate-ehcache-5.0.1.Final.jar中可以找到。

4.4.3 添加ehcache.xml

  • 解压EHCache的核心Jar包 ehcache-core-2.4.3.jar ,将其中的一个配置文件ehcache-failsafe.xml直接放到项目的src目录下,并更名为ehcache.xml。
  • 该配置文件的一级标签为<ehcache/>,其默认有两个子标签,它们的意义为: 1、<diskStore/>标签:
  • 指定一个文件目录,当内存空间不够,需要将二级缓存中数据写到硬盘上时,会写到这个指定目录中。其值一般为java.io.tmpdir,表示当前系统的默认文件临时目录。
  • 当前系统的默认文件临时目录,可以通过System.getProperty()方法查看: 2、<defaultCache/>标签:
  • 即设定缓存的默认属性数据。 1、maxElementsMemory:指定该内存缓存区可以存放缓存对象的最多个数。 2、eternal:意为“永恒”。设定缓存对象是否不会过期。若设为true,表示对象永远不会过期,此时会忽略timeToldSeconds和timeToLiveSeconds属性。默认值为false。 3、timeToldleSeconds:设定允许对象处于空闲状态的最长时间,以秒为单位。当对象自从最近一次被访问后,若处于空闲状态的时间超过了timeToldleSeconds设定的值,这个对象就会过期。当对象过期,EHCache就会将它从缓存中清除。设置值为0,则对象可以无限期地处于空闲状态。 4、timeToLiveSeconds:设定对象允许存在缓存中的最长时间,以秒为单位。当对象自从被存放到缓存后,若处于缓存中的时间超过了timeToLiveSeconds设定的值,这个对象就会过期。当对象过期,EHCache就会将它从缓存中清除。设置值为0,则对象可以无限期地存在于缓存中。注意,只有timeToLiveSeconds >= timeToldleSeconds时,才有意义。 5、overflowToDisk:设定为true,表示当缓存对象达到了 maxElementsInMemory界限时,会将溢出的对象写到<diskStore>元素指定的硬盘目录缓存中。 6、maxElementsOnDisk:指定硬盘缓存区可以存放缓存对象的最多个数。 7、diskPersistent:指定当程序结束时,硬盘缓存区中的缓存对象是否做持久化。 8、diskExpiryThreadIntervalSeconds:指定硬盘中缓存对象的失效时间间隔。 9、memoryStoreEvictionPolicy:如果内存缓存区超过限制,选择移向硬盘缓存区中的对象时使用的策略(Eviction:驱除)。支持三种策略:
  • FIFO:First In First Out,先进先出;
  • LFU:Less Frequenlty Used;使用频率最小的;
  • LRU:Least Recently Used;未被使用时间最长的;

4.4.4 指定缓存内容

  • 指定缓存内容,即指定哪个类或者哪个集合要进行二级缓存。指定的位置有两处:映射文件、主配置文件。这两种任选其一即可。它们的效果是相同的,但是各有利弊。
  • 在映射文件中指定缓存内容,查看其类的映射文件时,一眼就可看到类为缓存类,集合为缓存集合。但是弊端是,缓存的指定位置分散,缺乏项目的整体性。
  • 在主配置文件中指定缓存内容,可一眼看到整个项目中所有缓存类和缓存集合,但是弊端是,缓存内容的指定和类映射分离。 1、在映射文件中指定缓存内容:
  • 在相应的要缓存的类的<class/>标签下指定类缓存,其位置必须在<id/>前。在相应的集合标签<set/>下指定集合缓存,其位置必须在<key/>前。 2、在主配置文件中指定缓存内容:
  • 在<mapping/>标签的后面指定类缓存和集合缓存。注意集合的写法。
  • 无论使用哪种指定方式,usage属性值若指定为read-write,为了保证读到的数据时最新的数据,将无法从二级缓存中直接获取数据。所以,以下举例,均使用usage的值为read-only。
  • 以1:n的Country和Minister关联关系为例:
  • 首先插入数据:
			//开始操作
			Minister minister1 = new Minister("aaa");
			Minister minister2 = new Minister("bbb");
			
			Country country = new Country("USA");
			country.getMinisters().add(minister1);
			country.getMinisters().add(minister2);
			
			session.save(country);
  • 验证:证明二级缓存的存在
      //将id为1的Country对象加载到一级缓存session1中,将引发查询select执行
			Country country1 = session.get(Country.class, 1);
				
			System.out.println("cname = " + country1.getCname());
			
			//从一级缓存中读取id为1的Country对象,不会到DB中查询,即不引发select
			Country country2 = session.get(Country.class, 1);
			System.out.println("cname = " + country2.getCname());
			
			//session清空,当然,session中的Country对象也不存在了
			session.clear();
			
			//从二级缓存中读取id为1d的Country对象,不会到DB中查询,即不引发select
			Country country3 = session.get(Country.class, 1);
			System.out.println("cname3 = " + country3.getCname());
  • 下面是运行结果:

4.5 类缓存、

  • 二级缓存为单独开辟的内存空间,与Session缓存不同,所以存放在这两个缓存中的对象也为不同对象。即使它们是同一个查询语句查询出的结果。
  • 类缓存中存放的是缓存对象的详情。
			Country country1 = session.get(Country.class, 1);
			System.out.println("查看一级缓存中的Country对象 = " + country1);
			
			session.clear();
			
			Country country2 = session.get(Country.class, 1);
			System.out.println("查看二级缓存中的Country对象 = " + country2);
  • 下面是运行结果:

4.6 集合缓存

  • 类缓存缓存的是对象的详情,而集合缓存只缓存集合元素对象的id,而不缓存详情。

4.6.1 运行条件

  • 为了测试集合缓存只缓存对象id,下面的例子,首先将集合的泛型类Minister的类缓存给关闭,而Set<Minister>的集合缓存开启。

4.6.2 测试过程及其原理

  • 为了证明集合缓存只是将元素id缓存到了二级缓存,首先通过Country对象加载集合。可以输出集合大小或者集合详情。此时后台会做对集合详情的select查询。
  • 将session清空,一级缓存数据消失。
  • 再次通过Country对象加载集合,此时做集合大小输出。会发现其并未做select查询。但是,若在此时对集合详情进行输出,会发现执行了select查询。并且其所作的select查询,是根据每个minister的id所作的详情查询,与第一个session中所做查询完全不同。说明,这是从二级缓存中读取了存放其中的集合中元素的id,逐个id进行详情查询。
			//查询Country对象以及关联对象集合对象,将集合详情全部放到一级、二级缓存中
			//要求Country类,Minister类指定为类缓存,集合ministers指定为集合缓存
			Country country1 = session.get(Country.class, 1);
			Set<Minister> minister1 = country1.getMinisters();
			System.out.println(minister1.size());
			
			session.clear();
			
			//从二级缓存中获取Country对象的集合对象
			Country country2 = session.get(Country.class, 1);
			Set<Minister> minister2 = country2.getMinisters();
			System.out.println(minister2.size());

4.7 Query缓存

4.7.1 Query查询结果会存放于缓存中

  • Query查询后的结果与使用get()、load()查询结果一样,也要存放到一、二缓存中。
			//第一次查询
			String hql = "from Country where cid = 1";
			Country country = (Country) session.createQuery(hql).uniqueResult();
			System.out.println("country.cname = " + country.getCname());
			
			//第二次查询
			Country country2 = session.get(Country.class, 1);
			System.out.println("country2.cname = " + country2.getCname());
			
			//第三次查询
			Country country3 = session.get(Country.class, 1);
			System.out.println("country3.cname = " + country3.getCname());
  • 运行结果为:

4.7.2 Query查询不会从缓存中读取数据

  • Query查询结果虽然存放在一、二级缓存中,但是默认情况下,再使用Query进行查询时,是不会从一、二级缓存中直接读取数据的。
		//第一次查询
		String hql = "from Country where cid = 1";
		Country country = (Country) session.createQuery(hql).uniqueResult();
		System.out.println("country.cname = " + country.getCname());

		//第二次查询
		String hql2 = "from Country where cid = 1";
		Country country2 = (Country) session.createQuery(hql2).uniqueResult();
		System.out.println("country2.cname = " + country2.getCname());

		//第三次查询
		String hql3 = "from Country where cid = 1";
		Country country3 = (Country) session.createQuery(hql3).uniqueResult();
		System.out.println("country3.cname = " + country3.getCname());
  • 执行结果:

4.7.3 使用Query缓存的步骤

  • 若要使用Queyr从缓存中读取数据,则需要开启Query缓存功能。 1、开启Query缓存总开关
  • 在Hibernate主配置文件中添加开启Query缓存的设置。该设置可以从/project/etc/hibernate.properties中查看。
		<!-- 注册二级缓存区工厂  -->
		<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
		<!-- 开启Query缓存 -->
		<property name="hibernate.cache.use_query_cache">true</property>

2、为每个Query查询打开子开关

  • Query接口有一个方法settCacheable(),用于为当前Query对象打开Query查询的子开关。在Query对象后设置开关:query.setCacheable(true)。
  • 对于开启Query查询的Query对象,无论是第一次从DB中查询,还是以后从Query缓存中获取,均需要使用setCacheable()方法打开子开关。第一次查询后会将结果放于一、二级缓存。以后的只要是开启Query查询的Query对象要访问第一次的查询结果,均是从二级缓存中获取的。 3、要求查询语句完全相同
  • 需要注意的是,若要从Query缓存中读取查询结果,必须保证查询语句完全相同。即使查询语句意思相同,或者查询结果中存在包含关系,也不会从Query缓存中读取。
				//查询所有country
				String hql = "from Country";
				List<Country> countrys = (List<Country>) session.createQuery(hql).setCacheable(true)
															 .list();
				for(Country country: countrys) {
					System.out.println(country.getCname());
				}
				//查询id为1的Country
				String hql1 = "from Country where cid in (1)";
				Country country1 = (Country) session.createQuery(hql1).setCacheable(true).uniqueResult();
				System.out.println("Country1.cname = " + country1.getCname());
				session.clear();
				
				//查询id为1的Country
				String hql2 = "from Country where cid = 1";
				Country country2 = (Country) session.createQuery(hql1).setCacheable(true).uniqueResult();
				System.out.println("Country1.cname = " + country1.getCname());
  • 执行结果:

4.8 绕过一级缓存的修改

  • 对从DB中查询出的对象进行的修改,一般都是通过修改Session缓存中的数据后,同步到DB中的。但是,Query接口也提供了绕过缓存对数据进行修改的API:通过executeUpdate()方法,可以绕过一级缓存直接修改DB中的数据。

4.8.1 绕过一级缓存的判断依据

  • 如何说明该修改绕过了一级缓存呢?绕过一级缓存的判断依据是,修改未对一级缓存中的原有数据产生影响,即修改前后的数据是一样的。

4.8.2 该修改是否绕过二级缓存?

  • 该修改没有绕过二级缓存。而是修改了二级缓存中的一个状态变量updateTimestamp修改时间戳。该变量值的修改,将会对查询是否执行select语句,即查询结果是否从二级缓存直接读取,起决定作用:该值被修改,则执行select语句,从DB中进行查询。否则,直接从二级缓存中读取数据,不再执行select语句。
				Country country1 = session.get(Country.class, 1);
				System.out.println("修改前:" + country1.getCname());
				//绕过一、二级缓存,将修改直接应用到DB中。
				session.createQuery("update Country set cname = ? where cid = ?")
					   .setString(0, "DEF").setInteger(1, 1).executeUpdate();
				System.out.println("修改后:" + country1.getCname());
				
				session.clear();
				
				Country country2 = session.get(Country.class, 1);
				System.out.println("二级缓存中:" + country2.getCname());
  • 运行结果:

4.8.3 修改时间戳

  • 在二级缓存中存放的对象,比一级缓存中多出一个属性:updateTimeStamp,修改时间戳。只要这个属性发生改变,就说明有操作修改了DB中的数据,二级缓存中的该缓存对象已经不是最新数据,需要从DB中再次查询更新。
  • 而Query接口的executeUpdate()方法所进行的更新,可以绕过一级缓存,但是会修改二级缓存中缓存对象的updateTimeStamp值。而该值的改变,使得本例在清空session后,又进行了一次查询。其实,只要执行executeUpdate()方法,都会引发后台进行update操作,都会引发updateTimeStamp值的改变,都会使二级缓存通过新的查询来更新数据。无论是否真的有数据更新。

4.9 与二级缓存管理相关的方法

  • 与二级缓存管理相关的方法,一般都定义在Cache接口中,而Cache对象的获取,需要通过SessionFactory的getCache()方法:
  • Cache cache = sessionFactory.getCache();
  • 部分方法说明如下: 1、evict(Class c):从二级缓存中删除参数指定类型的所有对象。如参数为Account.class,则会删除二级缓存中所有Account类型的对象; 2、evictCollection(String s):从二级缓存中删除指定的集合。注意,该参数要指定对象的集合属性名。如删除com.eason.po.Country实体类中的ministers集合:evictCollection(“com.eason.po.Country.ministers”); 3、evictEntity(String s):与evict()的作用相同,只不过这里的参数为字符串类型的完整性类名。 4、evictEntity(String s, Serializable id):从二级缓存中删除由s指定类型,由id指定的对象。除了以上方法外,二级缓存相关管理方法还有很多。Ctrl + O,查看Cache接口组成情况如下: