前言
最近还是在弄片刻的二次版本,在写后端服务的时候,因为考虑的比较多,一是想多学习学习其他的技术,二是想巩固现在已经会的技术,所以在写的时候束手束脚的,对于很多以前用的东西理解的都不是很透彻,导致了原本有简便的方法解决问题,现在却用了一种很麻烦的解决办法来解决问题。
所以我又给自己挖了一个坑。
简介
MyBatis 在内部提供了查询缓存的机制,用于减轻数据库压力,提高数据库性能。
MyBatis 可以提供一级缓存和二级缓存,一级缓存的主要作用域在Sqlsession,而二级缓存的作用域在Mapper 。
一级缓存的作用域在SqlSession,在操作数据库时需要构造SqlSession 对象,在对象中有一个数据结构用于存储缓存数据,不同的SqlSession 之间的缓存数据区域是互不影响。
Mybatis 默认开启一级缓存。
一级缓存在同一个SqlSession 下执行两次相同的sql 语句,第一次执行sql 语句会将从数据库查询到的数据写入到缓存中去,而第二次会直接从缓存中去取数据,不再去数据库查询数据,从而提高查询效率。
从上述德尔描述中,大致能明白在SqlSession 中进行sql 语句执行,遇到插入、更新、删除操作时,一级缓存中的数据会被删除,直到下一次查询,数据才会重新被放到缓存中。
二级缓存的作用域在Mapper ,多个SqlSession 去操作同一个Mapper 中的sql 语句,多个SqlSession 操作数据库会获得二级缓存,而SqlSession 之间是可以共享二级缓存的,也就是说二级缓存是跨SqlSession的。
MyBatis 默认不开启二级缓存。
一级缓存
在系统运行过程中,有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL 语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
基础
每个SqlSession 中持有了Executor ,每个Executor 中有一个LocalCache 。
当用户发起查询时,MyBatis 根据当前执行的语句生成MappedStatement ,在Local Cache 进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache,最后返回结果给用户。
如何使用MyBatis 的一级缓存。只需在MyBatis 的配置文件中,添加如下语句,就可以使用一级缓存。
共有两个选项,SESSION 或者STATEMENT ,默认是SESSION 级别,即在一个MyBatis 会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT 级别,可以理解为缓存只对当前执行的这一个Statement 有效。
1 | <setting name="localCacheScope" value="SESSION"/> |
命中原则
- SqlSession 不同,由于一级缓存是基于SqlSession 级别的,所以当使用不同SqlSession 进行查询时缓存也不同。
- SqlSession 相同,手动清空一级缓存。
- SqlSession 相同,两次查询之间执行了增删改操作。
- SqlSession 相同,查询条件不同。
生命周期
源码解读
- SqlSession: 对外提供了用户和数据库之间交互需要的所有方法,隐藏了底层的细节。默认实现类是DefaultSqlSession 。
- Executor: SqlSession 向用户提供操作数据库的方法,但和数据库操作有关的职责都会委托给Executor ,一级缓存主要学习BaseExecutor 的内部实现。
- Cache: MyBatis 中的Cache 接口,提供了和缓存相关的最基本的操作。
总结
- MyBatis 一级缓存的生命周期和SqlSession 一致。
- MyBatis 一级缓存内部设计简单,只是一个没有容量限定的HashMap ,在缓存的功能性上有所欠缺。
- MyBatis 的一级缓存最大范围是SqlSession 内部,有多个SqlSession 或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement 。(在这种情况下,建议使用自定义缓存)
二级缓存
在一级缓存中,其最大的共享范围就是一个SqlSession 内部,如果多个SqlSession 之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用CachingExecutor 装饰Executor ,进入一级缓存的查询流程前,先在CachingExecutor 进行二级缓存的查询。
基础
二级缓存开启后,同一个namespace 下的所有操作语句,都影响着同一个Cache ,即二级缓存被多个SqlSession 共享,是一个全局的变量。
当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
配置二级缓存:
- 在MyBatis的配置文件中开启二级缓存。
1
<setting name="cacheEnabled" value="true"/>
- 在MyBatis的映射XML中配置cache或者 cache-ref 。
1
<cache/>
- type: cache使用的类型,默认是PerpetualCache ,这在一级缓存中提到过。
- eviction: 定义回收的策略,常见的有FIFO 、LRU 。
- flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。
- size: 最多缓存对象的个数。
- readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
- blocking: 若缓存中找不到对应的key,是否会一直blocking ,直到有对应的数据进入缓存。
1 | <cache-ref namespace="mapper.StudentMapper"/> |
cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。
命中原则
二级缓存的命中原则基本上与一级缓存一致,但是二级缓存的基础是在SqlSessionFactory 上,而一级缓存是在SqlSession 上,这是区别点。
源码解读
MyBatis 二级缓存的工作流程和前文提到的一级缓存类似,只是在一级缓存处理前,用CachingExecutor 装饰了BaseExecutor 的子类,在委托具体职责给delegate 之前,实现了二级缓存的查询和写入功能。
自定义缓存
- 引入Redis、 Memcache 等缓存组件。
1
2
3
4
5
6
7
8
9
10
11
12<!-- spring-redis实现 -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.6.2.RELEASE</version>
</dependency>
<!-- redis客户端jar -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.0</version>
</dependency> - 配置Redis、 Memcache 等配置文件。
1
2
3
4
5
6
7redis.host=192.168.31.55
redis.port=6379
redis.pass=
redis.maxIdle=300
redis.maxActive=600
redis.maxWait=1000
redis.testOnBorrow=true - 扩展MyBatis 自定义的cache 接口,使用Redis、 Memchache 将其重写。
1
public class RedisCache implements Cache{}
- 在mapper 文件中加入二级缓存配置。
1
<cache type="com.example.mybatis.cache.RedisCache"/>
- MyBatis config 文件中开启二级缓存
1
2
3
4<settings>
<!--这个配置使全局的映射器(二级缓存)启用或禁用缓存-->
<setting name="cacheEnabled" value="true" />
</setting> - 单元测试
后记
引用
https://www.imooc.com/learn/1238
https://tech.meituan.com/2018/01/19/mybatis-cache.html
总结
- MyBatis 的二级缓存相对于一级缓存来说,实现了SqlSession 之间缓存数据的共享,同时粒度更加的细,能够到namespace 级别,通过Cache 接口实现类不同的组合,对Cache 的可控性也更强。
- MyBatis 在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。为了解决这个问题,在使用二级缓存时,建议使用自定义缓存机制来实现二级缓存。
- 在分布式环境下,由于默认的MyBatis Cache 实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis 的Cache 接口实现,有一定的开发成本,直接使用Redis、Memcached 等分布式缓存可能成本更低,安全性也更高。
个人备注
此博客内容均为作者学习慕课网《Mybatis缓存详解》所做笔记,侵删!
若转作其他用途,请注明来源!