缓存,相信大家应该不陌生,在工作中,我们也接触过一些缓存中间件,比如Redis等。缓存的作用就是为了提供查询的效率,减少访问数据库的次数,从而提供性能。同样的,Mybatis里面也提供了缓存功能,包括一级缓存和二级缓存。本篇文章我们将总结Mybatis的一级缓存、二级缓存怎么用的以及分析它们的作用域、实现原理等。
MyBatis中的缓存相关类都在cache包下面,而且定义了一个顶级接口Cache,Cache定义了缓存的基本操作,比如设置缓存,获取缓存的方法。Cache类的源码如下:
public interface Cache {
/**
* 缓存的唯一标识
*/
String getId();
/**
* 设置缓存,key-value键值对方式
*/
void putObject(Object key, Object value);
/**
* 根据key获取对应的缓存
*/
Object getObject(Object key);
/**
* 根据key移除对应的缓存
*/
Object removeObject(Object key);
/**
* 清空所有的缓存
*/
void clear();
/**
* 缓存中元素总数
*/
int getSize();
/**
* 读写锁
*/
ReadWriteLock getReadWriteLock();
}
Cache类的实现子类主要有PerpetualCache、BlockingCache、LruCache、SerializedCache、FifoCache等等。这里我们介绍一种比较简单的缓存类,同时也是默认的缓存实现子类PerpetualCache:
PerpetualCache里面持有一个HashMap结构的cache成员属性,用于存放缓存对应的数据,key-value形式存储,对缓存的操作其实就是对cache hashmap的操作。
public class PerpetualCache implements Cache {
private final String id;
// 以map存储缓存
private final Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return cache.size();
}
//存放缓存,实际上就是往map里面放入一条数据
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
@Override
public boolean equals(Object o) {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
if (this == o) {
return true;
}
if (!(o instanceof Cache)) {
return false;
}
Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}
@Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
return getId().hashCode();
}
}
首先先通过一个示例来看看一级缓存到底是个什么东西。
一级缓存也叫本地缓存,在MyBatis中,一级缓存是SqlSession级别的缓存,在同一个会话中,如果执行两次相同的sql,第一次会执行查询打印sql,第二次则是直接从一级缓存中获取,不会从数据库查询,所以不会打印sql。
@SpringBootTest
class MybatisLocalCacheDemoApplicationTests {
public static void main(String[] args) {
//1、读取配置文件
String resource = "mybatis-config.xml";
InputStream inputStream;
SqlSession sqlSession = null;
try {
inputStream = Resources.getResourceAsStream(resource);
//2、初始化mybatis,创建SqlSessionFactory类实例
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
System.out.println(sqlSessionFactory);
//3、创建Session实例
sqlSession = sqlSessionFactory.openSession();
//4、获取Mapper接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//5、执行SQL操作
//在同一个会话中,执行两次相同的SQL
User user = userMapper.getById(1L);
User user2 = userMapper.getById(1L);
System.out.println(user);
System.out.println(user2);
} catch (IOException e) {
e.printStackTrace();
} finally {
//6、关闭sqlSession会话
if (null != sqlSession) {
sqlSession.close();
}
}
}
}
启动单元测试,观察后台日志:
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@eb21112]
==> Preparing: select * from user where id = ?
==> Parameters: 1(Long)
<== Columns: id, username
<== Row: 1, 张三
<== Total: 1
User{id='1', username='张三'}
User{id='1', username='张三'}
可以看到,控制台只输出了一条SQL语句,说明只有第一次查询了数据库,然后第二次查询是从一级缓存中获取的,不会发送SQL。
如果不同的SqlSession级别的执行两条相同的sql,是否只会发送一条SQL?稍微修改一下代码:
@SpringBootTest
class MybatisLocalCacheDemoApplicationTests {
public static void main(String[] args) {
//1、读取配置文件
String resource = "mybatis-config.xml";
InputStream inputStream;
SqlSession sqlSession = null;
SqlSession sqlSession2 = null;
try {
inputStream = Resources.getResourceAsStream(resource);
//2、初始化mybatis,创建SqlSessionFactory类实例
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
System.out.println(sqlSessionFactory);
//3、创建Session实例
/*********************第一个会话***********************/
sqlSession = sqlSessionFactory.openSession();
//4、获取Mapper接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//5、执行SQL操作
User user = userMapper.getById(1L);
System.out.println(user);
/*********************第二个会话***********************/
sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = userMapper2.getById(1L);
System.out.println(user2);
} catch (IOException e) {
e.printStackTrace();
} finally {
//6、关闭sqlSession会话
if (null != sqlSession) {
sqlSession.close();
}
}
}
}
后台日志输入如下:
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@eb21112]
==> Preparing: select * from user where id = ?
==> Parameters: 1(Long)
<== Columns: id, username
<== Row: 1, 张三
<== Total: 1
User{id='1', username='张三'}
Opening JDBC Connection
Created connection 1381128261.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@52525845]
==> Preparing: select * from user where id = ?
==> Parameters: 1(Long)
<== Columns: id, username
<== Row: 1, 张三
<== Total: 1
User{id='1', username='张三'}
我们看到,控制台输出了两次SQL,说明Mybatis的一级缓存是SqlSession会话级别的,如果在两个不同的会话执行相同的两条SQL,同样会查询数据库两次,这一点需要注意一下。
前面已经介绍了Mybatis的一级缓存怎么使用的,默认一级缓存是开启的,无需手动开启,并且一级缓存的作用范围是SqlSession级别的,不同的SqlSession不能共享一级缓存。
如果要关闭一级缓存的功能,我们可以在mybatis-config.xml中的settings标签中将这个配置设置成Statement类型的:localCacheScope默认是SESSION的。
//有两种取值:SESSION和STATEMENT,默认是SESSION
//Configuration类成员属性: protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
<setting name="localCacheScope" value="STATEMENT"/>
如果某个select标签查询不需要缓存,在select标签加上flushCache="true" 也可以设置单个查询关闭缓存。
<!--flushCache="true": 强制刷新缓存,每次都从数据库查询 -->
<select id="getById" flushCache="true" resultType="com.wsh.mybatis.mybatisdemo.entity.User">
select * from user where id = #{id}
</select>
Mybatis的缓存是在执行器Executor中进行维护的。
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory.getLog(BaseExecutor.class);
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
//localCache就是一级缓存,类型是PerpetualCache,里面就是用的一个map保存的缓存
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
protected int queryStack;
private boolean closed;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
//在构造方法中初始化localCache,构建了一个PerpetualCache,默认缓存的唯一标识就是LocalCache
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
}
首先,我们可以猜一下一级缓存存放在哪里比较合适?SqlSession?Executor?Configuration?还是其他?
因为一级缓存是SqlSession会话级别的,自然存放在SqlSession最合适不过了,来看看SqlSession的唯一实现类DefaultSqlSession:
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
private final boolean autoCommit;
private boolean dirty;
private List<Cursor<?>> cursorList;
//...........
}
DefaultSqlSession中只有5个成员属性:
这三个看名字都能猜到,不可能用来存储缓存。
那么就只剩下Executor最有可能,然后联想到,sqlSession执行查询的时候,实际上都是交给executor去执行的,所以Executor比较合适用来存储缓存。
那么我们看看Executor的实现类BaseExecutor:
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory.getLog(BaseExecutor.class);
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
protected int queryStack;
private boolean closed;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
//............
}
看名字,本地缓存,没错,它就是用来存放一级缓存的,PerpetualCache内缓存是用一个HashMap来存储缓存。
首先我们先看看缓存的key是如何生成的:
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//拼接缓存Key
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
总结一下CacheKey的组成,CacheKey主要是由以下6部分组成
接下来就需要看看BaseExecutor的query方法是如何使用一级缓存的。
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//select标签是否配置了flushCache=true
//如果需要强制刷新的话,这里会清空一级缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
//通过localCache.getObject(key)去一级缓存中查询有没有对应的缓存
//有的话直接取,没有的话执行queryFromDatabase数据库查询
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);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// 如果关闭了一级缓存,查询完后清除一级缓存
clearLocalCache();
}
}
return list;
}
Debug一下源码,第一次执行getById的时候,很显然缓存中肯定没有对应的数据,所以第一次会从数据库查询。如下图:
第二次执行getById:第二次查询的时候,因为第一次从数据库查询出结果之后,会将结果存入一级缓存,所以这里判断一级缓存中存在对应的数据,直接从缓存中取出,并返回。
下面我们说一下一级缓存的清除,在执行update,commit,或者rollback操作的时候都会进行清除缓存操作,所有的缓存都将失效。例如update方法:
//org.apache.ibatis.executor.BaseExecutor#update
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//清空一级缓存
clearLocalCache();
//清空一级缓存,再执行更新操作
return doUpdate(ms, parameter);
}
可以看到clearLocalCache()方法就用清空本地缓存的意思。
一级缓存因为只能在同一个SqlSession中共享,所以会存在一个问题,在分布式或者多线程的环境下,不同会话之间对于相同的数据可能会产生不同的结果,因为跨会话修改了数据是不能互相感知的,所以就有可能存在脏数据的问题,正因为一级缓存存在这种不足,所以我们需要一种作用域更大的缓存,这就是二级缓存。
同样,猜一下二级缓存应该存储在哪里合适?
在MyBatis中,二级缓存其实是用了一个装饰器来维护,就是CachingExecutor,后面详细介绍。
接下来我们通过一个例子来验证一下二级缓存的使用。
第一步,首先我们需要手动开启二级缓存,开启方法就是在mybatis-config.xml加入如下设置:
<setting name="cacheEnabled" value="true"/>
第二步,在mapper.xml中加入二级缓存的配置:
<!--flushCache="true": 强制刷新缓存,每次都从数据库查询
useCache="true" : 开启二级缓存
-->
<select id="getById" useCache="true" resultType="com.wsh.mybatis.mybatisdemo.entity.User">
select * from user where id = #{id}
</select>
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024"
eviction="LRU"
flushInterval="120000"
readOnly="false"/>
第三步:编写单元测试,验证不同SqlSession会话间执行两条相同的SQL,是否能够共享缓存。
@SpringBootTest
class MybatisSecondCacheDemoApplicationTests {
public static void main(String[] args) {
//1、读取配置文件
String resource = "mybatis-config.xml";
InputStream inputStream;
SqlSession sqlSession = null;
SqlSession sqlSession2 = null;
try {
inputStream = Resources.getResourceAsStream(resource);
//2、初始化mybatis,创建SqlSessionFactory类实例
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
System.out.println(sqlSessionFactory);
//3、创建Session实例
/*********************第一个会话***********************/
sqlSession = sqlSessionFactory.openSession();
//4、获取Mapper接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//5、执行SQL操作
User user = userMapper.getById(1L);
System.out.println(user);
sqlSession.close();
/********************第二个会话**********************/
sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = userMapper2.getById(1L);
System.out.println(user2);
sqlSession2.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
观察后台日志:
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@704921a5]
==> Preparing: select * from user where id = ?
==> Parameters: 1(Long)
<== Columns: id, username
<== Row: 1, 张三
<== Total: 1
User{id='1', username='张三'}
Cache Hit Ratio [com.wsh.mybatis.mybatisdemo.mapper.UserMapper]: 0.0
Opening JDBC Connection
Created connection 1270144618.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@4bb4de6a]
==> Preparing: select * from user where id = ?
==> Parameters: 1(Long)
<== Columns: id, username
<== Row: 1, 张三
<== Total: 1
User{id='1', username='张三'}
我们看到,还是打印了2次sql,说明缓存没生效,配置也都配置正确了,那会是什么问题呢?
造成这个的原因其实是因为Mybatis的二级缓存存储的时候,是先保存在临时属性中,等事务提交的时候再保存到真实的二级缓存。
我们知道Mybatis的缓存管理都是由Executor来进行管理,要看缓存相关代码,肯定也是在Executor执行器中。看一下CachingExecutor类的query方法:
//org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//根据key从二级缓存中查找对应的数据
List<E> list = (List<E>) tcm.getObject(cache, key);
//如果二级缓存中没有找到对应的数据
if (list == null) {
//执行普通逻辑,从一级缓存中进行查找,找不到再从数据库直接查询
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//将查询出来的结果放入二级缓存中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
再看看CachingExecutor的commit方法,在commit的时候才会将缓存放到真正的缓存中,这样做的目的就是为了防止不同SqlSession间的脏读,一个SqlSession读取了另一个SqlSession还未提交的数据。
public void commit(boolean required) throws SQLException {
delegate.commit(required);
//TransactionalCacheManager tcm = new TransactionalCacheManager()
//真正提交二级缓存,会将临时中间变量entriesToAddOnCommit的值刷新到二级缓存中
tcm.commit();
}
所以很显然,我们的单元测试少了sqlSession.commit();提交这一步骤,下面我们将它加上:
@SpringBootTest
class MybatisSecondCacheDemoApplicationTests {
public static void main(String[] args) {
//1、读取配置文件
String resource = "mybatis-config.xml";
InputStream inputStream;
SqlSession sqlSession = null;
SqlSession sqlSession2 = null;
try {
inputStream = Resources.getResourceAsStream(resource);
//2、初始化mybatis,创建SqlSessionFactory类实例
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
System.out.println(sqlSessionFactory);
//3、创建Session实例
/*********************第一个会话***********************/
sqlSession = sqlSessionFactory.openSession();
//4、获取Mapper接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//5、执行SQL操作
User user = userMapper.getById(1L);
System.out.println(user);
sqlSession.commit();
sqlSession.close();
/********************第二个会话**********************/
sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = userMapper2.getById(1L);
System.out.println(user2);
sqlSession2.commit();
sqlSession2.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
重新启动单元测试,观察后台日志:
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@704921a5]
==> Preparing: select * from user where id = ?
==> Parameters: 1(Long)
<== Columns: id, username
<== Row: 1, 张三
<== Total: 1
User{id='1', username='张三'}
Cache Hit Ratio [com.wsh.mybatis.mybatisdemo.mapper.UserMapper]: 0.5
User{id='1', username='张三'}
可以看到,这次只输出了一次SQL,说明第二次查询是从二级缓存中获取的,第一次查询提交了事务后,第二次直接命中了缓存,也印证了事务提交才会将查询结果放到缓存中。
前面已经介绍了Mybatis的二级缓存怎么使用的,我们都知道一级缓存的作用范围是SqlSession级别的,但是SqlSession是单线程的,不同线程间的操作会有一些脏数据的问题。二级缓存的范围更大,是Mapper级别的缓存,因此不同sqlSession间可以共享缓存。
Mybatis的二级缓存默认是开启的,可以在全局配置文件中进行配置:
<setting name="cacheEnabled" value="true"/>
cacheEnabled默认值就是true,下图是从Mybatis官网看到的:
注意:如果cacheEnabled配成false,其余各个Mapper XML文件配成支持cache也没用。
并且在需要进行开启二级缓存的mapper中新增cache配置,cache配置有很多属性:
<!--
type : 缓存实现类,默认是PerpetualCache,也可以是第三方缓存的实现,比如ehcache, oscache 等等;
size:最多缓存对象的个数;
eviction:缓存回收策略,默认是LRU最近最少使用策略;
LRU:最近最少使用策略,回收最长时间不被使用的缓存;
FIFO:先进先出策略,回收最新进入的缓存;
SOFT - 软引用:移除基于垃圾回收器状态和软引用规则的对象;
WEAK - 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象;
flushInterval:缓存刷新的间隔时间,默认是不刷新的;
readOnly : 是否只读,true 只会进行读取操作,修改操作交由用户处理; false 可以进行读取操作,也可以进行修改操作;
-->
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024"
eviction="LRU"
flushInterval="120000"
readOnly="false"/>
二级缓存是Mapper接口级别的缓存,在前面几篇文章介绍Executor的时候,细心的小伙伴肯定会发现,在创建Executor的时候,如果开启了二级缓存,Mybatis使用了装饰者模式对executor进行包装成CachingExecutor。具体源码如下:
//org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
//如果开启了二级缓存,Mybatis使用了装饰者模式对executor进行包装成CachingExecutor
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
点开CachingExecutor 类的源码,可以看到CachingExecutor 中只有两个成员变量,其中一个就是TransactionalCacheManager用来管理缓存,还有一个delegate则是被包装的执行器。
//被包装的执行器
private final Executor delegate;
//缓存管理类,用来管理TransactionalCache
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
继续看一下缓存管理类TransactionalCacheManager的代码:
public class TransactionalCacheManager {
//内部维护着一个HashMap缓存,其中TransactionalCache实现了Cache接口
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
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) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
}
TransactionalCache类的代码也比较简单,主要属性如下:
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
//二级缓存对象
private final Cache delegate;
//ture表示提交事务时,清空缓存
private boolean clearOnCommit;
//存放临时缓存,当commit的时候,才会加到二级缓存中
//为了解决不同sqlSession之间可能会产生脏读现象
private final Map<Object, Object> entriesToAddOnCommit;
//存放没有命中缓存的key值
private final Set<Object> entriesMissedInCache;
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<>();
this.entriesMissedInCache = new HashSet<>();
}
//..........
}
为什么二级缓存需要先放到一个临时中间变量entriesToAddOnCommit?
原因其实是为了解决不同sqlSession之间可能会产生脏读现象,假如有两个线程,线程A在一个事务中修改了数据,然后另外一个线程此时去查询数据,直接放入缓存,那么假如线程A此时回滚了,那么其实缓存中的数据就是不正确的,所以Mybatis采用了先临时存储一下来解决脏读。
接下来看看Mybatis的二级缓存是如何工作的。来看CachingExecutor的query方法:
// org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//获取二级缓存配置标签
Cache cache = ms.getCache();
if (cache != null) {
//select标签是否配置了flushCache属性
flushCacheIfRequired(ms);
//如果select标签配置了useCache属性
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//根据cacheKey获取二级缓存
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//如果二级缓存为空,执行普通查询,再去查询一级缓存,如果一级缓存也没命中,则查询数据库放到缓存中
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//二级缓存先存放在一个中间变量中,等事务提交的时候,再真正保存到二级缓存
tcm.putObject(cache, key, list); // issue #578 and #116
}
//二级缓存命中,直接返回取出的数据
return list;
}
}
//没开启缓存也是执行普通查询
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
与一级缓存一样,在执行更新、删除等操作时,会清空二级缓存中的数据,如下:
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
//org.apache.ibatis.executor.CachingExecutor#flushCacheIfRequired
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
//清除缓存
tcm.clear(cache);
}
}
最后,我们Debug一下源码,看下二级缓存怎么工作的。
直接在org.apache.ibatis.executor.CachingExecutor#query方法打一个断点。
第一次执行查询:
如上,第一次执行的时候,二级缓存中没有对应的数据,所以会执行普通查询,后续就会进行一级缓存的查询。
在CachingExecutor#commit方法打一个断点,这时候第一次查询提交事务sqlSession.commit(),我们查看此时TransactionalCacheManager中的缓存如下图:
真正提交缓存如下图:
第二次执行:
如上可以看到,当第二次查询时,从二级缓存中获取到了对应的数据,那么就不会执行普通查询了,直接返回缓存中获取的数据。以上就是关于一级缓存和二级缓存的详细使用和工作原理分析。
MyBatis的缓存有两种:一级缓存和二级缓存,一级缓存的作用范围是SqlSession级别的,同一个会话中执行相同的SQL语句只会发送一条SQL查询数据库;而二级缓存是Mapper接口级别的,在不同的会话中执行相同的SQL只会发送一条SQL查询数据库。
二级缓存注意点:
如果不实现序列化接口则会报如下错误【由此也证明了二级缓存实际上就是通过序列化与反序列化实现的】:
MyBatis 中的一级缓存和二级缓存都是默认开启的,不过二级缓存还要额外在mapper和statement中配置缓存属性。一级缓存和二级缓存适用于读多写少的场景,如果频繁的更新数据,将降低查询性能。鉴于笔者水平有限,如果文章有什么错误或者需要补充的,希望小伙伴们指出来,希望这篇文章对大家有帮助。