【笔记】Mybatis高级查询(十)--Mybatis缓存使用

【笔记】Mybatis高级查询(准备)
【笔记】Mybatis高级查询(一)–使用自动映射处理一对一关系
【笔记】Mybatis高级查询(二)–使用resultMap配置一对一映射
【笔记】Mybatis高级查询(三)–使用标签实现嵌套查询及延迟加载
【笔记】Mybatis高级查询(四)–使用resultMap的标签实现一对多和多对多查询
【笔记】Mybatis高级查询(五)–使用resultMap的进行嵌套查询及延迟加载
【笔记】Mybatis高级查询(小结)–嵌套查询及延迟加载
【笔记】Mybatis高级查询(六)–鉴别器discrimiator的使用
【笔记】Mybatis高级查询(七)–存储过程调用
【笔记】Mybatis高级查询(八)–枚举处理器的使用
【笔记】Mybatis高级查询(九)–Mybatis代码生成器的使用

一般提到的Mybatis缓存都是指二级缓存。一级缓存(也叫本地缓存)默认会启用,并且不能控制,可以理解为是Mybatis的sqlSession缓存。sqlSession关闭后就失效了。因此一级缓存的生命周期与sqlSession一样。二级缓存可以理解为是存在于SqlSessionFactory的缓存。

1、一级缓存的示例

  • 在ex.mybatis.rbac.mapper包下创建CacheTest类,内容如下
package ex.mybatis.rbac.mapper;

import org.apache.ibatis.session.SqlSession;
import org.junit.Test;

import ex.mybatis.rbac.model.SysRole;
import ex.mybatis.rbac.model.SysUser;

/**
 * 缓存测试类
 * @author huangqh
 *
 */
public class CacheTest extends BaseMapperTest {
	@Test
	public void testL1Cache() {// 一级缓存测试,存在于sqlSession
		// 获取SqlSession
		SqlSession sqlSession = openSession();
		SysUser user1 = null;
		try {
			// 获取SysUserMapper接口
			SysUserMapper userMapper = sqlSession.getMapper(SysUserMapper.class);
			
			// 调用selectByPrimaryKey方法
			user1 = userMapper.selectByPrimaryKey(1L);
			
			// 对user1进行重新赋值
			user1.setUserName("new name");
			
			// 再次调用selectByPrimaryKey方法
			SysUser user2 = userMapper.selectByPrimaryKey(1L);
			
			// 虽然没有更新数据库,但是user2的用户名与user1赋值后的一样。
			System.out.println("user2.getUserName() = " + user2.getUserName() + ", user1.getUserName() = " + user1.getUserName());
			
			// 无论如何user1和user2是同一个对象
			System.out.println("user1 == user2:" + (user1 == user2));
			System.out.println("user1 = " + Integer.toHexString(user1.hashCode()));
			System.out.println("user2 = " + Integer.toHexString(user2.hashCode()));
		} finally {
			// 关闭当前的sqlSession
			sqlSession.close();
		}
		
		// 再开启新的sqlSession
		System.out.println("\n开启新的sqlSession");
		sqlSession = openSession();
		try {
			// 获取SysUserMapper接口
			SysUserMapper userMapper = sqlSession.getMapper(SysUserMapper.class);
			
			// 调用selectByPrimaryKey方法
			SysUser user2 = userMapper.selectByPrimaryKey(1L);
			
			// 虽然没有更新数据库,但是user2的用户名与user1赋值后的一样。
			System.out.println("user2.getUserName() = " + user2.getUserName() + ", user1.getUserName() = " + user1.getUserName());
			
			// user2和前一个sqlSession的user1不是同一个对象
			System.out.println("user1 == user2:" + (user1 == user2));
			System.out.println("user1 = " + Integer.toHexString(user1.hashCode()));
			System.out.println("user2 = " + Integer.toHexString(user2.hashCode()));
			
			// 执行删除操作
			System.out.println();
			userMapper.deleteByPrimaryKey(2L);
			
			// 获取user3
			SysUser user3 = userMapper.selectByPrimaryKey(1L);
			
			// user2和user3不是同一个对象(由于Mybatis的insert、update、delete操作会清空一级缓存)
			// 在<select>中加入flushCache="true"后,在查询时也会清空一级缓存
			System.out.println("user2 == user3:" + (user2 == user3));
			System.out.println("user2 = " + Integer.toHexString(user2.hashCode()));
			System.out.println("user3 = " + Integer.toHexString(user3.hashCode()));
		} finally {
			// 关闭当前的sqlSession
			sqlSession.close();
		}
	}
}
  • 运行结果
[ex.mybatis.rbac.mapper.SysUserMapper.selectByPrimaryKey] - ==>  Preparing: select id, user_name, user_password, user_email, create_time, user_info, head_img from sys_user where id = ? 
[ex.mybatis.rbac.mapper.SysUserMapper.selectByPrimaryKey] - ==> Parameters: 1(Long)
[ex.mybatis.rbac.mapper.SysUserMapper.selectByPrimaryKey] - <==    Columns: id, user_name, user_password, user_email, create_time, user_info, head_img
[ex.mybatis.rbac.mapper.SysUserMapper.selectByPrimaryKey] - <==        Row: 1, admin, 123456, admin@mybatis.ex, 2018-10-01 18:27:36.0, <<BLOB>>, <<BLOB>>
[ex.mybatis.rbac.mapper.SysUserMapper.selectByPrimaryKey] - <==      Total: 1
user2.getUserName() = new name, user1.getUserName() = new name
user1 == user2:true
user1 = 32cf48b7
user2 = 32cf48b7

开启新的sqlSession
[ex.mybatis.rbac.mapper.SysUserMapper.selectByPrimaryKey] - ==>  Preparing: select id, user_name, user_password, user_email, create_time, user_info, head_img from sys_user where id = ? 
[ex.mybatis.rbac.mapper.SysUserMapper.selectByPrimaryKey] - ==> Parameters: 1(Long)
[ex.mybatis.rbac.mapper.SysUserMapper.selectByPrimaryKey] - <==    Columns: id, user_name, user_password, user_email, create_time, user_info, head_img
[ex.mybatis.rbac.mapper.SysUserMapper.selectByPrimaryKey] - <==        Row: 1, admin, 123456, admin@mybatis.ex, 2018-10-01 18:27:36.0, <<BLOB>>, <<BLOB>>
[ex.mybatis.rbac.mapper.SysUserMapper.selectByPrimaryKey] - <==      Total: 1
user2.getUserName() = admin, user1.getUserName() = new name
user1 == user2:false
user1 = 32cf48b7
user2 = 49c43f4e

[ex.mybatis.rbac.mapper.SysUserMapper.deleteByPrimaryKey] - ==>  Preparing: delete from sys_user where id = ? 
[ex.mybatis.rbac.mapper.SysUserMapper.deleteByPrimaryKey] - ==> Parameters: 2(Long)
[ex.mybatis.rbac.mapper.SysUserMapper.deleteByPrimaryKey] - <==    Updates: 0
[ex.mybatis.rbac.mapper.SysUserMapper.selectByPrimaryKey] - ==>  Preparing: select id, user_name, user_password, user_email, create_time, user_info, head_img from sys_user where id = ? 
[ex.mybatis.rbac.mapper.SysUserMapper.selectByPrimaryKey] - ==> Parameters: 1(Long)
[ex.mybatis.rbac.mapper.SysUserMapper.selectByPrimaryKey] - <==    Columns: id, user_name, user_password, user_email, create_time, user_info, head_img
[ex.mybatis.rbac.mapper.SysUserMapper.selectByPrimaryKey] - <==        Row: 1, admin, 123456, admin@mybatis.ex, 2018-10-01 18:27:36.0, <<BLOB>>, <<BLOB>>
[ex.mybatis.rbac.mapper.SysUserMapper.selectByPrimaryKey] - <==      Total: 1
user2 == user3:false
user2 = 49c43f4e
user3 = 2d127a61
  • 结果分析
    第一次执行selectByPrimaryKey方法获取SysUser数据的时候,真正执行了数据库查询,得到了user1的结果。第二次执行selectByPrimaryKey方法获取user2的时候,从日志上看到是在“开启新的sqlSession”上面,只查询了一次数据库,也就是说查询user2的时候没到数据库查询,直接从缓存中返回。
    从测试代码来看,执行了user1.setUserName(“new name”)后并没有进行任何的更新数据库的操作。因此发现获取的user2的userName竟然与user1的一样,都是"new name",再往下看发现user1和user2竟然是同一个对象。之所以这样就是因为Mybatis的一级缓存。

Mybatis的一级缓存存在于SqlSession的生命周期中,在同一个SqlSession中查询时,Mybatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map中。如果在同一个SqlSession中执行的方法与参数完全一致,则会从缓存中返回。因此user1和user2是同一个对象。

在使用Mybatis的过程中,要避免在使用如上代码中的user2时出现的错误。我们可能以为获取的user2是从数据库取的,却不知道user1的一个重新赋值会影响的user2。如果不想让selectByPrimaryKey方法使用一级缓存,可以在Mapper.xml中修改该方法加入flushCache=“true”,该属性为true后,会在查询数据前清空一级缓存,每次都从数据库中取。但是由于这个方法清空了一级缓存,会影响当前SqlSession中所有缓存的查询。因此大需要反复查询获取只读数据的情况下,会增加数据库的查询次数。要避免这样用。修改如下

在这里插入图片描述

  • 继续看结果,在关闭第一个SqlSession后,又重新开启了一个新的SqlSession。再次执行selectByPrimaryKey方法获取user2,发现这次user2从数据库查询了。此时的user2是个新实例,和user1没有任何关系。
  • 接下来又执行了一个deleteByPrimaryKey操作。再次调用selectByPrimaryKey获取user3。发现user3也是从数据库查询出来的。这是因为Mybatis任何的INSERT、UPDATE、DELETE操作都会清空一级缓存

由于一级缓存是默认开启的,不能控制的,因此要避免在使用过程中由于不了解而发生觉察不到的错误。

2、二级缓存的使用

Mybatis的二级缓存非常强大,它不同于一级缓存只存在于SqlSession,而是可以理解为存在于SqlSessionFactory的生命周期中。当存在多个SqlSessionFactory时,它们的缓存都是绑定于各自的对象上,缓存数据不共享。

  • 2.1 配置二级缓存
  • 在Mybatis的全局配置settings中有一个参数cacheEnabled,这个参数是二级缓存的全局开关,默认为true。如果把这个参数设置为false。即使在其它地方设置了二级缓存也不会生效。由于这个参数默认为true,因此不需要配置。如果要配置的话,可以在mybatis_config.xml看添加以下代码:
    在这里插入图片描述
  • Mybatis的二级缓存是与命令空间绑定的,即二级缓存需要配置在Mapper.xml映射文件中或在Mapper接口中。在映射文件中,命名空间就是XML根节点mapper的namespace属性。在Mapper接口中,命名空间就是接口的全限定名称。
  • 2.1.1 在Mapper.xml中配置二级缓存
在保证全局二级缓存开启的情况下,给SysRoleMapper.xml开启二级缓存只需要添加<cache/>标签即可,如下:

在这里插入图片描述

默认的二级缓存<cache/>会有以下效果
  • 映射文件中的所有SELECT语句将会被缓存
  • 映射语句文件中所有INSERT、UPDATE、DELETE语句会刷新缓存
  • 缓存会使用Least Recently Used(LRU,最近最少使用)算法来回收缓存
  • 根据时间表(如no flush Interval,没有刷新间隔),缓存不会以任何时间顺序来刷新
  • 缓存会存储集合或对象的1024个引用
  • 缓存会被视为read/write(可读写)的。意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其它调用者所做的修改
所有的这些属性都可以通过缓存属性修改,以下是一个可读写的FIFO的,60秒刷新一次且大小为512的缓存配置

在这里插入图片描述

<cache/>可以配置的属性如下
  • eviction(回收策略),有以下可选值
    LRU(最近最少使用):移除最长时间不被使用的缓存,这是默认值。
    FIFO(先进先出):按对象进入缓存的顺序来移除缓存
    SOFT(软引用):移除基于垃圾回收器状态和软引用规则的对象
    WEAK(弱引用):更积极地移除基于垃圾回收器状态和弱引用规则的对象
  • flushInterval(刷新间隔):可以设置为任意正整数,单位为毫秒。默认情况下不设置,即没有刷新间隔。缓存只在调用语句时刷新。
  • size(大小):可以设置为任意正整数。要记住缓存对象的数目和运行环境的可用内存大小有关。默认是1024
  • readOnly(只读):属性可设置为true和false。只读缓存会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写缓存会通过序列化返回缓存对象的拷贝,这种方式会慢一些,但是安全。因此默认为false。
  • 2.1.2 在Mapper接口中配置二级缓存
使用注解@CacheNamespace可以配置二级缓存。

默认的方式
在这里插入图片描述
配置参数的方式
在这里插入图片描述

注意:当同时使用注解方式和XML映射文件,同时配置了上述的二级缓存,会抛出以下异常

org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: java.lang.IllegalArgumentException: Caches collection already contains value for ex.mybatis.rbac.mapper.SysRoleMapper在这里插入图片描述
这是因为Mapper接口和对应的XML文件是相同的命名空间。可以把Mapper接口的二级缓存修改为以下就可以同时存在了,把@CacheNamespace改为@CacheNamespaceRef(SysRoleMapper.class)
在这里插入图片描述

  • 2.2 使用二级缓存
  • 由于2.1中配置的是读写缓存,对于读写缓存Mybatis使用SerializedCache(org.apache.ibatis.cache.decorators.SerializedCache)序列化缓存来实现可读写缓存类的,并通过序列化与反序列化来保证通过缓存获取数据时,得到的是一个新实例。因此,如果配置的是只读缓存。Mybatis都会返回同一个实例对象。
    因为是可读写缓存,因此要求所有被序列化的对象必须实现Java的Serializable接口。修改SysRole和CreateInfo类如下:
    SysRole类
    在这里插入图片描述
    CreateInfo类
    在这里插入图片描述
  • 在上一节的CacheTest类添加testL2Cache测试方法
	@Test
	public void testL2Cache() {// 二级缓存测试,存在于SqlSessionFactory
		// 获取SqlSession
		SqlSession sqlSession = openSession();
		SysRole role1 = null;
		try {
			// 获取SysRoleMapper接口
			SysRoleMapper roleMapper = sqlSession.getMapper(SysRoleMapper.class);
			
			// 调用selectByPrimaryKey方法
			role1 = roleMapper.selectByPrimaryKey(1L);
			
			// 对role1进行重新赋值
			role1.setRoleName("new role name");
			
			// 再次调用selectByPrimaryKey方法
			SysRole role2 = roleMapper.selectByPrimaryKey(1L);
			
			// 虽然没有更新数据库,但是role2的角色名与role1赋值后的一样。
			System.out.println("role2.getRoleName() = " + role2.getRoleName() + ", role1.getRoleName() = " + role1.getRoleName());
			
			// 无论如何role1和role2是同一个对象
			System.out.println("role1 == role2:" + (role1 == role2));
			System.out.println("role1 = " + Integer.toHexString(role1.hashCode()));
			System.out.println("role2 = " + Integer.toHexString(role2.hashCode()));
		} finally {
			// 关闭当前的sqlSession
			sqlSession.close();
		}
		
		// 再开启新的sqlSession
		System.out.println("\n开启新的sqlSession");
		sqlSession = openSession();
		try {
			// 获取SysRoleMapper接口
			SysRoleMapper roleMapper = sqlSession.getMapper(SysRoleMapper.class);
			
			// 调用selectByPrimaryKey方法
			SysRole role2 = roleMapper.selectByPrimaryKey(1L);
			
			// 虽然没有更新数据库,但是role2的用户名与role1赋值后的一样。
			System.out.println("role2.getRoleName() = " + role2.getRoleName() + ", role1.getRoleName() = " + role1.getRoleName());
			
			// role2和前一个sqlSession的role1不是同一个对象
			System.out.println("role1 == role2:" + (role1 == role2));
			System.out.println("role1 = " + Integer.toHexString(role1.hashCode()));
			System.out.println("role2 = " + Integer.toHexString(role2.hashCode()));
			
			// 获取role3
			SysRole role3 = roleMapper.selectByPrimaryKey(1L);
			
			// role2和role3不是同一个对象(由于Mybatis的反序列化得到的对象)
			System.out.println("role2 == role3:" + (role2 == role3));
			System.out.println("role2 = " + Integer.toHexString(role2.hashCode()));
			System.out.println("role3 = " + Integer.toHexString(role3.hashCode()));
		} finally {
			// 关闭当前的sqlSession
			sqlSession.close();
		}
	}
  • 运行结果
[ex.mybatis.rbac.mapper.SysRoleMapper] - Cache Hit Ratio [ex.mybatis.rbac.mapper.SysRoleMapper]: 0.0
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - ==>  Preparing: select id, role_name, enabled, create_by, create_time from sys_role where id = ? 
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - ==> Parameters: 1(Long)
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - <==    Columns: id, role_name, enabled, create_by, create_time
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - <==        Row: 1, 管理员, 1, 1, 2018-10-01 18:27:36.0
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - <==      Total: 1
[ex.mybatis.rbac.mapper.SysRoleMapper] - Cache Hit Ratio [ex.mybatis.rbac.mapper.SysRoleMapper]: 0.0
role2.getRoleName() = new role name, role1.getRoleName() = new role name
role1 == role2:true
role1 = 6e2c9341
role2 = 6e2c9341

开启新的sqlSession
[ex.mybatis.rbac.mapper.SysRoleMapper] - Cache Hit Ratio [ex.mybatis.rbac.mapper.SysRoleMapper]: 0.3333333333333333
role2.getRoleName() = new role name, role1.getRoleName() = new role name
role1 == role2:false
role1 = 6e2c9341
role2 = 564718df
[ex.mybatis.rbac.mapper.SysRoleMapper] - Cache Hit Ratio [ex.mybatis.rbac.mapper.SysRoleMapper]: 0.5
role2 == role3:false
role2 = 564718df
role3 = 51b7e5df
  • 结果分析
    日志中有几条Cache Hit Ratio开头的语句,这行后面输出的值为缓存命中率。在测试的第一部分中,第一次查询获取role1的时候由于没有缓存,所以执行了数据库查询。再次获取role2时,由于一级缓存的关系,role1和role2是同一个缓存对象。
    当调用close方法关闭SqlSession时,SqlSession才会保存查询数据到二级缓存中。在这之后二级缓存才会有数据。所以看到前2次查询,命中率都是0。
    在第二部分测试代码中,再次获取role2,日志中并没有数据库查询语句并且缓存命中值为0.3333333333333333,这是因为该方法一共进行了3次查询,有一次命中,所以命中率为三分之一。后面再获取role3时就是4次调用,命中2次,因此值为0.5。并且因为是可读写缓存的原因,role2和role3都是反序列化得到的结果,所以role2和role3不相等。
提示:在这个例子中并没有真正的读写安全,因为加入了一段role1.setRoleName(“new role name”),按照常理这里修改了role1的属性值后,应该更新数据库,更新后会清空一级和二级缓存,这样在第二部分代码查role2时就不会得到roleName也是“new role name”了。所以想要安全使用缓存,要避免毫无意义的修改。这样就可以避免人为产生脏数据。避免缓存与数据库不一致。

3、脏数据的产生和避免

二级缓存虽然能提高应用的效率,减轻数据库的压力,但是如果使用不当,很容易产生脏数据。这些脏数据会在不知不觉中影响业务逻辑。所以我们需要了解Mybatis缓存中的脏数据是如何产生的,也要掌握避免脏数据的技巧。
  • 3.1 脏数据的产生原因
Mybatis二级缓存是和命名空间绑定的。所以通常情况下每一个Mapper映射文件都拥有自己的二级缓存,不同的Mapper的二级缓存互不影响。当进行多表查询时,肯定会把这个查询放到某个命名空间的映射文件中,这样一个多表查询就会缓存在该命名空间的二级缓存中。涉及到这些表的增删改操作通常不在一个映射文件中。它们的命名空间不同。因此当有数据变化时,多表查询的缓存未必会被清空。这时就产生了脏数据。
	<select id="selectUserAndRoleById" resultType="ex.mybatis.rbac.model.SysUser">
		select
		u.id,
		u.user_name,
		u.user_password,
		u.user_email,
		u.create_time,
		u.user_info,
		u.head_img,
		r.id "role.id",
		r.role_name "role.roleName",
		r.enabled "role.enabled",
		r.create_by "role.createBy",
		r.create_time "role.createTime"
		from sys_user u
		inner join sys_user_role ur on u.id = ur.user_id
		inner join sys_role r on ur.role_id = r.id
		where u.id = #{id}
	</select>
  • 这个SQL语句关联两个表来查询用户角色数据。现在给SysUserMapper.xml添加二级缓存,增加<cache/>元素,修改SysUser类实现Serializable接口。

在这里插入图片描述
在这里插入图片描述

  • 在CacheTest类中添加testDirtyData方法,代码如下
	/**
	 * 缓存脏数据测试方法
	 */
	@Test
	public void testDirtyData() {
		// 获取SqlSession
		SqlSession sqlSession = openSession();
		try {
			// 获取SysUserMapper接口
			SysUserMapper userMapper = sqlSession.getMapper(SysUserMapper.class);

			// 调用selectUserAndRoleById方法
			SysUser user = userMapper.selectUserAndRoleById(1001L);

			// 打印角色名
			System.out.println("角色名:" + user.getRole().getRoleName());
		} finally {
			sqlSession.close();
		}

		// 开启新的sqlSession(修改role的角色名)
		System.out.println("\n开启新的sqlSession");
		sqlSession = openSession();
		try {
			// 获取SysRoleMapper接口
			SysRoleMapper roleMapper = sqlSession.getMapper(SysRoleMapper.class);

			// 调用selectByPrimaryKey方法获取role信息
			SysRole role = roleMapper.selectByPrimaryKey(2L);

			// 修改roleName
			role.setRoleName("脏数据");

			// 更新roleName到数据库
			roleMapper.updateByPrimaryKey(role);

			// 提交修改
			sqlSession.commit();
		} finally {
			sqlSession.close();
		}

		// 开启新的sqlSession(修改role的角色名)
		System.out.println("\n开启新的sqlSession");
		sqlSession = openSession();
		try {
			// 获取SysUserMapper接口
			SysUserMapper userMapper = sqlSession.getMapper(SysUserMapper.class);
			
			// 获取SysRoleMapper接口
			SysRoleMapper roleMapper = sqlSession.getMapper(SysRoleMapper.class);
			
			// 调用selectUserAndRoleById方法
			SysUser user = userMapper.selectUserAndRoleById(1001L);

			// 调用selectByPrimaryKey方法获取role信息
			SysRole role = roleMapper.selectByPrimaryKey(2L);
			
			// 打印角色名
			System.out.println("\nuser.getRole().getRoleName()从缓存中获取的角色名 = " + user.getRole().getRoleName());
			System.out.println("数据库的真实角色名role.getRoleName() = " + role.getRoleName());
			
			System.out.println();
			
			// 还原roleName
			role.setRoleName("普通用户");

			// 更新roleName到数据库
			roleMapper.updateByPrimaryKey(role);

			// 提交修改
			sqlSession.commit();
		} finally {
			sqlSession.close();
		}
	}
  • testDirtyData运行结果
[ex.mybatis.rbac.mapper.SysUserMapper] - Cache Hit Ratio [ex.mybatis.rbac.mapper.SysUserMapper]: 0.0
[ex.mybatis.rbac.mapper.SysUserMapper.selectUserAndRoleById] - ==>  Preparing: select u.id, u.user_name, u.user_password, u.user_email, u.create_time, u.user_info, u.head_img, r.id "role.id", r.role_name "role.roleName", r.enabled "role.enabled", r.create_by "role.createBy", r.create_time "role.createTime" from sys_user u inner join sys_user_role ur on u.id = ur.user_id inner join sys_role r on ur.role_id = r.id where u.id = ? 
[ex.mybatis.rbac.mapper.SysUserMapper.selectUserAndRoleById] - ==> Parameters: 1001(Long)
[ex.mybatis.rbac.mapper.SysUserMapper.selectUserAndRoleById] - <==    Columns: id, user_name, user_password, user_email, create_time, user_info, head_img, role.id, role.roleName, role.enabled, role.createBy, role.createTime
[ex.mybatis.rbac.mapper.SysUserMapper.selectUserAndRoleById] - <==        Row: 1001, test, 123456, test@mybatis.ex, 2018-10-02 17:17:11.0, <<BLOB>>, <<BLOB>>, 2, 普通用户, 0, 1, 2018-10-01 18:27:37.0
[ex.mybatis.rbac.mapper.SysUserMapper.selectUserAndRoleById] - <==      Total: 1
角色名:普通用户

开启新的sqlSession
[ex.mybatis.rbac.mapper.SysRoleMapper] - Cache Hit Ratio [ex.mybatis.rbac.mapper.SysRoleMapper]: 0.0
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - ==>  Preparing: select id, role_name, enabled, create_by, create_time from sys_role where id = ? 
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - ==> Parameters: 2(Long)
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - <==    Columns: id, role_name, enabled, create_by, create_time
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - <==        Row: 2, 普通用户, 0, 1, 2018-10-01 18:27:37.0
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - <==      Total: 1
[ex.mybatis.rbac.mapper.SysRoleMapper.updateByPrimaryKey] - ==>  Preparing: update sys_role set role_name = ?, enabled = ?, create_by = ?, create_time = ? where id = ? 
[ex.mybatis.rbac.mapper.SysRoleMapper.updateByPrimaryKey] - ==> Parameters: 脏数据(String), 0(Integer), 1(Long), 2018-10-01 18:27:37.0(Timestamp), 2(Long)
[ex.mybatis.rbac.mapper.SysRoleMapper.updateByPrimaryKey] - <==    Updates: 1

开启新的sqlSession
[ex.mybatis.rbac.mapper.SysUserMapper] - Cache Hit Ratio [ex.mybatis.rbac.mapper.SysUserMapper]: 0.5
[ex.mybatis.rbac.mapper.SysRoleMapper] - Cache Hit Ratio [ex.mybatis.rbac.mapper.SysRoleMapper]: 0.0
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - ==>  Preparing: select id, role_name, enabled, create_by, create_time from sys_role where id = ? 
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - ==> Parameters: 2(Long)
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - <==    Columns: id, role_name, enabled, create_by, create_time
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - <==        Row: 2, 脏数据, 0, 1, 2018-10-01 18:27:37.0
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - <==      Total: 1

user.getRole().getRoleName()从缓存中获取的角色名 = 普通用户
数据库的真实角色名role.getRoleName() = 脏数据

[ex.mybatis.rbac.mapper.SysRoleMapper.updateByPrimaryKey] - ==>  Preparing: update sys_role set role_name = ?, enabled = ?, create_by = ?, create_time = ? where id = ? 
[ex.mybatis.rbac.mapper.SysRoleMapper.updateByPrimaryKey] - ==> Parameters: 普通用户(String), 0(Integer), 1(Long), 2018-10-01 18:27:37.0(Timestamp), 2(Long)
[ex.mybatis.rbac.mapper.SysRoleMapper.updateByPrimaryKey] - <==    Updates: 1
  • testDirtyData结果分析
  • 在这个测试中一共有3个不同的sqlSession,第一个sqlSession获取用户和关联的角色信息,第二个sqlSession查询角色信息并修改角色名。第三个sqlSession再次查询用户和关联的角色信息。这时从缓存中直接取出数据,就出现了脏数据。因为角色名称已在第二个sqlSession更改为“脏数据”。但第三个sqlSession查同来的角色名仍然是修改前的名字“普通用户”。因此出现了脏读。
  • 避免脏读的方法
  • 可以使用参照缓存来避免以上脏读的出现。当某几个表可以作为一个业务整体时,通常是让几个有关联的表同时使用一个二级缓存。这样就能解决脏数据问题。
  • 解决方法是把SysUserMapper.xml的缓存配置<cache/>修改<cache-ref namespace="ex.mybatis.rbac.mapper.SysRoleMapper"/>
  • 缓存配置修改后运行结果(可以看到缓存给刷新了)
[ex.mybatis.rbac.mapper.SysRoleMapper] - Cache Hit Ratio [ex.mybatis.rbac.mapper.SysRoleMapper]: 0.0
[ex.mybatis.rbac.mapper.SysUserMapper.selectUserAndRoleById] - ==>  Preparing: select u.id, u.user_name, u.user_password, u.user_email, u.create_time, u.user_info, u.head_img, r.id "role.id", r.role_name "role.roleName", r.enabled "role.enabled", r.create_by "role.createBy", r.create_time "role.createTime" from sys_user u inner join sys_user_role ur on u.id = ur.user_id inner join sys_role r on ur.role_id = r.id where u.id = ? 
[ex.mybatis.rbac.mapper.SysUserMapper.selectUserAndRoleById] - ==> Parameters: 1001(Long)
[ex.mybatis.rbac.mapper.SysUserMapper.selectUserAndRoleById] - <==    Columns: id, user_name, user_password, user_email, create_time, user_info, head_img, role.id, role.roleName, role.enabled, role.createBy, role.createTime
[ex.mybatis.rbac.mapper.SysUserMapper.selectUserAndRoleById] - <==        Row: 1001, test, 123456, test@mybatis.ex, 2018-10-02 17:17:11.0, <<BLOB>>, <<BLOB>>, 2, 普通用户, 0, 1, 2018-10-01 18:27:37.0
[ex.mybatis.rbac.mapper.SysUserMapper.selectUserAndRoleById] - <==      Total: 1
角色名:普通用户

开启新的sqlSession
[ex.mybatis.rbac.mapper.SysRoleMapper] - Cache Hit Ratio [ex.mybatis.rbac.mapper.SysRoleMapper]: 0.0
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - ==>  Preparing: select id, role_name, enabled, create_by, create_time from sys_role where id = ? 
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - ==> Parameters: 2(Long)
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - <==    Columns: id, role_name, enabled, create_by, create_time
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - <==        Row: 2, 普通用户, 0, 1, 2018-10-01 18:27:37.0
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - <==      Total: 1
[ex.mybatis.rbac.mapper.SysRoleMapper.updateByPrimaryKey] - ==>  Preparing: update sys_role set role_name = ?, enabled = ?, create_by = ?, create_time = ? where id = ? 
[ex.mybatis.rbac.mapper.SysRoleMapper.updateByPrimaryKey] - ==> Parameters: 脏数据(String), 0(Integer), 1(Long), 2018-10-01 18:27:37.0(Timestamp), 2(Long)
[ex.mybatis.rbac.mapper.SysRoleMapper.updateByPrimaryKey] - <==    Updates: 1

开启新的sqlSession
[ex.mybatis.rbac.mapper.SysRoleMapper] - Cache Hit Ratio [ex.mybatis.rbac.mapper.SysRoleMapper]: 0.0
[ex.mybatis.rbac.mapper.SysUserMapper.selectUserAndRoleById] - ==>  Preparing: select u.id, u.user_name, u.user_password, u.user_email, u.create_time, u.user_info, u.head_img, r.id "role.id", r.role_name "role.roleName", r.enabled "role.enabled", r.create_by "role.createBy", r.create_time "role.createTime" from sys_user u inner join sys_user_role ur on u.id = ur.user_id inner join sys_role r on ur.role_id = r.id where u.id = ? 
[ex.mybatis.rbac.mapper.SysUserMapper.selectUserAndRoleById] - ==> Parameters: 1001(Long)
[ex.mybatis.rbac.mapper.SysUserMapper.selectUserAndRoleById] - <==    Columns: id, user_name, user_password, user_email, create_time, user_info, head_img, role.id, role.roleName, role.enabled, role.createBy, role.createTime
[ex.mybatis.rbac.mapper.SysUserMapper.selectUserAndRoleById] - <==        Row: 1001, test, 123456, test@mybatis.ex, 2018-10-02 17:17:11.0, <<BLOB>>, <<BLOB>>, 2, 脏数据, 0, 1, 2018-10-01 18:27:37.0
[ex.mybatis.rbac.mapper.SysUserMapper.selectUserAndRoleById] - <==      Total: 1
[ex.mybatis.rbac.mapper.SysRoleMapper] - Cache Hit Ratio [ex.mybatis.rbac.mapper.SysRoleMapper]: 0.0
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - ==>  Preparing: select id, role_name, enabled, create_by, create_time from sys_role where id = ? 
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - ==> Parameters: 2(Long)
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - <==    Columns: id, role_name, enabled, create_by, create_time
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - <==        Row: 2, 脏数据, 0, 1, 2018-10-01 18:27:37.0
[ex.mybatis.rbac.mapper.SysRoleMapper.selectByPrimaryKey] - <==      Total: 1

user.getRole().getRoleName()从缓存中获取的角色名 = 脏数据
数据库的真实角色名role.getRoleName() = 脏数据

[ex.mybatis.rbac.mapper.SysRoleMapper.updateByPrimaryKey] - ==>  Preparing: update sys_role set role_name = ?, enabled = ?, create_by = ?, create_time = ? where id = ? 
[ex.mybatis.rbac.mapper.SysRoleMapper.updateByPrimaryKey] - ==> Parameters: 普通用户(String), 0(Integer), 1(Long), 2018-10-01 18:27:37.0(Timestamp), 2(Long)
[ex.mybatis.rbac.mapper.SysRoleMapper.updateByPrimaryKey] - <==    Updates: 1

注意:虽然使用参数缓存可以解决脏数据问题,但是并不是所有关联查询都可以这么解决,如果有几十个表甚至所有表都以不同的关联关系存在于各自的映射文件中时,使用参照缓存就没有意义了。

4、二级缓存适用的场景

二级缓存虽然好处很多,但不是什么时候都可以用,在以下场景中,推荐使用二级缓存。
  • 以查询为主的应用中,只有尽可能少的增、删、改操作。
  • 绝大多数以单表操作存在时,由于很少存在互相关联的情况。因此不会出现脏读。
  • 可以按业务划分对表进行分组时,如果关联的表比较少,可以通过参照缓存进行配置。
除了以上3个场景,如果脏读对系统没有影响,也可以考虑使用。在无法保证数据不出现脏读的情况下,建议在业务层使用可控制的缓存替代二级缓存,如Redis。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值