MyBatis学习笔记(10)------二级缓存


MyBatis 的二级缓存可以理解为存在于 SqlSessionFactory 的生命周期中。当存在多个 SqlSessionFactory 时,它们的缓存是绑定在各自的对象上的,缓存数据一般不相通。

配置二级缓存

MyBatis 默认开启二级缓存,如果想要关闭,可以在 mybatis-config.xml 中配置:

<settings>
	<!-- 其它配置 --> 
 	<!-- 关闭二级缓存(默认为开启) -->
 	<setting name="cacheEnabled" value="false"/>
</settings>

MyBatis 的二级缓存和命名空间绑定,即二级缓存需要配置在 XML 映射文件或者 Mapper 接口中。

Mapper.xml 中配置二级缓存

示例:在 RoleMapper.xml 开启二级缓存

<mapper namespace="tk.mybatis.simple.mapper.RoleMapper">
	<cache/>
	<!--其他配置-->
</mapper>

默认的二级缓存会有如下效果:

  • 映射语句文件中的所有 SELECT 语句会被缓存
  • 映射语句文件中的所有 INSERT、UPDATE、DELETE 语句会刷新缓存
  • 缓存会使用 Least Resently Used(LRU,最近最少使用)算法收回
  • 缓存不会以任何时间顺序来刷新
  • 缓存会存储集合或对象(无论查询方法返回什么类型的值)的1024个引用
  • 缓存是 read/write 的,意味着对象检索不是共享,而且可以安全的被调用者修改,而不干扰其他调用者或线程所作的潜在修改

所有属性都可以通过 cache 标签的属性来修改:

<cache
		eviction="FIFO"
		flushInterval="60000"
		size="1024"
		readOnly="false"/>

创建了一个FIFO缓存,每隔60秒刷新一次,存储集合或对象的512个应用,而且返回的对象被认为是只读的,

  • eviction(收回策略)
    LRU(最近最少使用):移除最长时间不被使用的对象,默认值
    FIFO(先进先出):按对象进入缓存的顺序来移除它们
    SOFT(软引用):移除基于垃圾回收器状态和软引用规则的对象
    WEAK(弱引用):更积极的移除基于垃圾回收器状态和弱引用规则的对象
  • flushInterval(刷新间隔):可以被设置为任意的正整数。默认情况不设置,缓存仅在调用语句时刷新
  • size(引用数目):默认值是1024
  • redonly(只读):属性可以设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例,具有性能优势。可读写的缓存会通过序列化返回缓存对象的拷贝,默认是 false。

Mapper 接口中配置二级缓存

只需要在接口名称前增加 @CacheNamespace(org.apache.ibatis.annotations.CacheNamespace)注解即可,该注解同样可以配置各项属性

@CacheNamespace(
	eviction = FifoCache.class,
	flushInterval = 60000,
	size = 512,
	readWrite = true
)

同时配置上述的二级缓存会抛出异常,因为 Mapper 接口和对应的 XML 文件是相同的命名空间。这时候可以使用参照缓存。在 Mapper 接口中,参照缓存配置如下:

@CacheNamespaceRef(RoleMapper.class)
public interface RoleMapper {
}

也可以在 XML 中配置参照缓存:

<cache-ref namespace="tk.mybatis.simple.mapper.RoleMapper"/>

这样可以解决同时配置二级缓存所导致的冲突,但参照缓存并不是为了解决这个问题而设计的,MyBatis 中很少同时使用两种配置。

使用二级缓存

MyBatis 使用 SerializedCache 序列化缓存来实现可读写缓存类,并通过序列化(将一个对象写入磁盘)和反序列化(从磁盘中恢复序列化的对象)来保证通过缓存获取数据时,得到的是一个新的实例。如果配置为只读缓存,MyBatis 会使用 Map 来存储缓存值,这种情况下,从缓存中获取的对象就是同一个实例。

因为 RoleMapper 使用可读写缓存,需要使用 SerializedCache 序列化缓存,这个缓存类要求所有被序列化的对象必须实现 Serializable(java.io.Serializable)接口。

public class SysRole implements Serializable{
	
	private static final long serialVersionUID = 111111L;
	//其它属性和getter,setter方法
}

测试代码:

	@Test
	public void testL2Cache() {
		SqlSession sqlSession = getSqlSession();
		SysRole role1 = null;
		
		try {
			RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
			//查询id=1的角色
			role1 = roleMapper.selectById(1L);
			//对当前获取的对象重新赋值
			role1.setRoleName("new name");
			//再次查询相同id的角色
			SysRole role2 = roleMapper.selectById(1L);
			//虽然没有更新数据库,但是这个角色名和role1重新赋值的名字相同
			Assert.assertEquals("new name", role2.getRoleName());
			//role1和role2是同一个实例
			Assert.assertEquals(role1, role2);
		} finally {
			// 关闭当前session
			sqlSession.close();
		}
		
		System.out.println("开启新的session");
		sqlSession = getSqlSession();
		
		try {
			RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
			//查询id=1的角色
			SysRole role2 = roleMapper.selectById(1L);
			//第二个session获取的角色名仍是new name
			Assert.assertEquals("new name", role2.getRoleName());
			//这里role2和前一个session查询的结果是两个不同的实例
			Assert.assertNotEquals(role1, role2);
			//再次获取id=1的角色
			SysRole role3 = roleMapper.selectById(1L);
			//这里的role2和role3是两个不同的实例
			Assert.assertNotEquals(role2, role3);
		} finally {
			// 关闭session
			sqlSession.close();
		}
		
	}

测试结果:

DEBUG [main] - Cache Hit Ratio [tk.mybatis.simple.mapper.RoleMapper]: 0.0
DEBUG [main] - ==>  Preparing: select id, role_name roleName, enabled, create_time 'createInfo.createTime', create_by 'createInfo.createBy' from sys_role where id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
TRACE [main] - <==    Columns: id, roleName, enabled, createInfo.createTime, createInfo.createBy
TRACE [main] - <==        Row: 1, 管理员, 1, 2019-01-06 10:36:27.0, 1
DEBUG [main] - <==      Total: 1
DEBUG [main] - Cache Hit Ratio [tk.mybatis.simple.mapper.RoleMapper]: 0.0
开启新的session
DEBUG [main] - Cache Hit Ratio [tk.mybatis.simple.mapper.RoleMapper]: 0.3333333333333333
DEBUG [main] - Cache Hit Ratio [tk.mybatis.simple.mapper.RoleMapper]: 0.5

以 Cache Hit Ratio 开头的语句为当前执行方法的缓存命中率。

在第一部分测试中,第一次查询获取 role1 的时候由于没有缓存,所以执行了数据库查询。第二个查询获取 role2 的时候,role2 和 role1 是完全相同的实例,这里使用的是一级缓存,所以返回同一个实例。

调用 close 方法关闭 session 的时候,sqlSession 才会保存数据到二级缓存中,所以第一部分的两次查询命中率为 0。

在第二部分的测试中,再次获取 role2 和 role3 时是从缓存中取得的值,命中率分别为 0.333、0.5。因为是可读写缓存,role2 和 role3 都是反序列换得到的结果,所以它们是不同的实例。

脏数据的产生与避免

MyBatis 的二级缓存是和命名空间绑定的,通常情况下每一个 Mapper 映射文件都有自己的二级缓存,不同的 Mapper 的二级缓存互不影响。在常见的数据库操作中,多表联合查询非常常见。在关联多表查询时,肯定会将该查询放在某个命名空间下的映射文件中,这样一个多表的查询就会缓存在该命名空间的二级缓存中。涉及这些表的增、删、改操作在不同的映射文件中,它们的命名空间不同,当有数据变化时,多表查询的缓存未必会被清空,这种情况下就产生了脏数据。

在 UserMapper 中有 selectUserAndRoleById 方法:

select
	u.id,
	u.user_name userName,
	u.user_password userPassword,
	u.user_info userInfo,
	u.create_time createTime,
	r.role_name as "role.roleName",
	r.enabled as "role.enabled"
from sys_user u
inner join sys_user_role ur on ur.user_id = u.id
inner join sys_role r on ur.role_id = r.id
where u.id = #{id}
	@Test
	public void testDirtyDate() {
		SqlSession sqlSession = getSqlSession();
		
		try {
			UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
			SysUser user1 = userMapper.selectUserAndRoleById(1001L);
			//id为1001的用户角色名是普通用户
			Assert.assertEquals("普通用户", user1.getRole().getRoleName());
			System.out.println("角色名:" + user1.getRole().getRoleName());
		} finally {
			// TODO: handle finally clause
			sqlSession.close();
		}
		sqlSession = getSqlSession();
		try {
			RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
			SysRole role = roleMapper.selectById(2L);
			Assert.assertEquals("普通用户", role.getRoleName());
			role.setRoleName("脏数据");
			roleMapper.updateById(role);
			//提交修改
			sqlSession.commit();
		} finally {
			// TODO: handle finally clause
			sqlSession.close();
		}
		
		System.out.println("开启新的Session");
		sqlSession = getSqlSession();
		try {
			UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
			RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
			SysUser user = userMapper.selectUserAndRoleById(1001L);
			SysRole role = roleMapper.selectById(2L);
			System.out.println("角色名:" + user.getRole().getRoleName());
			Assert.assertEquals("普通用户", user.getRole().getRoleName());
			Assert.assertEquals("脏数据", role.getRoleName());
			//还原数据
			role.setRoleName("普通用户");
			roleMapper.updateById(role);
			sqlSession.commit();
		} finally {
			// TODO: handle finally clause
			sqlSession.close();
		}
	}

第一个 session 中获取了用户和关联的角色信息,此时角色名为“普通用户”;第二个 session 中查询角色并修改了角色信息,此时角色名更改为“脏数据”;第三个 session 中查询用户和关联的角色信息,这时从缓存中直接取出数据,取出的用户关联的角色名仍然为修改前的“普通用户”,出现了脏读现象。

使用参照缓存可以避免脏数据的出现。当某几个表可以作为一个业务整体时,通常让几个会关联的 ER 表同时使用一个二级缓存。在上面的例子中,由于是更新 Role 表导致 UserMapper 中出现脏数据,所以在 UserMapper.xml 中配置参照缓存:

<mapper namespace="tk.mybatis.simple.mapper.UserMapper">
	<cache-ref namespace="tk.mybatis.simple.mapper.RoleMapper"/>
</mapper>

二级缓存适用场景

  • 以查询为主的应用中,只有尽可能少的增、改、删操作。
  • 绝大多数以单表操作存在时。
  • 可以按业务划分对表进行分组时,如关联的表比较少,可以通过参照缓存进行配置。
  • 脏读对系统没有影响。

在无法保证数据不出现脏读的情况下,建议在业务层使用可控制的缓存代替二级缓存。

集成 EhCache 缓存

EhCache 是一个 java 进程内的缓存框架。EhCache 的主要特性如下:

  • 快速
  • 简单
  • 多种缓存策略
  • 缓存数据具有内存和磁盘两级,无需担心容量问题
  • 缓存数据会在虚拟机重启过程中写入磁盘
  • 可以通过 RMI、可插入 API 等方式进行分布式缓存
  • 具有缓存和缓存管理器的侦听接口
  • 支持多缓存管理器实例以及一个实例的多个缓存区域

添加项目依赖

<dependency>
	<groupId>org.mybatis.caches</groupId>
	<artifactId>mybatis-ehcache</artifactId>
	<version>1.0.3</version>
</dependency>

配置 EhCache

在 src/main/resources 目录下新增 ehcache.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="ehcache.xsd"
    updateCheck="false" monitoring="autodetect"
    dynamicConfig="true">
    
    <diskStore path="D:/cache" />
            
	<defaultCache      
		maxElementsInMemory="3000"      
		eternal="false"      
		copyOnRead="true"
		copyOnWrite="true"
		timeToIdleSeconds="3600"      
		timeToLiveSeconds="3600"      
		overflowToDisk="true"      
		diskPersistent="true"/> 
</ehcache>

copyOnRead 的含义是,判断从缓存中读取数据时是返回对象的引用还是复制一个对象返回。默认是 false,返回数据引用。
copyOnWrite 的含义是,判断写入缓存时是直接缓存对象的引用还是复制一个对象然后缓存。默认是 false,直接缓存数据。
如果想使用可读写缓存,就需要将这两个属性配置为 true。

需要在 RoleMapper.xml 中应用:

<mapper namespace="tk.mybatis.simple.mapper.RoleMapper">
	<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
</mapper>

在 ehcache.xml 中有一个默认的缓存配置,也可以针对某一个命名空间进行配置,例如针对 RoleMapper :

	<cache      
		name="tk.mybatis.simple.mapper.RoleMapper"
		maxElementsInMemory="3000"      
		eternal="false"      
		copyOnRead="true"
		copyOnWrite="true"
		timeToIdleSeconds="3600"      
		timeToLiveSeconds="3600"      
		overflowToDisk="true"      
		diskPersistent="true"/>

集成 Redis 缓存

Redis 是一个高性能的 key-value 数据库。

添加项目依赖

<dependency>
	<groupId>org.mybatis.caches</groupId>
	<artifactId>mybatis-redis</artifactId>
	<version>1.0.0-beta2</version>
</dependency>

配置 Redis

有关 Redis 安装启动可参考Redis官方文档

Redis 服务启动后,在 src/main/resources 目录下新增 redis.properties 文件:

host=192.168.16.142
port=6379
connectionTimeout=5000
soTimeout=5000
password=
database=0
clientName=

修改 RoleMapper.xml 中的缓存配置

<mapper namespace="tk.mybatis.simple.mapper.RoleMapper">
	<cache type="org.mybatis.caches.redis.RedisCache"/>
</mapper>

RedisCache 在保存和获取缓存数据时,使用了 Java 的序列化和反序列化,因此被缓存的对象需要实现 serializable 接口。

Redis 并不会因为应用的关闭而失效,所有的查询都使用缓存,是可读写缓存。使用 Redis 的缓存服务可以将分布式应用连接到同一个缓存服务器,实现分布式应用间的缓存共享。

猜你喜欢

转载自blog.csdn.net/BZeHong/article/details/87196794