前言
我们在开发中经常会使用缓存来减轻生产数据库的压力,提升网站的QPS。MyBatis
同样提供了两种缓存机制,虽然我们在开发中很少用到,但是作为面试中的常问知识点,我们还是要有一定的了解。
本博文详细阐述了MyBatis
缓存的原理以及使用方式,详情看正文。
MyBatis缓存详解
总览
MyBatis
的缓存分为一级缓存和二级缓存。其结构图如下:

由上图可知:
- 一级缓存是
SqlSession
级别的缓存。在操作数据库时需要构造sqlSession
对象,在对象中有一个数据结构(HashMap
)用于存储缓存数据。不同的sqlSession
之间的缓存数据区域(HashMap
)是互相不影响的。 - 二级缓存是
Mapper
级别的缓存,多个SqlSession
去操作同一个Mapper
的sql
语句,多个SqlSession
可以共用二级缓存,二级缓存是跨SqlSession
的。
一级缓存
Session是什么
我们知道,一级缓存是SqlSession
级别的。在这里我们首先来解释一下Session是什么。
我们在各大框架的学习过程中(无论是Spring
还是MyBatis
等等),都会有Session的概念。Session
意义上来说就是“会话”。这个怎么理解呢?其实也就是我们客户端和服务端的一次交互,交互的内容和双方身份以及交互中产生的数据结果都可以存储到会话中,并且会话不会永久储存,都会有过期时间。
对于SpringMVC
来说,Session
使用Cookie
技术来实现。我们的MyBatis
使用工厂来生产会话对象。
为什么MyBatis集成Spring之后一级缓存失效
我们听说过MyBatis
有一级缓存,但是在我们的日常开发中(通常使用SSM架构)的时候,根本就没有用过MyBatis
的一级缓存。查看过源码的同学都知道,MyBatis
的一级缓存默认是开启的(是一个Map
结构),为什么在集成Spring
之后就失效了呢?
其实,一级缓存并没有失效,而是我们每次执行查询请求,Spring
都会新开启一个SqlSession
来进行数据库交互,交互完就销毁掉了。所以我们的一级缓存是不生效的。在Spring
中,MyBatis
的执行流程如下:
- 接受到请求,
Spring
申请一个SqlSession
(将其视为资源),并绑定当前线程的上下文中(ThreadLocal
) Mapper
代理对象内部的template
属性获取SqlSession
去执行操作- 查询结束,释放资源
- 返回响应结果
具体Spring
怎么将Mapper
对象生成代理而放入容器中我们不细讲,我们从Mapper
的代理过程讲起,来更具体的阐述一下原因。
我们都知道,Mapper
的代理接口是由SqlSession
产生的,而SqlSession
是由SqlSessionFactory
产生的。我们查看一个SSM环境下的SqlSession
接口的实现如下:

可以看到,有三个实现类,其中我们需要关注的是SqlSessionTemplate
这个实现类。如下图:

我们可以看到,本类就是一个SqlSeesion
实现类,它的getMapper
方法的实现和默认的一样,都是一层一层的调用,最终将自己封装进动态代理类里面。有兴趣的可以自己翻阅源码看一下,这里就不再赘述。我们在调用代理Mapper
的方法的时候,都会调用到本类的增删改查方法,而本类的增删改查方法的实现如下图所示:

由上图可以看出,所有的方法都是调用内部属性sqlSessionProxy
的方法。那么,这个sqlSessionProxy
是怎么产生的呢?

由构造器我们可以看出,他是一个接口代理类,实现逻辑是通过一个叫SqlSessionInterceptor
的内部类实现的,我们看一下这个内部类:

由内部类逻辑我们可以看出来,我们的每次数据库操作真正执行的SqlSession
使用过一个SqlSessionUtils
来生产的,我们点进去看一下:

看到这里,逻辑就很清晰了。获取SqlSession
的过程就是先去资源池(Spring
会把SqlSession
当作资源,会在请求开始的时候放到线程上下文,也就是ThreadLocal
里面)中获取,获取不到就新创建一个。也就是说,我们每次请求的执行都是新的SqlSession
来执行的,所以一级缓存会失效。
一级缓存的简单使用
在一个sqlSession
中,对User
表根据id进行两次查询,查看他们发出SQL
语句的情况:
@Test
public void test1(){
//根据 sqlSessionFactory 产生 session
SqlSession sqlSession = sessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//第一次查询,发出sql语句,并将查询出来的结果放进缓存中
User u1 = userMapper.selectUserByUserId(1);
System.out.println(u1);
//第二次查询,由于是同一个sqlSession,会在缓存中查询结果
//如果有,则直接从缓存中取出来,不和数据库进行交互
User u2 = userMapper.selectUserByUserId(1);
System.out.println(u2);
sqlSession.close();
}
查看控制台打印情况:

我们发现,第二次查询是走了缓存的。那么缓存什么时候会失效呢?带着这个疑问我们进行下面的验证:
@Test
public void test2(){
//根据 sqlSessionFactory 产生 session
SqlSession sqlSession = sessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//第一次查询,发出sql语句,并将查询的结果放入缓存中
User u1 = userMapper.selectUserByUserId( 1 );
System.out.println(u1);
//第二步进行了一次更新操作,sqlSession.commit()
u1.setSex("女");
userMapper.updateUserByUserId(u1);
sqlSession.commit();
//第二次查询,由于是同一个sqlSession.commit(),会清空缓存信息
//则此次查询也会发出sql语句
User u2 = userMapper.selectUserByUserId(1);
System.out.println(u2);
sqlSession.close();
}
查看控制台打印情况:

我们看到,更新操作会清掉一级缓存。
由此,我们总结如下:
- 第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从数据库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。
- 如果中间
sqlSession
去执行commit
操作(执行插入、更新、删除),则会清空SqlSession
中的 一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。 - 第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直接从缓存中获取用户信息。
一级缓存的实现原理
一级缓存到底是什么?一级缓存什么时候被创建、一级缓存的工作流程是怎样的?我们可能会有这样的疑问,下面我们就从源码中看一下一级缓存的实现原理。
大家可以这样想,上面我们一直提到一级缓存,那么提到一级缓存就绕不开SqlSession
,所以索性我们就直接从SqlSession
入手,看看有没有创建缓存或者与缓存有关的属性或者方法。

调研了一圈,发现上述所有方法中,好像只有clearCache()
和缓存沾点关系,那么就直接从这个方法入手,分析源码时,我们要看它(此类)是谁,它的父类和子类分别又是谁,对如上关系了解了,我们才会对这个类有更深的认识。

再深入分析,流程走到Perpetualcache
中的clear()
方法之后,会调用其cache.clear()
方法,那么这个cache
是什么东西呢?
点进去发现,cache
其实就是private Map cache = new HashMap();
也就是一个Map
,所以说cache.clear()
其实就是map.clear()
,也就是说,缓存其实就是本地存放的一个map
对象,每一个SqISession
都会存放一个map
对象的引用,那么这个cache
是何时创建的呢?
你觉得最有可能创建缓存的地方是哪里呢?我觉得是Executor
,为什么这么认为?因为Executor
是执行器,用来执行SQL
请求,而且清除缓存的方法也在Executor
中执行,所以很可能缓存的创建也很有可能在Executor
中,看了一圈发现Executor
中有一个createCacheKey
方法,这个方法很像是创建缓存的方法啊,跟进去看看,你发现createCacheKey
方法是由BaseExecutor
执行的,代码如下:
CacheKey cacheKey = new CacheKey();
//MappedStatement 的 id
// id就是Sql语句的所在位置包名+类名+ SQL名称
cacheKey.update(ms.getId());
// offset 就是 0
cacheKey.update(rowBounds.getOffset());
// limit 就是 Integer.MAXVALUE
cacheKey.update(rowBounds.getLimit());
//具体的SQL语句
cacheKey.update(boundSql.getSql());
//后面是update 了 sql中带的参数
cacheKey.update(value);
...
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
创建缓存key
会经过一系列的update
方法,update
方法由一个CacheKey
这个对象来执行的,这个update
方法最终由updateList
的list
来把五个值存进去。如下图所示:

那么我们回归正题,那么创建完缓存之后该用在何处呢?总不会凭空创建一个缓存不使用吧?绝对不会的,经过我们对一级缓存的探究之后,我们发现一级缓存更多是用于查询操作,毕竟一级缓存也叫做查询缓存。我们先来看一下这个缓存到底用在哪了,我们跟踪到query
方法如下:
Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
//创建缓存
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
...
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
//这个主要是处理存储过程用的。
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
...
}
// queryFromDatabase 方法
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
如果查不到的话,就从数据库查,在queryFromDatabase
中,会对localcache
进行写入。 localcache
对象的put
方法最终交给Map
进行存放。
private Map<Object, Object> cache = new HashMap<Object, Object>();
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
二级缓存
原理
二级缓存的原理和一级缓存原理一样,第一次查询,会将数据放入缓存中,然后第二次查询则会直接去缓存中取。但是一级缓存是基于sqlSession
的,而二级缓存是基于mapper
文件的namespace
的,也就是说多个sqlSession
可以共享一个mapper
中的二级缓存区域,并且如果两个mapper
的namespace
相同,即使是两个mapper
,那么这两个mapper
中执行SQL
查询到的数据也将存在相同的二级缓存区域中。

二级缓存的使用
二级缓存默认是关闭的,需要手动开启。开启方法如下:
首先在全局配置文件sqlMapConfig.xml
文件中加入如下代码:
<!--开启二级缓存-->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
其次在UserMapper.xml
文件中开启缓存:
<!--开启二级缓存-->
<cache></cache>
我们可以看到mapper.xml
文件中就这么一个空标签,其实这里可以配置。PerpetualCache
这个类是mybatis
默认实现缓存功能的类。我们不写type
就使用mybatis
默认的缓存,也可以去实现Cache
接口来自定义缓存,我们在此不在赘述,有兴趣的小伙伴可以试一下,但是要注意返回对象实现序列化接口,否则缓存会有问题。
二级缓存的使用和一级缓存大体类似,开启了之后,查询操作会在命名空间内共享查询结果。有事务提交操作会清空缓存。
useCache和flushCache
MyBatis
中还可以配置useCache
和flushCache
等配置项。细节如下:
useCache
是用来设置是否禁用二级缓存的,在statement
中设置useCache=false
可以禁用当前select
语句的二级缓存,即每次查询都会发出SQL
去查询,默认情况是true
,即该SQL
使用二级缓存。flushCache
是用来设置是不是要刷新缓存的,默认是true
,即默认是刷新缓存的。如果update
、insert
或者delete
操作设置成了false
,则不会刷新缓存,有可能造成数据脏读。
二级缓存整合Redis
上面我们介绍了MyBatis
自带的二级缓存,但是这个缓存是单服务器工作,无法实现分布式缓存。 那么什么是分布式缓存呢?假设现在有两个服务器1和2,用户访问的时候访问了 1服务器,查询后的缓存就会放在1服务器上,假设现在有个用户访问的是2服务器,那么他在2服务器上就无法获取刚刚那个缓存,如下图所示:

为了解决这个问题,就得找一个分布式的缓存,专门用来存储缓存数据的,这样不同的服务器要缓存数据都往它那里存,取缓存数据也从它那里取,如下图所示:

如上图所示,在几个不同的服务器之间,我们使用第三方缓存框架,将缓存都放在这个第三方框架中, 然后无论有多少台服务器,我们都能从缓存中获取数据。
这里我们介绍MyBatis
与Redis
的整合。
刚刚提到过,MyBatis
提供了一个Cache
接口,如果要实现自己的缓存逻辑,实现Cache
接口开发即可。MyBatis
本身默认实现了一个,但是这个缓存的实现无法实现分布式缓存,所以我们要自己来实现。Redis
分布式缓存就可以,MyBatis
提供了一个针对Cache
接口的Redis
实现类,该类存在mybatis-redis包中。所以我们要使用Redis
来实现二级缓存,就要加入如下依赖:
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
修改我们的Mapper
文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.rubin.mapper.IUserMapper">
<cache type="org.mybatis.caches.redis.RedisCache" />
<select id="findAll" resultType="com.rubin.pojo.User" useCache="true">
select * from user
</select>
</mapper>
在我们的classpath
下创建redis.properties
配置文件:
redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0
编写测试代码:
@Test
public void SecondLevelCache(){
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();
IUserMapper mapper1 = sqlSession1.getMapper(IUserMapper.class);
lUserMapper mapper2 = sqlSession2.getMapper(lUserMapper.class);
lUserMapper mapper3 = sqlSession3.getMapper(IUserMapper.class);
User user1 = mapper1.findUserById(1);
//清空一级缓存
sqlSession1.close();
User user = new User();
user.setId(1);
user.setUsername("lisi");
mapper3.updateUser(user);
sqlSession3.commit();
User user2 = mapper2.findUserById(1);
System.out.println(user1==user2);
}
二级缓存的问题
二级缓存由于是namespace
级别的缓存,就是说,在事务间是共享的。那么在高并发的场景下,就可能会引发线程安全问题,也就是脏读问题。我们的MyBatis
已经很好的解决了这个问题,解决的方式就是利用装饰器模式装饰了一个事务级别的Cache
。详情如下:
通过TransactionalCacheManager
来管理事务之间的二级缓存:
/** 事务缓存管理器 */
public class TransactionalCacheManager {
// Cache 与 TransactionalCache 的映射关系表
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
public void clear(Cache cache) {
// 获取 TransactionalCache 对象,并调用该对象的 clear 方法,下同
getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
// 直接从TransactionalCache中获取缓存
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
// 直接存入TransactionalCache的缓存中
getTransactionalCache(cache).putObject(key, value);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
private TransactionalCache getTransactionalCache(Cache cache) {
// 从映射表中获取 TransactionalCache
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
// TransactionalCache 也是一种装饰类,为 Cache 增加事务功能
// 创建一个新的TransactionalCache,并将真正的Cache对象存进去
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
}
TransactionalCacheManager
内部维护了 Cache
实例与 TransactionalCache
实例间的映射关系,该类也仅负责维护两者的映射关系,真正做事的还是 TransactionalCache
。TransactionalCache
是一种缓存装饰器,可以为 Cache
实例增加事务功能。我在之前提到的脏读问题正是由该类进行处理的。下面分析一下该类的逻辑。
public class TransactionalCache implements Cache {
//真正的缓存对象,和上面的Map<Cache, TransactionalCache>中的Cache是同一个
private final Cache delegate;
private boolean clearOnCommit;
// 在事务被提交前,所有从数据库中查询的结果将缓存在此集合中
private final Map<Object, Object> entriesToAddOnCommit;
// 在事务被提交前,当缓存未命中时,CacheKey 将会被存储在此集合中
private final Set<Object> entriesMissedInCache;
@Override
public Object getObject(Object key) {
// 查询的时候是直接从delegate中去查询的,也就是从真正的缓存对象中查询
Object object = delegate.getObject(key);
if (object == null) {
// 缓存未命中,则将 key 存入到 entriesMissedInCache 中
entriesMissedInCache.add(key);
}
if (clearOnCommit) {
return null;
} else {
return object;
}
}
@Override
public void putObject(Object key, Object object) {
// 将键值对存入到 entriesToAddOnCommit 这个Map中中,而非真实的缓存对象 delegate中
entriesToAddOnCommit.put(key, object);
}
@Override
public Object removeObject(Object key) {
return null;
}
@Override
public void clear() {
clearOnCommit = true;
// 清空 entriesToAddOnCommit,但不清空 delegate 缓存
entriesToAddOnCommit.clear();
}
public void commit() {
// 根据 clearOnCommit 的值决定是否清空 delegate
if (clearOnCommit) {
delegate.clear();
}
// 刷新未缓存的结果到 delegate 缓存中
flushPendingEntries();
// 重置 entriesToAddOnCommit 和 entriesMissedInCache
reset();
}
public void rollback() {
unlockMissedEntries();
reset();
}
private void reset() {
clearOnCommit = false;
// 清空集合
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
// 将 entriesToAddOnCommit 中的内容转存到 delegate 中
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
// 存入空值
delegate.putObject(entry, null);
}
}
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
try {
// 调用 removeObject 进行解锁
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("...");
}
}
}
}
存储二级缓存对象的时候是放到了TransactionalCache.entriesToAddOnCommit
这个map
中,但是每次查询的时候是直接从TransactionalCache.delegate
中去查询的,所以这个二级缓存查询数据库后,设置缓存值是没有立刻生效的,主要是因为直接存到 delegate
会导致脏数据问题。
那我们来看下SqlSession.commit()
方法做了什么。
// SqlSession.commit()
@Override
public void commit(boolean force) {
try {
// 主要是这句
executor.commit(isCommitOrRollbackRequired(force));
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error committing transaction.Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
// CachingExecutor.commit()
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
// 在这里
tcm.commit();
}
// TransactionalCacheManager.commit()
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();// 在这里
}
}
// TransactionalCache.commit()
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
//这一句
flushPendingEntries();
reset();
}
// TransactionalCache.flushPendingEntries()
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
// 在这里真正的将entriesToAddOnCommit的对象逐个添加到delegate中,只有这时,二级缓存才真正的生效
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
我们来看看SqlSession
的更新操作:
public int update(String statement, Object parameter) {
int var4;
try {
this.dirty = true;
MappedStatement ms = this.configuration.getMappedStatement(statement);
var4 = this.executor.update(ms, this.wrapCollection(parameter));
} catch (Exception var8) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + var8, var8);
} finally {
ErrorContext.instance().reset();
}
return var4;
}
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
this.flushCacheIfRequired(ms);
return this.delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
//获取MappedStatement对应的Cache,进行清空
Cache cache = ms.getCache();
//SQL需设置flushCache="true" 才会执行清空
if (cache != null && ms.isFlushCacheRequired()) {
this.tcm.clear(cache);
}
}
MyBatis
二级缓存只适用于不常进行增、删、改的数据,比如国家行政区省市区街道数据。一但数据变更,MyBatis
会清空缓存。因此二级缓存不适用于经常进行更新的数据。
二级缓存的总结
在二级缓存的设计上,MyBatis
大量地运用了装饰者模式,如CachingExecutor
, 以及各种Cache
接口的装饰器。
- 二级缓存实现了
Sqlsession
之间的缓存数据共享,属于namespace
级别。 - 二级缓存具有丰富的缓存策略。
- 二级缓存可由多个装饰器,与基础缓存组合而成。
- 二级缓存工作由 一个缓存装饰执行器
CachingExecutor
和 一个事务型预缓存TransactionalCache
完成。
文章评论