乐趣区

MyBatis缓存介绍

MyBatis 缓存介绍
正如大多数持久层框架一样,MyBatis 同样提供了一级缓存和二级缓存的支持

一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空。
二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。
对于缓存数据更新机制,当某一个作用域 (一级缓存 Session/ 二级缓存 Namespaces) 的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。

mybatis 的相关概念

SqlSession : 代表和数据库的一次会话,向用户提供了操作数据库的方法。
MappedStatement: 代表要发往数据库执行的指令,可以理解为是 Sql 的抽象表示。
Executor: 具体用来和数据库交互的执行器,接受 MappedStatement 作为参数。
映射接口: 在接口中会要执行的 Sql 用一个方法来表示,具体的 Sql 写在映射文件中。
映射文件: 可以理解为是 Mybatis 编写 Sql 的地方,通常来说每一张单表都会对应着一个映射文件,在该文件中会定义 Sql 语句入参和出参的形式。

一级缓存

MyBatis 的一级查询缓存(也叫作本地缓存)是基于 org.apache.ibatis.cache.impl.PerpetualCache 类的 HashMap 本地缓存,其作用域是 SqlSession
在同一个 SqlSession 中两次执行相同的 sql 查询语句,第一次执行完毕后,会将查询结果写入到缓存中,第二次会从缓存中直接获取数据,而不再到数据库中进行查询,这样就减少了数据库的访问,从而提高查询效率。
当一个 SqlSession 结束后,该 SqlSession 中的一级查询缓存也就不存在了。
myBatis 默认一级查询缓存是开启状态,且不能关闭。
增删改会清空缓存,无论是否 commit

当 SqlSession 关闭和提交时,会清空一级缓存
可能你会有疑惑,我的 mybatis bean 是由 spring 来管理的,已经屏蔽了 sqlSession 这个东西了?那怎么的一次操作才算是一次 sqlSession 呢?

spring 整合 mybatis 后,非事务环境下,每次操作数据库都使用新的 sqlSession 对象。因此 mybatis 的一级缓存无法使用(一级缓存针对同一个 sqlsession 有效)
在开启事物的情况之下,spring 使用 threadLocal 获取当前资源绑定同一个 sqlSession,因此此时一级缓存是有效的

在开启以及缓存的时候查询得到的对象是同一个对象。这种情况下会出现一个问题。我们先看一下代码。
public void listMybatisModel() {
List<MybatisModel> mybatisModels = mapper.listMybatisModel();
List<MybatisModel> mybatisModelsOther = mapper.listMybatisModel();
System.out.println(mybatisModels == mybatisModelsOther);
System.out.println(“list count: ” + mybatisModels.size());
}
System.out.println(mybatisModels == mybatisModelsOther);
输出结果竟然是 true,这样说来是同一个对象。会出现这种场景,第一次查出来的对象然后修改了,第二次查出来的就是修改后的对象。
一级缓存实现
对 SqlSession 的操作 mybatis 内部都是通过 Executor 来执行的。Executor 的生命周期和 SqlSession 是一致的。Mybatis 在 Executor 中创建了一级缓存,基于 PerpetualCache 类的 HashMap
二级缓存

MyBatis 的二级缓存是 mapper 范围级别的
SqlSession 关闭后才会将数据写到二级缓存区域
增删改操作,无论是否进行提交 commit(),均会清空一级、二级缓存
二级缓存是默认开启的。(想开启就不必做任何配置)
二级缓存会使用 Least Recently Used (LRU,最近最少使用的)算法来收回。
根据时间表(如 no Flush Interval,没有刷新间隔),缓存不会以任何时间顺序来刷新。
缓存会存储集合或对象(无论查询方法返回什么类型的值)的 1024 个引用。
缓存会被视为 read/write(可读/可写)的,意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

用下面这张图描述一级缓存和二级缓存的关系。

配置二级缓存
在保证二级缓存的全局配置开启的情况下,给某个 xml 开启二级缓存只需要在 xml 中添加 <cache /> 即可
// mybatis-config.xml 中配置
<settings>
默认值为 true。即二级缓存默认是开启的
<setting name=”cacheEnabled” value=”true”/>
</settings>

// 具体 mapper.xml 中配置
<mapper namespace=”cn.itcast.mybatis.mapper.UserMapper”>
<!– 开启本 mapper 的 namespace 下的二级缓存
type:指定 cache 接口的实现类的类型,mybatis 默认使用 PerpetualCache
要和 ehcache 整合,需要配置 type 为 ehcache 实现 cache 接口的类型 –>

<cache />

</mapper>
如果想要设置增删改操作的时候不清空二级缓存的话,可以在其 insert 或 delete 或 update 中添加属性 flushCache=”false”,默认为 true。
<delete id=”deleteStudent” flushCache=”false”>
DELETE FROM user where id=#{id}
</delete>
通过以下一些配置可以修改一些缓存参数。
<cache
eviction= “FIFO”
flushlnterval=”600000″
size=”512″
readOnly=”true” />
配置创建了一个 FIFO 缓存,并每隔 60 秒刷新一次,存储集合或对象的 512 个引用,而且返回的对象被认为是只读的, 因此在不同线程中的调用者之间修改它们会导致冲突。cache 可以配置的属性如下。eviction(收回策略)
LRU(最近最少使用的: 移除最长时间不被使用的对象,这是默认值。FIFO(先进先出〉:按对象进入缓存的顺序来移除它们。SOFT(软引用):移除基于垃圾回收器状态和软引用规则的对象。WEAK(弱引用):更积极地移除基于垃圾收集器状态和弱引用规则的对象
flushinterval(刷新间隔)。
可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况不设置,即没有刷新间隔,缓存仅仅在调用语句时刷新。

size(引用数目)。可以被设置为任意正整数,要记住缓存的对象数目和运行环境的可用内存资源数目。默认值是 1024。
readOnly(只读)。属性可以被设置为 true 或 false。只读的缓存会给所有调用者

返回缓存对象的相同实例,因此这些对象不能被修改,这提供了很重要的性能优势。可读写的缓存会通过序列化返回缓存对象的拷贝,这种方式会慢一些,但是安全,因此默认是 false。
当只使用注解方式配置二级缓存时,如果在 Mapper 接口中,则需要增加如下配置。
@CacheNamespace (
eviction = FifoCache.class ,
flushinterval = 60000 ,
size = 512 ,
readWrite = true)
public interface Mapper {

}
括号内的内容是配置缓存属性。
Mapper 接口和对应的 XML 文件是相同的命名空间,想使用二级缓存,两者必须同时配置(如果接口不存在使用注解方式的方法,可以只在 XML 中配置〉,因此按照上面的方
式进行配置就会出错,这个时候应该使用参照缓存。在 Mapper 接口中,参照缓存配置如下。
@CacheNarnespaceRef(RoleMapper.class)
public interface RoleMapper {
因为想让 RoleMapper 接口中的注解方法和 XML 中的方法使用相同的缓存,因此使用参照缓存配置 RoleMapper.class,这样就会使用命名空间为 xx.xxx.xxx.xxx.RoleMapper 的缓存配置,即 RoleMapper.xml 中配置的缓存。Mapper 接口可以通过注解引用 XML 映射文件或者其他接口的缓存,在 XML 中也可以配置参照缓存,如可以在 RoleMapper.xml 中进行如下修改。
<cache-ref narnespace=”xxx.xxx.xxx.xxx.RoleMapper”/>
这样配置后 XML 就会引用 Mapper 接口中配置的二级缓存,同样可以避免同时配置二级缓存导致的冲突。MyBatis 中很少会同时使用 Mapper 接口注解方式和 XML 映射文件,所以参照缓存并不是为了解决这个问题而设计的。参照缓存除了能够通过引用其他缓存减少配置外,主要的作用是解决脏读。
MyBatis 使用 SerializedCache(org.apache.ibaits.cache.decorators.SerializedCache)序列化缓存来实现可读写缓存类,井通过序列化和反序列化来保证通过缓存获取数据时,得到的是一个新的实例。因此,如果配置为只读缓存,MyBatis 就会使用 Map 来存储缓存值,这种情况下,从缓存中获取的对象就是同一个实例。因为使用可读写缓存,可以使用 SerializedCache 序列化缓存。这个缓存类要求所有被序列化的对象必须实现 Serializable (java.io.Serializable)接口虽然使用序列化得到的对象都是不一样的对象修改时都是互不影响,但是还是不安全的。
脏读的产生
Mybatis 的二级缓存是和命名空间绑定的,所以通常情况下每一个 Mapper 映射文件都有自己的二级缓存,不同的 mapper 的二级缓存互不影响。
引起脏读的操作通常发生在多表关联操作中,比如在两个不同的 mapper 中都涉及到同一个表的增删改查操作,当其中一个 mapper 对这张表进行查询操作,此时另一个 mapper 进行了更新操作刷新缓存,然后第一个 mapper 又查询了一次,那么这次查询出的数据是脏数据。出现脏读的原因是他们的操作的缓存并不是同一个。
脏读的避免

mapper 中的操作以单表操作为主,避免在关联操作中使用 mapper
使用参照缓存

集成 EhCache 缓存
缓存数据有内存和磁盘两级,无须担心容量问题。

缓存数据会在虚拟机重启的过程中写入磁盘。可以通过 RMI、可插入 API 等方式进行分布式缓存。
具有缓存和缓存管理器的侦昕接口。
支持多缓存管理器实例以及一个实例的多个缓存区域。

1. 添加项目依赖
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.0.3</version>
</dependency>
2. 配置 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>
有关 EhCache 的详细配置可以参考地址 http://www.ehcache.org/ehcach… 中的内容。

copyOnRead 的含义是,判断从缓存中读取数据时是返回对象的引用还是复制一个对象返回。默认情况下是 false,即返回数据的引用,这种情况下返回的都是相同的对象,和 MyBatis 默认缓存中的只读对象是相同的。如果设置为 true,那就是可读写缓存,每次读取缓存时都会复制一个新的实例。
copyOnWrite 的含义是,判断写入缓存时是直接缓存对象的引用还是复制一个对象然后缓存,默认也是 false。如果想使用可读写缓存,就需要将这两个属性配置为 true,如果使用只读缓存,可以不配置这两个属性,使用默认值 false 即可。

修改 Mapper.xml 中的缓存配置
ehcache-cache 提供了如下 2 个可选的缓存实现。

org.mybatis.caches.ehcache.EhcacheCache
org.mybatis.caches.ehcache.LoggingEhcache 这个是带日志的缓存。

在 xml 中添加
<cache type =”org.mybatis.caches.ehcache.EhcacheCache” />
只通过设置 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”/>
集成 Redis 缓存
添加依赖,目前只有 bata 版本。
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
配置 Redis
使用 Redis 前,必须有一个 Redis 服务,有关 Redis 安装启动的相关内容,可参考如下地址中的官方文档:https://redis.io/topics/quick…。Redis 服务启动后,在 src/main/resources 目录下新增 redis.properties 文件。
host=localhost
port=6379
connectionTimeout=SOOO
soTimeout=SOOO
password=
database=O
clientName=
修改 mapper.xml 中的配置。
<mapper namespace =”tk.mybat 工 s.s 工 mple.mapper.RoleMapper”〉
<cache type= “org.mybatis.caches.redis.RedisCache” />
〈!一其他自己直一 〉
</mapper>
配置依然很简单,RedisCache 在保存缓存数据和获取缓存数据时,使用了 Java 的序列化和反序列化,因此还需要保证被缓存的对象必须实现 Serializable 接口。改为 RedisCache 缓存配置后,testL2Cache 测试第一次执行时会全部成功,但是如果再次执行,就会出错。这是因为 Redis 作为缓存服务器,它缓存的数据和程序(或测试)的启动无关,Redis 的缓存并不会因为应用的关闭而失效。所以再次执行时没有进行一次数据库查询,所有查询都使用缓存,测试的第一部分代码中的 rolel 和 role2 都是直接从二级缓存中获取数据,因为是可读写缓存,所以不是相同的对象。当需要分布式部署应用时,如果使用 MyBatis 自带缓存或基础的 EhCahca 缓存,分布式应用会各自拥有自己的缓存,它们之间不会共享缓存,这种方式会消耗更多的服务器资源。如果使用类似 Redis 的缓存服务,就可以将分布式应用连接到同一个缓存服务器,实现分布式应用间的缓存共享。

退出移动版