Mybatis-本地缓存(一级缓存)的使用和原理
①什么是Mybatis-本地缓存
Mybatis 使用到了两种缓存:本地缓存(local cache)和二级缓存(second level cache)。
每当一个新 session 被创建,MyBatis 就会创建一个与之相关联的本地缓存。任何在 session 执行过的查询结果都会被保存在本地缓存中,所以,当再次执行参数相同的相同查询时,就不需要实际查询数据库了。本地缓存将会在做出修改、事务提交或回滚,以及关闭 session 时清空。
默认情况下,本地缓存数据的生命周期等同于整个 session 的周期。由于缓存会被用来解决循环引用问题和加快重复嵌套查询的速度,所以无法将其完全禁用。但是你可以通过设置 localCacheScope=STATEMENT 来只在语句执行时使用缓存。
以上引用自官网:https://mybatis.org/mybatis-3/zh/java-api.html
②如何使用Mybatis-本地缓存
mybatis本地缓存默认是开启的,具体对应 settings 配置下的 localCacheScope 属性,属性值默认为 **SESSION **
属性名 | 描述 | 可以设置的值 | 默认值 |
---|---|---|---|
localCacheScope | MyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。 默认值为 SESSION,会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。 | SESSION /STATEMENT | SESSION |
以上来自官方API: https://mybatis.org/mybatis-3/zh/configuration.html#settings |
③测试本地缓存使用情况
package com.hs.mybatis;
import com.hs.mybatis.bean.Actor;
import com.hs.mybatis.mapper.ActorMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class CacheTest {
public static void main(String[] args) {
String resource = "mybatis-config.xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
ActorMapper actorMapper = session.getMapper(ActorMapper.class);
Actor actor = actorMapper.selectOneActor(1);
System.out.println(actor);
Actor actor2 = actorMapper.selectOneActor(1);
System.out.println(actor2);
session.close();
SqlSession session2 = sqlSessionFactory.openSession();
ActorMapper actorMapper2 = session2.getMapper(ActorMapper.class);
Actor actor3 = actorMapper2.selectOneActor(1);
System.out.println(actor3);
session2.close();
}
}
以上结果中 actor 直接查询数据库(第一次查询),actor2 使用本地缓存中结果(在同一session会话周期内),actor3 查询数据库(上一个会话关闭,新开启了一个会话)
④Mybatis 本地缓存源码分析
1)获取 ActorMapper 代理对象
ActorMapper actorMapper = session.getMapper(ActorMapper.class);
交给org.apache.ibatis.binding.MapperProxyFactory 类创建,如下列出Mapper代理工厂类主要方法:
package org.apache.ibatis.binding;
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap();
protected T newInstance(MapperProxy<T> mapperProxy) {
return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
return this.newInstance(mapperProxy);
}
}
如上通过jdk动态代理方式创建 对应接口的实现类,MapperProxy 实现了InvocationHandler 接口
2)执行 ActorMapper selectOneActor 方法
实际执行入口即为 MapperProxy invoke 方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession);
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
invoke方法具体执行过程交给了 MapperProxy.DefaultMethodInvoker 处理,MapperProxy.DefaultMethodInvoker 类初始化过程如下:
private MapperProxy.MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
MapperProxy.MapperMethodInvoker invoker = (MapperProxy.MapperMethodInvoker)this.methodCache.get(method);
return invoker != null ? invoker : (MapperProxy.MapperMethodInvoker)this.methodCache.computeIfAbsent(method, (m) -> {
if (m.isDefault()) {
try {
return privateLookupInMethod == null ? new MapperProxy.DefaultMethodInvoker(this.getMethodHandleJava8(method)) : new MapperProxy.DefaultMethodInvoker(this.getMethodHandleJava9(method));
} catch (InstantiationException | InvocationTargetException | NoSuchMethodException | IllegalAccessException var4) {
throw new RuntimeException(var4);
}
} else {
return new MapperProxy.PlainMethodInvoker(new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration()));
}
});
} catch (RuntimeException var4) {
Throwable cause = var4.getCause();
throw (Throwable)(cause == null ? var4 : cause);
}
}
真正执行 invoke 的操作为 org.apache.ibatis.binding.MapperProxy.PlainMethodInvoker 类的 invoke 方法
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return this.mapperMethod.execute(sqlSession, args);
}
继续定位:org.apache.ibatis.binding.MapperMethod execute 方法
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
Object param;
switch(this.command.getType()) {
case INSERT:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
break;
case UPDATE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
break;
case DELETE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
break;
case SELECT:
if (this.method.returnsVoid() && this.method.hasResultHandler()) {
this.executeWithResultHandler(sqlSession, args);
result = null;
} else if (this.method.returnsMany()) {
result = this.executeForMany(sqlSession, args);
} else if (this.method.returnsMap()) {
result = this.executeForMap(sqlSession, args);
} else if (this.method.returnsCursor()) {
result = this.executeForCursor(sqlSession, args);
} else {
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);
if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + this.command.getName());
}
if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
} else {
return result;
}
}
可知 执行上述查询操作时调用的是 DefaultSqlSession 类 selectOne 方法,selectOne 方法内部调用 selectList 方法
public <T> T selectOne(String statement, Object parameter) {
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
List var5;
try {
MappedStatement ms = this.configuration.getMappedStatement(statement);
var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception var9) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + var9, var9);
} finally {
ErrorContext.instance().reset();
}
return var5;
}
3)数据查询以及缓存
上述代码执行完成后开始调用 org.apache.ibatis.executor.CachingExecutor query 方法,
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
//根据方法及参数等参数生成 cacheKey,保存数据到缓存时使用的就是这个key
CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
继续调用如下方法,在下面方法中 Cache cache = ms.getCache();
取得是二级缓存,本例中没有配置,所以为null
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) {
this.flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
this.ensureNoOutParams(ms, boundSql);
List<E> list = (List)this.tcm.getObject(cache, key);
if (list == null) {
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
this.tcm.putObject(cache, key, list);
}
return list;
}
}
return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
继续定位 this.delegate.query
方法,此处 delegate 即为 SimpleExecutor ,SimpleExecutor继承自 BaseExecutor,query为其父类BaseExecutor的方法
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 (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
this.clearLocalCache();
}
List list;
try {
++this.queryStack;
//首先从 localCache 本地缓存中取数据
list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
if (list != null) {
this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//没有值的时候直接查数据库
list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
--this.queryStack;
}
if (this.queryStack == 0) {
Iterator var8 = this.deferredLoads.iterator();
while(var8.hasNext()) {
BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
deferredLoad.load();
}
this.deferredLoads.clear();
//localCacheScope 配置为 STATEMENT 时清除本地缓存
if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
this.clearLocalCache();
}
}
return list;
}
}
BaseExecutor queryFromDatabase 方法
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
//往缓存中存入默认值
this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);
List list;
try {
//从数据库查询数据
list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
//移除缓存中的默认值
this.localCache.removeObject(key);
}
//将查询到的结果存入缓存
this.localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
this.localOutputParameterCache.putObject(key, parameter);
}
return list;
}
以上是Mybatis 本地缓存 存取过程的简单理解,通过 如下代码块可以很清楚的知道为什么localCacheScope 配置了 STATEMENT 值后mybatis不会使用本地缓存,因为,这里判断localCacheScope 属性值为STATEMENT 后清除了本地缓存
if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
this.clearLocalCache();
}
同理,为何 session.close 方法执行完后本地缓存也失效了,原因也是close方法内部调用了clearLocalCache方法
org.apache.ibatis.executor.BaseExecutor close 方法:
public void close(boolean forceRollback) {
try {
try {
this.rollback(forceRollback);
} finally {
if (this.transaction != null) {
this.transaction.close();
}
}
} catch (SQLException var11) {
log.warn("Unexpected exception on closing transaction. Cause: " + var11);
} finally {
this.transaction = null;
this.deferredLoads = null;
this.localCache = null;
this.localOutputParameterCache = null;
this.closed = true;
}
}
org.apache.ibatis.executor.BaseExecutor rollback 方法:
public void rollback(boolean required) throws SQLException {
if (!this.closed) {
try {
//清除本地缓存
this.clearLocalCache();
this.flushStatements(true);
} finally {
if (required) {
this.transaction.rollback();
}
}
}
}
⑤ Mybatis 本地缓存本质
Mybatis 本地缓存类为 org.apache.ibatis.cache.impl.PerpetualCache,具体如下,内部使用HashMap保存数据
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.apache.ibatis.cache.impl;
import java.util.HashMap;
import java.util.Map;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap();
public PerpetualCache(String id) {
this.id = id;
}
public String getId() {
return this.id;
}
public int getSize() {
return this.cache.size();
}
public void putObject(Object key, Object value) {
this.cache.put(key, value);
}
public Object getObject(Object key) {
return this.cache.get(key);
}
public Object removeObject(Object key) {
return this.cache.remove(key);
}
public void clear() {
this.cache.clear();
}
public boolean equals(Object o) {
if (this.getId() == null) {
throw new CacheException("Cache instances require an ID.");
} else if (this == o) {
return true;
} else if (!(o instanceof Cache)) {
return false;
} else {
Cache otherCache = (Cache)o;
return this.getId().equals(otherCache.getId());
}
}
public int hashCode() {
if (this.getId() == null) {
throw new CacheException("Cache instances require an ID.");
} else {
return this.getId().hashCode();
}
}
}
本地缓存在初始化 Executor时创建
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;
}