为什么使用缓存

  • 减少和数据库交互次数,提高执行效率

mybatis的缓存

  • mybatis一级缓存,也就是局部的sqlSession级别的缓存,默认是开启的
  • 每一个 session 会话都会有各自的缓存,这缓存是局部的,也就是所谓的一级缓存
  • mybatis二级缓存,是sqlSessionFactory级别的缓存,不同的sqlSession可以获取到同样SQL的缓存结果,在mybatis3中也是默认开启的,但是需要配置指定的接口或方法进行缓存

关闭一级缓存

xml配置:

<settings>
<!-- localCacheScope是本地缓存(一级缓存)的作用域,只有两种取值:SESSION和STATEMENT,取STATEMENT意味着关闭一级缓存-->
<setting name="localCacheScope" value="STATEMENT"/>
</settings>

yml配置:

mybatis-plus:
configuration:
local-cache-scope: STATEMENT
@Test
@Transactional
public void test_cache_one() {
userService.getUserCacheOne(1);
userService.getUserCacheOne(1);
userService.getUserCacheOne(1);
}

开启一级缓存(默认就是session)

xml配置:

<settings>
<!-- localCacheScope是本地缓存(一级缓存)的作用域,只有两种取值:SESSION和STATEMENT,取STATEMENT意味着关闭一级缓存-->
<setting name="localCacheScope" value="session"/>
</settings>

yml配置:

mybatis-plus:
configuration:
local-cache-scope: session
@Test
@Transactional
public void test_cache_one() {
userService.getUserCacheOne(1);
userService.getUserCacheOne(1);
userService.getUserCacheOne(1);
}

  • 从以上截图中可明显看到,默认开启一级缓存,myabits只跟数据库交互了一次

源码分析均在myabtis整合spring

1. 创建SqlSessionTemplate对象

com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration

	@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
if (executorType != null) {
return new SqlSessionTemplate(sqlSessionFactory, executorType);
} else {
// 创建sqlSession
return new SqlSessionTemplate(sqlSessionFactory);
}
}
  • new SqlSessionTemplate(sqlSessionFactory)创建sqlSession代理对象
  • 默认使用protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;

2. 创建sqlSessionProxy代理对象

public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) { notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required"); this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
this.sqlSessionProxy = (SqlSession)
// 创建sqlsession的动态代理,代理对象是SqlSessionInterceptor方法
newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// getSqlSession是个关键方法,决定是重新拿个新sqlSession还是复用
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
...
  • getSqlSession()方法是个关键方法,决定是创建一个新的sqlSession还是复用旧的sqlSession

3. 创建或复用sqlSession对象

 public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) { notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED); SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
// 判断是否在事务中,如果在事务中,就共同同一个session
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
LOGGER.debug(() -> "Creating a new SqlSession");
// 如果不是同一个事务中,则创建一个新的sqlsession,打印的日志很明显
session = sessionFactory.openSession(executorType);
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
  • 如果在同一个事务中,则使用旧的sqlSession对象
  • 如果不在同一个事务中,则重新通过session = sessionFactory.openSession(executorType);创建新的sqlSession对象
  • 如果不在同一个事务下,那么每次查询都会创建一个新的sqlSession,那就看不到一级缓存的效果了,所以想要看效果,需要开启事务,保证几次查询用的是同一个sqlSession
  • LOGGER.debug(() -> "Creating a new SqlSession");创建新的sqlSession对象这个日志很明显

4. 创建sqlSession()对象

session = sessionFactory.openSession(executorType);

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 配置数据库环境
final Environment environment = configuration.getEnvironment();
// 配置事务管理器
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 选择执行器,默认使用的是simple
final Executor executor = configuration.newExecutor(tx, execType);
// 返回使用DefaultSqlSession实现类
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
  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 {
// 默认使用的就是executorType = ExecutorType.SIMPLE
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
  • 这里的cacheEnabled就是二级缓存的开关,myabtis3默认是开启的 protected boolean cacheEnabled = true;

  • newExecutor()方法中不难看出,executor使用SimpleExecutor处理器,然后放入到CachingExecutor,二级缓存被称为SqlSessionFactory级别的缓存,它是可以缓存不同sqlSession会话中相同的sql语句

  • 创建sqlSession对象就此结束,myabtis的初始化也就到这里,接下来就是使用mapper层接口代理对象了

5. 创建MapperFactoryBean对象

MapperFactoryBean是一个Bean对象,继承了FactoryBean,在spring中通过getObject()获取对应接口方法的代理对象

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
@Override
public T getObject() throws Exception {
// 这个的getSession方法返回的就是上面产生的那个sqlSession
return getSqlSession().getMapper(this.mapperInterface);
}
...
}
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 创建mapperProxyFactory动态代理,这里的产生的代理对象主要是用来执行sql的
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
  • 生成代理对象,代理对象为MapperProxy
protected T newInstance(MapperProxy<T> mapperProxy) {
// 生成代理对象,代理对象为MapperProxy
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
} public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}

6. 执行代理方法

  @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
// 执行代理方法
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
  private static class PlainMethodInvoker implements MapperMethodInvoker {
private final MapperMethod mapperMethod; public PlainMethodInvoker(MapperMethod mapperMethod) {
super();
this.mapperMethod = mapperMethod;
} @Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
// 这里就是执行sql地方了
return mapperMethod.execute(sqlSession, args);
}
}

然后随便找个select语句一直往下,找到sqlSession提供的select方法

private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
  • 由于在创建openSessiond地方,创建Executor对象的时候,由于二级缓存是默认开启的,所以这里的executor.query()是先会走到CachingExecutor中的

7. 二级缓存判断

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 判断这个sqlStatement是否开启了二级缓存
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
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;
}
}
// 如果发现没有使用到二级缓存,则才会使用simple处理器去执行sql
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
  • 判断这个sql是否开启了二级缓存,这个缓存是可以在不同的sqlSession共享数据的,按照这个顺序应该是先走的二级缓存,然后再走到一级缓存,二级缓存的优先级更高
  • 如果有二级缓存,则使用缓存数据,缓存的key跟一级缓存的key一致
  • 如果没有二级缓存,则会使用默认的simpleExecutor处理器去执行sql语句
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
// 找个key就是一级缓存的key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

MyBatis 的一级缓存的key是由以下三个部分组成的:

  1. 执行的SQL 语句(包括语句的类型、命名空间以及具体的 SQL 语句内容)。
  2. SQL 语句中的参数(参数的值)。
  3. 数据库连接的标识(Connection Id)。

key的生成策略:id + offset + limit + sql + param value + environment id,这些值都相同,生成的key就相同。

这三个部分共同构成了一级缓存的 key,用于唯一标识一个查询操作。只有当这三个部分完全匹配时,才会从缓存中获取相应的结果。如果其中任何一个部分不匹配,就会重新执行 SQL 查询,并将结果存入缓存。

8. 一级缓存

@Override
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.");
}
// ms.isFlushCacheRequired() 如果不是select语句,则会清空缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
// 清空一级缓存
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 根据key去查询缓存
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();
}
deferredLoads.clear();
// 这里就是配置是否启动一级缓存的地方
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// 清空一级缓存
clearLocalCache();
}
}
return list;
}
  • ms.isFlushCacheRequired()判断是否为select语句,如果不是则会清理一级缓存的内容
  • clearLocalCache();清空一级缓存
  • localCache.getObject(key)根据组装的key去查询缓存是否存在
  • queryFromDatabase()如果缓存不存在,则去查询数据
  • configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT这里也是配置一级缓存的开关,默认是LocalCacheScope.SESSION开启的,如果设置成LocalCacheScope.STATEMENT则会关闭一级缓存
 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 {
// 与数据库交互,组装sql参数
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 删除缓存
localCache.removeObject(key);
}
// 添加一级缓存,list就是返回的数据
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
// 用于缓存存储过程输出参数的缓存对象
localOutputParameterCache.putObject(key, parameter);
}
return list;
}

mybatis缓存源码解析的更多相关文章

  1. mybatis缓存源码分析之浅谈缓存设计

    本文是关于mybatis缓存模块设计的读后感,关于缓存的思考,关于mybatis的缓存源码详细分析在另一篇文章:https://www.cnblogs.com/gmt-hao/p/12448896.h ...

  2. MyBatis详细源码解析(上篇)

    前言 我会一步一步带你剖析MyBatis这个经典的半ORM框架的源码! 我是使用Spring Boot + MyBatis的方式进行测试,但并未进行整合,还是使用最原始的方式. 项目结构 导入依赖: ...

  3. [原创]Laravel 的缓存源码解析

    目录 前言 使用 源码 Cache Facade CacheManager Repository Store 前言 Laravel 支持多种缓存系统, 并提供了统一的api接口. (Laravel 5 ...

  4. MyBatis 3源码解析(一)

    一.SqlSessionFactory 对象初始化 //加载全局配置文件 String resource = "mybatis-config.xml"; InputStream i ...

  5. Mybatis SqlSessionTemplate 源码解析

    As you may already know, to use MyBatis with Spring you need at least an SqlSessionFactory and at le ...

  6. MyBatis 3源码解析(四)

    四.MyBatis 查询实现 Employee empById = mapper.getEmpById(1); 首先会调用MapperProxy的invoke方法 @Override public O ...

  7. Mybatis SqlNode源码解析

    1.ForEachSqlNode mybatis的foreach标签可以将列表.数组中的元素拼接起来,中间可以指定分隔符separator <select id="getByUserI ...

  8. MyBatis 3源码解析(二)

    二.获取SqlSession对象 1.首先调用DefaultSqlSessionFactory 的 openSession 方法,代码如下: @Override public SqlSession o ...

  9. MyBatis 3源码解析(三)

    三.getMapper获取接口的代理对象 1.先调用DefaultSqlSession的getMapper方法.代码如下: @Override public <T> T getMapper ...

  10. myBatis源码解析-二级缓存的实现方式

    1. 前言 前面近一个月去写自己的mybatis框架了,对mybatis源码分析止步不前,此文继续前面的文章.开始分析mybatis一,二级缓存的实现.附上自己的项目github地址:https:// ...

随机推荐

  1. c++基础之语句

    上一次总结了一下c++中表达式的相关内容,这篇博文主要总结语句的基础内容 简单语句 c++ 中语句主要是以分号作为结束符的,最简单的语句是一个空语句,空语句主要用于,语法上需要某个地方,但是逻辑上不需 ...

  2. C/C++ 通过HTTP实现文件上传下载

    WinInet(Windows Internet)是 Microsoft Windows 操作系统中的一个 API 集,用于提供对 Internet 相关功能的支持.它包括了一系列的函数,使得 Win ...

  3. 外部文件使用django的models

    #外部文件使用django的models,需要配置django环境 import os if __name__ == '__main__': os.environ.setdefault("D ...

  4. Linux下开发基于.NET的三维绘图程序

    很多人可能知道使用.NET Core可以开发跨平台(包括Windows,Linux.MacOS)的App,但知道在Linux下使用.NET Core可以开发三维程序的恐怕就很少了.本文通过借助.NET ...

  5. Linux系统NTP校时的微调模式

    前言: Linux系统有两个时间同步服务:ntpd和chrony,一般较低版本的系统使用ntpd,新版本系统使用chrony. ntpd有两种校时策略slew和step: slew是平滑.缓慢的渐进式 ...

  6. Python-统计执行时间

    方法一:datetime.datetime.now() import datetime import time starttime = datetime.datetime.now() print(st ...

  7. UUID算法:独一无二的标识符解决方案

    引言 在分布式系统和大数据环境下,唯一标识符的生成和管理是一项关键任务.UUID(Universally Unique Identifier)算法应运而生,成为了解决重复数据和标识符冲突的有效工具.本 ...

  8. 玩转 CMS

    玩转 CMS 目前接手的内容管理系统(CMS)基于 ant-design-vue-pro(简称模板项目或ant-vue-pro) 开发的,经过许多次迭代,形成了现在的模样(简称本地项目). 假如让一名 ...

  9. 贝壳云P1刷机记录(5.10内核Armbian)

    说明 贝壳云基于瑞芯微的RK3328芯片, 芯片介绍, Cortex-A53架构, 4核, 1G内存, 8G eMMC. 板载1个千兆网口, 4个USB3.0. 这个盒子比较赞的地方就是不到百元的价格 ...

  10. 【Unity3D】UGUI回调函数

    1 简述 ​ UGUI 回调函数主要指鼠标进入.离开.点下.点击中.抬起.开始拖拽.拖拽中.拖拽结束 UI 控件触发的回调.使用 UGUI 回调函数时,需要引入 UnityEngine.EventSy ...