MyBatis快速入门(14)XML映射文件---缓存

前面的内容我们讨论了映射文件里面几乎所有的标签和用法,下面我来讨论最后一个,也就是缓存的用法。







一级缓存

Mybatis对缓存提供支持,但是在没有配置的默认情况下,它只开启一级缓存,一级缓存只是相对于同一个SqlSession而言。所以在参数和SQL完全一样的情况下,我们使用同一个SqlSession对象调用一个Mapper方法,往往只执行一次SQL,因为使用SelSession第一次查询后,MyBatis会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession都会取出当前缓存的数据,而不会再次发送SQL到数据库。

一级缓存的生命周期有多长?

a、MyBatis在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象。Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。

b、如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。

c、如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。

d、SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用

怎么判断某两次查询是完全相同的查询?mybatis认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询。

传入的statementId

查询时要求的结果集中的结果范围

这次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql() )

传递给java.sql.Statement要设置的参数值







一级缓存示例

一级缓存是默认开启的,一级缓存只是相对于同一个SqlSession而言,也就是说,同一个SqlSession执行同一个查询语句两次,只会在第一次访问数据,例如:

我们来执行这个方法,看下控制台输出:

可以看到,SQL语句只打印了一次,但是name打印了两次。但是如果第一次查询后,执行了清理缓存或者增删改之类的操作,可以清理掉一级缓存,例如:

session.clearCache();可以清理掉sqlSession级别的缓存。看一下效果:

可以看到,执行了查询两次,每次都访问了数据库,打印SQL语句也是两次。还有另一种方法可以针对专门的语句,在查询前清空缓存,设置方法是在SQL语句的标签上设置属性 flushCache="true" ,

同样的调用方法:

可以看到控制台输出的结果:

关于一级缓存,是默认开启默认执行的,sqlSession级别的,使用的时候要注意,不要轻易关闭,否则增加数据库负担。






二级缓存

MyBatis的二级缓存是应用级别的缓存,它可以提高对数据库查询的效率,以提高应用的性能。MyBatis的缓存机制整体设计以及二级缓存的工作模式:

SqlSessionFactory层面上的二级缓存默认是不开启的,二级缓存的开席需要进行配置,实现二级缓存的时候,MyBatis要求返回的POJO必须是可序列化的。 也就是要求实现Serializable接口,配置方法很简单,只需要在映射XML文件配置就可以开启缓存了:

<cache/>

如果我们配置了二级缓存就意味着:

映射语句文件中的所有select语句将会被缓存。

映射语句文件中的所欲insert、update和delete语句会刷新缓存。

缓存会使用默认的Least Recently Used(LRU,最近最少使用的)算法来收回。

根据时间表,比如No Flush Interval,(CNFI没有刷新间隔),缓存不会以任何时间顺序来刷新。

缓存会存储列表集合或对象(无论查询方法返回什么)的1024个引用

缓存会被视为是read/write(可读/可写)的缓存,意味着对象检索不是共享的,而且可以安全的被调用者修改,不干扰其他调用者或线程所做的潜在修改。

缓存只作用于 cache 标签所在的映射文件中的语句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的语句将不会被默认缓存。你需要使用 @CacheNamespaceRef 注解指定缓存作用域(Java API后面会讨论)。

这些属性可以通过 cache 元素的属性来修改。比如:

这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。

可用的清除策略有(默认的清除策略是 LRU):

LRU – 最近最少使用:移除最长时间不被使用的对象。

FIFO – 先进先出:按对象进入缓存的顺序来移除它们。

SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。

WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。

size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。

readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。

提示: 二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。







二级缓存示例

首先在mybatis配置文件中,开启全局二级缓存:

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

这个参数默认值是true,所以不写也可以。如果设置成false,即使有二级缓存的配置也不会生效。二级缓存是和命名空间绑定的,所以需要在mapper映射文件中增加cache标签:

<cache/>

或者在mapper接口中配置:

不过为了代码整洁,一般都选择在SQL映射文件中配置。

对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新。 但你可能会想要在多个命名空间中共享相同的缓存配置和实例。要实现这种需求,你可以使用 cache-ref 元素来引用另一个缓存:

或者在Java接口类中配置:

按照上面配置好以后,二级缓存就已经开始起作用了。先来执行一个简单的例子:

调用方法两次,会生成两个sqlSession,执行同样的操作语句我们看一下控制台打印几次:

可以看到二级缓存确实起到了作用。这次我们查询的是个单个字段没有问题,如果我们查询一个对象,由于配置的是可读写的缓存,对象类必须实现序列化接口,所以需要修改类:

下面看一下测试代码:

执行效果如下:

可以看到不同的sqlSession查询用户对象两次,只执行了一次数据库查询,二级缓存确实起到了作用。








集成自定义缓存--Redis

mybatis还可以集成第三方的缓存,来提高可靠性。下面来介绍mybatis集成最常用的redis缓存。mybatis项目本身提供了redis的二级缓存实现,首先添加项目依赖:

下一步在resource目录下添加redis本身的配置文件redis.properties,配置连接参数:

在我们加入的redis缓存依赖中,有一个mybatis的缓存实现类,需要配置到SQL映射文件中:

这样第三方缓存就集成完了,非常简单。下面运行方法测试一次:

运行结果:

这样其实还看不出集成了第三方缓存,我们再来执行一次:

可以看到从新运行第二次的时候,由于缓存已经存在,所以一次也不从数据库查了。我们来看一下redis中是否存了进去:

可以看到数据的确是存入了缓存中。通过其中的key可以分辨出是不是完全相同的一次查询:





避免脏数据

在上面的二级缓存示例中,提到的cache-ref标签可以使多个mapper共享一个缓存,在多个表有关联的时候,可以在多个mapper中共享一个缓存,这样能规避关联查询中的脏读问题。


代码地址:https://gitee.com/blueses/mybatis-demo  07

推荐阅读更多精彩内容