MyBatis——一级缓存与二级缓存

1、一级缓存

	@Test
    public void testL1Cache() {
        //获取SqlSession
        SqlSession sqlSession = getSqlSession();
        SysUser user1 = null;
        try {
            //获取UserMapper接口
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            //查询id=1的用户
            user1 = userMapper.selectById(1l);
            //重新赋值
            user1.setUserName("New Name");
            //再次查询id相同的用户
            SysUser user2 = userMapper.selectById(1l);
            //user1和user2是同一个对象
            Assert.assertEquals("New Name", user2.getUserName());
            Assert.assertEquals(user1, user2);
        } finally {
            sqlSession.close();
        }

        //开启新的sqlSession
         sqlSession = getSqlSession();
        try {
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            //查询id=1的用户
            SysUser user2 = userMapper.selectById(1l);
            //user2的name为admin
            System.out.println(user2.getUserName());//admin
            //user2和上面的user1是两个不同的实例
            System.out.println(user1 == user2); //false
            //执行删除操作
            userMapper.deleteById(2l);
            //获取user3
            SysUser user3 = userMapper.selectById(1l);
            //user2和user3是两个不同的实例
            System.out.println(user2);
            System.out.println(user3);
            System.out.println(user2 == user3);//false
        } finally {
            sqlSession.close();
        }
    }

在第一次执行selectById方法获取SysUser数据时,真正执行了数据库查询,得到了user1的结果。第二次执行获取user2的时候,从日志可以看到,在“开启新的sqlSession”
这行日志上面,只有一次查询,也就是说第二次查询并没有执行数据库操作。
从测试代码来看,获取user1后重新设置了userName的值,之后没有进行任何更新数据库的操作。在获取user2对象后,发现user2对象的userName值竟然和user1重新设
置后的值一样。再往下可以发现,原来user1和user2竟然是同一个对象,之所以这样就是因为MyBatis的一级缓存。

MyBatis的一级缓存存在于SqlSession的生命周期中,在同一个SqlSession中查询时,MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中。如果同一个SqlSession中执行的方法和参数完全一致,那么通过算法会生成相同的键值,当Map缓存对象中已经存在该键值时,则会返回缓存中的对象。

缓存中的对象和我们得到的结果是同一个对象,反复使用相同参数执行同一个方法时,总是返回同一个对象,因此就会出现上面测试代码中的情况。在使用MyBatis的过程中,要避免在使用如上代码中的user2时出现的错误。我们可能以为获取的user2应该是数据库中的数据,却不知道user1的一个重新赋值会影响到user2。如果不想让selectById方法使用一级缓存,可以对该方法做如下修改。

    <select id="selectById" resultMap="userMap" flushCache="true">
        select * from sys_user where id = #{id}
    </select>

该修改在原来方法的基础上增加了f1 ushCache:=“true”,这个属性配置为true后,会在查询数据前清空当前的一级缓存,因此该方法每次都会重新从数据库中查询数据,此时的user2和user1就会成为两个不同的实例,可以避免上面的问题。但是由于这个方法清空了一级缓存,会影响当前SqlSession中所有缓存的查询,因此在需要反复查询获取只读数据的情况下,会增加数据库的查询次数,所以要避免这么使用。

在关闭第一个SqlSession后,又重新获取了一个SqlSession,因此又重新查询了user2,这时在日志中输出了数据库查询SQL,user2是一个新的实例,和user1没有任何
关系。这是因为一级缓存是和SqlSession绑定的,只存在于SqlSession的生命周期中。

接下来执行了一个deleteById操作,然后使用相同的方法和参数获取了user3实例,从日志和结果来看,user3和user2也是完全不同的两个对象。这是因为任何的INSERT、
UPDATE、DELETE操作都会清空一级缓存,所以查询user3的时候由于缓存不存在,就会再次执行数据库查询获取数据。

2、二级缓存

MyBatis的二级缓存非常强大,它不同于一级缓存只存在于SqlSession的生命周期中,而是可以理解为存在于SqlSessionFactory的生命周期中。虽然目前还没接触过同时存在多个SqlSessionFactory的情况,但可以知道,当存在多个SqlSessionFactory时,它们的缓存都是绑定在各自对象上的,缓存数据在一般情况下是不相通的。只有在使用如Rdis这样的缓存数据库时,才可以共享缓存。

2.1 配置二级缓存

首先从MyBatis最简单的二级缓存配置开始。在MyBatis的全局配置settings中有一个参数cacheEnabled,这个参数是二级缓存的全局开关,默认值是true,初始状态为启用状态。如果把这个参数设置为false,即使有后面的二级缓存配置,也不会生效。由于这个参数值默认为true,所以不必配置,如果想要配置,可以在mybatis-config.xml
中添加如下代码。

    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>

MyBatis的二级缓存是和命名空间绑定的,即二级缓存需要配置在Mapper.xml映射文件中,或者配置在Mapper.java接口中。在映射文件中,命名空间就是XML根节点mapper的
namespace属性。在Mapper接口中,命名空间就是接口的全限定名称。

xml中配置二级缓存:

在保证二级缓存的全局配置开启的情况下,给RoleMapper…xml开启二级缓存只需要在
UserMapper.xml中添加<cache/>元素即可,添加后的UserMapper…xml如下。

<mapper namespace="pers.zhang.simple.mapper.UserMapper">
    <cache/>
    <!-- 其它配置 -->
</mapper>

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

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

所有的这些属性都可以通过缓存元素的属性来修改,示例如下。

    <cache
            eviction="FIFO"
            flushInterval="60000"
            size="512"
            readOnly="true"/>

这个更高级的配置创建了一个FIF0缓存,并每隔60秒刷新一次,存储集合或对象的512个引用,而且返回的对象被认为是只读的,因此在不同线程中的调用者之间修改它们会导致冲突。
cache可以配置的属性如下。

  • eviction(收回策略)
    • LRU(最近最少使用的):移除最长时间不被使用的对象,这是默认值。
    • FIF0(先进先出):按对象进入缓存的顺序来移除它们。
    • S0FT(软引用):移除基于垃圾回收器状态和软引用规则的对象。
    • WEAK(弱引用):更积极地移除基于垃圾收集器状态和弱引用规则的对象。
  • flushInterval(刷新间隔)。可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况不设置,即没有刷新间隔,缓存仅仅在调用语句时刷新。
  • size(引用数目)。可以被设置为任意正整数,要记住缓存的对象数目和运行环境的可用内存资源数目。默认值是1024。
  • readOnly(只读)。属性可以被设置为true或false。只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,这提供了很重要的性能优势。可读写的缓存会通过序列化返回缓存对象的拷贝,这种方式会慢一些,但是安全,因此默认是false。

Mapper接口中配置二级缓存:

在使用注解方式时,如果想对注解方法启用二级缓存,还需要在Mapper接口中进行配置,如果Mapper接口也存在对应的XML映射文件,两者同时开启缓存时,还需要特殊配置。

当只使用注解方式配置二级缓存时,如果在RoleMapper接口中,则需要增加如下配置。

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

}

当同时使用注解方式和XML映射文件时,如果同时配置了上述的二级缓存,就会抛出异常。
这是因为Mapper接口和对应的XML文件是相同的命名空间,想使用二级缓存,两者必须同时配置(如果接口不存在使用注解方式的方法,可以只在XML中配置),因此按照上面的方式进行配置就会出错,这个时候应该使用参照缓存。

@CacheNamespaceRef(RoleMapper.class)
public interface RoleMapper {

}

因为想让RoleMapper接口中的注解方法和XML中的方法使用相同的缓存,因此使用参照缓存配置RoleMapper.class,这样就会使用命名空间为tk.mybatis.simple,mapper.
RoleMapper的缓存配置,即RoleMapper…xml中配置的缓存。
Mapper接口可以通过注解引用XML映射文件或者其他接口的缓存,在XML中也可以配置参照缓存,如可以在RoleMapper.xml中进行如下修改。

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

这样配置后,XML就会引用Mapper接口中配置的二级缓存,同样可以避免同时配置二级缓存导致的冲突。
MyBatis中很少会同时使用Mapper接口注解方式和XML映射文件,所以参照缓存并不是为了解决这个问题而设计的。参照缓存除了能够通过引用其他缓存减少配置外,主要的作用是解决脏读。

为了保证后续测试一致,对RoleMapper接口和XML映射文件进行如下配置:

@CacheNamespaceRef(RoleMapper.class)
public interface RoleMapper {
}
<mapper namespace="pers.zhang.simple.mapper.RoleMapper">
    
    <cache
        eviction="FIFO"
        flushInterval="60000"
        size="512"
        readOnly="false" />

</mapper>
2.2 使用二级缓存

由于配置的是可读可写的缓存,而MyBatis使用SerializedCache (org.apache.ibatis.cache.decorators.SerializedCache)
序列化缓存来实现可读写缓存类,并通过序列化和反序列化来保证通过缓存获取数据时,得到的是一个新的实例。因此,如果配置为只读缓存,MyBatis就会使用Map来存储缓存值,这种情况下,从缓存中获取的对象就是同一个实例。

因为使用可读写缓存,可以使用SerializedCache序列化缓存。这个缓存类要求有被序列化的对象必须实现Serializable(java.io.Serializable)接口,所以还需要
修改SysRole对象,代码如下。

public class SysRole  implements Serializable {
}

测试类:


日志:

DEBUG [main] - Cache Hit Ratio [pers.zhang.simple.mapper.RoleMapper]: 0.0
DEBUG [main] - ==>  Preparing: select id, role_name roleName, enabled, create_by createBy, create_time createTime from sys_role where id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
TRACE [main] - <==    Columns: id, roleName, enabled, createBy, createTime
TRACE [main] - <==        Row: 1, 管理员, 1, 1, 2016-04-01 17:02:14.0
DEBUG [main] - <==      Total: 1
DEBUG [main] - Cache Hit Ratio [pers.zhang.simple.mapper.RoleMapper]: 0.0
true
开启新的sqlSession
DEBUG [main] - Cache Hit Ratio [pers.zhang.simple.mapper.RoleMapper]: 0.3333333333333333
New Name
false
DEBUG [main] - Cache Hit Ratio [pers.zhang.simple.mapper.RoleMapper]: 0.5
false

日志中存在好几条以Cache Hit Ratio开头的语句,这行日志后面输出的值为当前执行方法的缓存命中率。在测试第一部分中,第一次查询获取ro1e1的时候由于没有缓存,所以执行了数据库查询。在第二个查询获取ro1e2的时候,ro1e2和role1是完全相同的实例,这里使用的是一级缓存,所以返回同一个实例。

当调用close方法关闭SqlSession时,SqlSession才会保存查询数据到二级缓存中。在这之后二级缓存才有了缓存数据。所以可以看到在第一部分的两次查询时,命中率都是0。

在第二部分测试代码中,再次获取ro1e2时,日志中并没有输出数据库查询,而是输出了命中率,这时的命中率是0.3333333333333333。这是第3次查询,并且得到了缓存的值,因此该方法一共被请求了3次,有1次命中,所以命中率就是三分之一。后面再获取ro1e3的时候,就是4次请求,2次命中,命中率为0.5。并且因为可读写缓存的缘故,ro1e2和ro1e3都是反序列化得到的结果,所以它们不是相同的实例。在这一部分,这两个实例是读写安全的,其属性不会互相影响。

MyBatis默认提供的缓存实现是基于Map实现的内存缓存,已经可以满足基本的应用。但是当需要缓存大量的数据时,不能仅仅通过提高内存来使用MyBatis的二级缓存,还可以选择一些类似EhCache的缓存框架或Redis缓存数据库等工具来保存MyBatis的二级缓存数据。

3、集成EhCache缓存

EhCache是一个纯粹的Java进程内的缓存框架,具有快速、精干等特点。具体来说,EhCache主要的特性如下。

  • 快速。
  • 简单。
  • 多种缓存策略。
  • 缓存数据有内存和磁盘两级,无须担心容量问题。
  • 缓存数据会在虚拟机重启的过程中写入磁盘。
  • 可以通过RMI、可插入API等方式进行分布式缓存。
  • 具有缓存和缓存管理器的侦听接口。
  • 支持多缓存管理器实例以及一个实例的多个缓存区域。
3.1 添加依赖
<dependency>
	<groupId>org.mybatis.caches</groupId>
	<artifactId>mybatis-echcache</artifactId>
	<version>1.0.3</version>
</dependency>
3.2 配置EhCache

在src/main/resources目录下新增echcache.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和copyOnWrite属性。这两个属性的配置会对后面使用二级缓存产生很大影响。
copyOnRead的含义是,判断从缓存中读取数据时是返回对象的引用还是复制一个对象返回。默认情况下是false,即返回数据的引用,这种情况下返回的都是相同的对象,和MyBatis默认缓存中的只读对象是相同的。如果设置为true,
那就是可读写缓存,每次读取缓存时都会复制一个新的实例。
copyOnWrite的含义是,判断写入缓存时是直接缓存对象的引用还是复制一个对象然后缓存,默认也是false。如果想使用可读写缓存,就需要将这两个属性配置为true,如果使
用只读缓存,可以不配置这两个属性,使用默认值fa1se即可。

3.3 修改RoleMapper.xml中的缓存配置

ehcache-cache提供了如下2个可选的缓存实现。

  • org.mybatis.caches.ehcache.EhcacheCache
  • org.mybatis.caches.ehcache.LoggingEhcache

在这两个缓存中,第二个是带日志的缓存,由于MyBatis初始化缓存时,如果Cache不是
LoggingEhcache(org.mybatis.caches.ehcache.LoggingEhcache),MyBatis便会使用Logging Ehcache装饰代理缓存,所以上面两个缓存使用时并没有区别,都会输出缓存命中率的日志。

修改RoleMapper.xml中的配置如下。

<mapper namespace="tk.mybatis.simple.mapper.RoleMapper">
	<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
	<!--其他配置-->
</mapper>

只通过设置type属性就可以使用EhCache缓存了,这时cache的其他属性都不会起到任何作用,针对缓存的配置都在ehcache.xml中进行。在ehcache.xml配置文件中,只有一个默认的缓存配置,所以配置使用EhCache缓存的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"/>

4、集成Redis缓存

Redis是一个高性能的key-value数据库。
MyBatis项目开发者提供了Redis的MyBatis二级缓存实现,该项目名为redis-cache,目前只有beta版本,项目地址是https:/∥github.com/mybatis/redis-cache。

4.1 添加项目依赖
<dependency>
	<groupId>org.mybatis.caches</groupId>
	<artifactId>mybatis-redis</artifactId>
	<version>1.0.0-beta2</version>
</dependency>
4.2 配置Redis

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

host=localhost
p0rt=6379
connectionTimeout=5000
soTimeout=5000
password=
database=0

上面这几项是redis-cache项目提供的可以配置的参数,这里配置了服务器地址、端口和超时时间。

4.3 修改RoleMapper.xml中的缓存配置

redis-cache提供了1个Mybatis的缓存实现,org.mytis.caches.redis.RedisCache。

修改RoleMapper.xml中的配置如下。

<mapper namespace="tk.mybatis.simple.mapper.RoleMapper">
	<cache type="org.mybatis.caches.redis.RedisCache"/>
<!--其他配置-->
</mapper>

配置依然很简单,RedisCache在保存缓存数据和获取缓存数据时,使用了Java的序列化和反序列化,因此还需要保证被缓存的对象必须实现Serializable接口。改为RedisCache缓存配置后,testL2Cache测试第一次执行时会全部成功,但是如果再次执行,就会出错。

这是因为Redis作为缓存服务器,它缓存的数据和程序(或测试)的启动无关,Redis的缓存并不会因为应用的关闭而失效。所以再次执行时没有进行一次数据库查询,所有查询都使用缓存,测试的第一部分代码中的ro1e1和role2都是直接从二级缓存中获取数据,因为是可读写缓存,所以不是相同的对象。
当需要分布式部署应用时,如果使用MyBatis自带缓存或基础的EhCahca缓存,分布式应用会各自拥有自己的缓存,它们之间不会共享缓存,这种方式会消耗更多的服务器资源。如果使用类似Redis的缓存服务,就可以将分布式应用连接到同一个缓存服务器,实现分布式应用间的缓存共享。

5、二级缓存适用场景

二级缓存虽然好处很多,但并不是什么时候都可以使用。在以下场景中,推荐使用二级缓存。

  • 以查询为主的应用中,只有尽可能少的增、删、改操作。
  • 绝大多数以单表操作存在时,由于很少存在互相关联的情况,因此不会出现脏数据。
  • 可以按业务划分对表进行分组时,如关联的表比较少,可以通过参照缓存进行配置。

除了推荐使用的情况,如果脏读对系统没有影响,也可以考虑使用。在无法保证数据不出
现脏读的情况下,建议在业务层使用可控制的缓存代替二级缓存。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值