MyBatis缓存机制流程分析

前言

在进行分析之前,建议快速浏览之前写的理解MyBatis原理、思想,这样更容易阅读、理解本篇内容。

验证一级缓存

MyBatis的缓存有两级,一级缓存默认开启,二级缓存需要手动开启。

重复读取跑缓存

可以看到,第二次请求的时候,没有打印SQL,而是使用了缓存。

@Test
public void test1() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    
    SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
    SysRoleMapper mapper1 = sqlSession1.getMapper(SysRoleMapper.class);
    SysRole role = mapper1.getById(2);
    SysRole role2 = mapper1.getById(2);
    System.out.println(role);
    System.out.println(role2);
}

//------------------------------打印SQL--------------------------------------

==>  Preparing: select * from sys_role where role_id = ?
==> Parameters: 2(Integer)
<==    Columns: role_id, role_name, role_key, role_sort, data_scope, status, del_flag, create_by, create_time, update_by, update_time, remark
<==        Row: 2, 测试2, common, 2, 2, 0, 0, admin, 2022-08-29 15:58:05, , null, 普通角色
<==      Total: 1
SysRole{role_id=2, role_name='测试2'}


SysRole{role_id=2, role_name='测试2'}

同一会话的更新操作刷新缓存

通过测试结果可以看到,因为更新操作的原因,两次查询都查了数据库。

@Test
public void test2() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    
    SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
    SysRoleMapper mapper1 = sqlSession1.getMapper(SysRoleMapper.class);
    SysRole role = mapper1.getById(2);
    mapper1.updateRoleNameById("测", 2);
    SysRole role2 = mapper1.getById(2);
    System.out.println(role);
    System.out.println(role2);
}


//------------------------------打印SQL--------------------------------------


==>  Preparing: select * from sys_role where role_id = ?
==> Parameters: 2(Integer)
<==    Columns: role_id, role_name, role_key, role_sort, data_scope, status, del_flag, create_by, create_time, update_by, update_time, remark
<==        Row: 2, 测试2, common, 2, 2, 0, 0, admin, 2022-08-29 15:58:05, , null, 普通角色
<==      Total: 1


==>  Preparing: update sys_role set role_name = ? where role_id = ?
==> Parameters:(String), 2(Integer)
<==    Updates: 1


==>  Preparing: select * from sys_role where role_id = ?
==> Parameters: 2(Integer)
<==    Columns: role_id, role_name, role_key, role_sort, data_scope, status, del_flag, create_by, create_time, update_by, update_time, remark
<==        Row: 2,, common, 2, 2, 0, 0, admin, 2022-08-29 15:58:05, , null, 普通角色
<==      Total: 1


SysRole{role_id=2, role_name='测试2'}
SysRole{role_id=2, role_name='测'}

跨会话更新数据没有刷新缓存

@Test
public void test() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    
    // 会话一
    System.out.println("会话一");
    SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
    SysRoleMapper mapper1 = sqlSession1.getMapper(SysRoleMapper.class);
    SysRole role = mapper1.getById(2);
    System.out.println(role);
    
    // 会话二
    System.out.println("会话二");
    SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
    SysRoleMapper mapper2 = sqlSession2.getMapper(SysRoleMapper.class);
    mapper2.updateRoleNameById("测试2", 2);
    System.out.println(mapper2.getById(2));
    
    // 会话一重新查询
    System.out.println("会话一重新查询");
    role = mapper1.getById(2);
    System.out.println(role);

}


//------------------------------打印结果--------------------------------------


会话一
SysRole{role_id=2, role_name='测试'}
会话二
SysRole{role_id=2, role_name='测试2'}
会话一重新查询
SysRole{role_id=2, role_name='测试'}

源码分析的入口点

我们要阅读、分析源码,就需要先找准一个切入点,我们以下面代码为例子,SysRoleMapper#getById()方法作为调试入口:

@Test
public void test1() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    
    SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
    SysRoleMapper mapper1 = sqlSession1.getMapper(SysRoleMapper.class);
	// 调试入口
    SysRole role = mapper1.getById(2);
    SysRole role2 = mapper1.getById(2);
    System.out.println(role);
    System.out.println(role2);
}

在分析之前,我们就先约定一下:👉符号表示你的视角要焦距在哪几行代码。


一级缓存流程分析

好,现在我们开始分析一级缓存的流程,了解其设计思想,看看能学到什么。

MapperProxy

  • 首先,我们可以看到,通过getMapper方法拿到的对象mapper1,其实是一个代理对象MapperProxy的实例。

image.png

  • MapperProxy实现了InvocationHandler接口,所以SysRoleMapper调用的 方法 都会进入代理对象MapperProxyinvoke方法。
public class MapperProxy<T> implements InvocationHandler, Serializable {
    // 略

    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            // 先拿到声明method方法的类(在这里具体指定是SysRoleMapper)。
            // 如果是 Object 类,则表明调用的是一些通用方法,比如 toString()、hashCode() 等,就直接调用即可。
    👉👉👉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);
        }
    }


    // 略

}

小结:从上面可以知道,我们调用SysRoleMapper接口中的 方法,其实都会进入MapperProxy#invoke方法中。


现在,我们进一步看,由于getById方法不是Object默认的方法,所以会跑else分支,详情分析请看代码:

  @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 {// 跑else分支
        // 整体了解此行代码的流程:
        // 1.首先Method会被包装成MapperMethod;1️⃣
        // 2.MapperMethod被封装到PlainMethodInvoker类内;2️⃣
        // 3.此类(PlainMethodInvoke)提供一个普通的方法invoke,此方法会实际调用MapperMethod的execute方法3️⃣
👉👉👉return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }



private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
      return MapUtil.computeIfAbsent(methodCache, method, m -> {
        // 是否Java语言规范定义的默认方法?否
        if (m.isDefault()) {
            // 这里的细节不要深究了
          try {
            if (privateLookupInMethod == null) {
              return new DefaultMethodInvoker(getMethodHandleJava8(method));
            } else {
              return new DefaultMethodInvoker(getMethodHandleJava9(method));
            }
          } catch (IllegalAccessException | InstantiationException | InvocationTargetException
              | NoSuchMethodException e) {
            throw new RuntimeException(e);
          }
        } else { // 看这里,跑的是else分支
          // 对于普通的方法(如SysRoleMapper#getById),使用的是PlainMethodInvoker实现类。
          // >>    其中,MapperMethod表示对原始的Method方法对象进行了一次包装(细节就先不深究了)
          // >>    mapperInterface 信息在创建MapperProxy对象的时候写入,信息默认来源于我们定义的mybatis-config.xml文件, 包括sqlSession也是。
          return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));//1️⃣️
        }
      });
    } catch (RuntimeException re) {
      Throwable cause = re.getCause();
      throw cause == null ? re : cause;
    }
  }


  private static class PlainMethodInvoker implements MapperMethodInvoker {
    private final MapperMethod mapperMethod;

    public PlainMethodInvoker(MapperMethod mapperMethod) {
      super();
      this.mapperMethod = mapperMethod;//2️⃣
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
      return mapperMethod.execute(sqlSession, args);//3️⃣
    }
  }

从上面注释中,相信你已经了解到MapperProxy#invoke方法下一步会流向哪个类:MapperMethod#execute()

MapperMethod

现在我们看看MapperMethod#execute()做了什么:根据command属性提供的sql方法类型调用sqlSession接口中合适的的处理方法。

public class MapperMethod {

    // 方法对应的sql类型:select、update、delete、insert
    // 在MapperProxy#invoke#cachedInvoker方法中创建MapperMethod类时设置的,感兴趣的可以回看
    private final SqlCommand command; 
    private final MethodSignature method;

    public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
        this.command = new SqlCommand(config, mapperInterface, method);
        this.method = new MethodSignature(config, mapperInterface, method);
    }

    // 这个方法整体做了什么?根据command提供的sql方法类型调用sqlSession接口中合适的的处理方法。
    // >>    我们之前封装MapperMethod的时候,定义了此类的command、method属性;
    // >>    其中command这个属性表示sql方法的类型
👉👉public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;

    	// getById方法是查询语句,所以会进入SELECT分支
        switch (command.getType()) {
            case INSERT: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = rowCountResult(sqlSession.insert(command.getName(), param));
                break;
            }
            case UPDATE: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = rowCountResult(sqlSession.update(command.getName(), param));
                break;
            }
            case DELETE: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = rowCountResult(sqlSession.delete(command.getName(), param));
                break;
            }
            case SELECT:
                if (method.returnsVoid() && method.hasResultHandler()) { // 无返回值,同时有专门的结果处理类
                    executeWithResultHandler(sqlSession, args);
                    result = null;
                } else if (method.returnsMany()) {  // 返回多个结果
                    result = executeForMany(sqlSession, args);
                } else if (method.returnsMap()) { // 返回map类型的结果
                    result = executeForMap(sqlSession, args);
                } else if (method.returnsCursor()) { // 返回结果是数据库游标类型
                    result = executeForCursor(sqlSession, args);
                } else { // 看这里,跑的是else分支:
                    // 获取参数对象,不用关注细节
                    Object param = method.convertArgsToSqlCommandParam(args);
                    // SysRoleMapper#getById结果类型是单个对象,所以最终跑的是这行代码
                👉👉result = sqlSession.selectOne(command.getName(), param);
                    // 下面代码细节不重要,就不展开了
                    if (method.returnsOptional()
                        && (result == null || !method.getReturnType().equals(result.getClass()))) {
                        result = Optional.ofNullable(result);
                    }
                }
                break;
            case FLUSH:
                result = sqlSession.flushStatements();
                break;
            default:
                throw new BindingException("Unknown execution method for: " + command.getName());
        }
        if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
            throw new BindingException("Mapper method '" + command.getName()
                                       + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
        }
        return result;
    }

    // 略

}

好了,看完上面代码,相信你已经知道下一步代码会跑到那里了:SqlSession#selectOne()

SqlSession是一个接口,定义了一些列通用的SQL操作,如selectList、insert、update、commit 和 rollback等操作。

小结:通过上面的分析,我们已经知道,我们调用SysRoleMapper#getById方法本质上其实还是调用SqlSession接口提供的通用SQL操作方法。只不过利用 代理 Mapper接口 的方式,实现方法调用 自动路由到SqlSession接口对应的方法。


SqlSession

通过上面分析,想必你已经知道下一步要走哪了,SqlSession接口默认的实现类是DefaultSqlSession,所以selectOne方法跑的是这个实现类:

public class DefaultSqlSession implements SqlSession {

    // 略

    @Override
    public <T> T selectOne(String statement, Object parameter) {
        // Popular vote was to return null on 0 results and throw exception on too many.(译:大众投票是在 0 个结果上返回 null,并在太多结果上抛出异常。)
        
        // 很明显,selectOne最终跑的是selectList方法
        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;
        }
    }

    // 略


    /**
       * 封装MappedStatement对象,通过executor发起查询。
       * @param statement 映射信息,方法的全路径:cn.lsj.seckill.SysRoleMapper.getById
       * @param parameter SQL参数
       * @param rowBounds 辅助分页,默认不分页。RowBounds(int offset, int limit)
       * @param handler 处理结果回调。查询完成之后调用回调
       * @return
       * @param <E>
       */
    private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
        try {
            // 这个类主要封装了SysRoleMapper相关信息,包括:方法全路径(id)、原始xml文件(resource)、
            // sql语句相关信息(sqlSource)、结果类型映射信息、与映射语句关联的缓存配置信息(cache)等
            MappedStatement ms = configuration.getMappedStatement(statement);
            
            // wrapCollection是懒加载机制的一部分,不用关注细节
    👉👉👉return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }

}

通过上述代码可以知道,selectOne方法内部最终还是依靠Executor接口的query方法去执行具体的sql,只不过在此之前会从Configuration配置类里面通过 映射信息 statement 拿到MappedStatement封装对象,然后传递给query方法。


Executor

在上面,我们了解到下一步走的是Executor接口的query方法,CachingExecutorExecutor接口的实现类,基于装饰者模式Executor功能进行了增强:增加了缓存机制。

public class CachingExecutor implements Executor {

    private final Executor delegate; // 默认被装饰的实现类 SimpleExecutor

    private final TransactionalCacheManager tcm = new TransactionalCacheManager();

    public CachingExecutor(Executor delegate) {
        this.delegate = delegate;
        delegate.setExecutorWrapper(this);
    }

    // 略


    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        // 表示一条 SQL 语句以及相关参数(不用关注细节)
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        // 构造缓存的KEY(不用关注细节)
        CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
        return query(ms, parameterObject, rowBounds, resultHandler, key, 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) {
            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;
            }
        }
        // 通过断点可以看到:默认被装饰的Executor接口实现类是SimpleExecutor (图1️⃣)
        // 由于SimpleExecutor继承了抽象类BaseExecutor 但没有实现query方法,所以,最终指向的还是BaseExecutor#query() (图2️⃣)
👉👉👉return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

    // 略

}

  • 图1️⃣

过断点查看delegate属性可知:默认被装饰的Executor接口实现类是SimpleExecutor

  • 图2️⃣

SimpleExecutor继承了抽象类BaseExecutor但没有实现query方法


通过上面代码注释,我们最终了解到CachingExecutor#query方法跑向的是BaseExecutor#query

现在,我们看一下BaseExecutor类的query方法:


public abstract class BaseExecutor implements Executor {

    // 略
    
    protected PerpetualCache localCache; // 缓存Cache(一级缓存)具体的一个实现类
    
    // 略

    @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.");
        }
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
            clearLocalCache();
        }
        // 很明显,这个是存储查询结果的,我们围绕这个对象来看代码
        List<E> list;
        try {
            queryStack++;
            // 从缓存中读取结果(第一次查询没有缓存)
            list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
            if (list != null) {
                handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            } else { // 跑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) {
                // issue #482
                clearLocalCache();
            }
        }
        return list;
    }

    // 略


    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        List<E> list;
        // 给key对应的缓存值设置一个占位值(只是用于占位)
        localCache.putObject(key, EXECUTION_PLACEHOLDER);
        try {
            // 真正处理查询的方法
            // 抽象类没有实现doQuery方法,所以方法的调用是其实现类 SimpleExecutor#doQuery
    👉👉👉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;
    }
}

StatementHandler

RoutingStatementHandler

现在,我们在看看SimpleExecutor#doQuery方法,没有太多复杂逻辑,直接是交由StatementHandler接口处理了,接口的实现类是RoutingStatementHandler

在划分上,StatementHandler属于Executor的一部分,参与SQL处理:

  • RoutingStatementHandler :根据执行的 SQL 语句的类型(SELECT、UPDATE、DELETE 等)选择不同的 StatementHandler 实现进行处理。
  • PreparedStatementHandler :处理预编译 SQL 语句的实现类。预编译 SQL 语句是指在数据库预先编译 SQL 语句并生成执行计划,然后在后续的执行中,只需要传递参数并执行编译好的执行计划,可以提高 SQL 的执行效率。
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
        Configuration configuration = ms.getConfiguration();
        // 此接口用于处理数据库的 Statement 对象的创建和执行
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        stmt = prepareStatement(handler, ms.getStatementLog());
👉👉👉return handler.query(stmt, resultHandler); // 打断点可以看到handler实现类:RoutingStatementHandler,它作用就是选择合适的StatementHandler实现类执行SQL
    } finally {
        closeStatement(stmt);
    }
}

我们再看看RoutingStatementHandler#query方法,使用了装饰者模式,被装饰类是PreparedStatementHandler
image.png

PreparedStatementHandler

RoutingStatementHandler选择了合适的处理类来执行SQL:PreparedStatementHandler

现在打开看看PreparedStatementHandler#query方法:

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    // Java JDBC 中的一个接口,用于执行预编译的 SQL 语句。使用过JDBC编程的应该见过,可以看文末的JDBC编程Demo回忆回忆。
    PreparedStatement ps = (PreparedStatement) statement;
    // 执行 SQL 语句。
    ps.execute();
    // “结果处理器”会处理并返回查询结果(在这里就不深究了)
    return resultSetHandler.handleResultSets(ps);
  }

现在,让我们往回看BaseExecutor#queryFromDatabase方法:

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 给key对应的缓存值设置一个占位值(只是用于占位)
    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;
  }

到这里,我们经历了一次(第一次)查询的过程,并在BaseExecutor#queryFromDatabase方法中,将查询结果写入到localCache属性中。

我们再查一次,就会发现,在BaseExecutor#query中,这次直接拿到了缓存的数据:

@Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // 略
    List<E> list;
    try {
      queryStack++;
       // 从本地缓存拿到了上次的查询结果
 👉👉👉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--;
    }
    // 略
    return list;
  }

小结

整个流程下来,发现最关键的地方就是BaseExecutor抽象类的queryqueryFromDatabase这两个方法,它们在一级缓存方面,围绕localCache属性做缓存操作。

  • 第一次查询,跑queryFromDatabase方法,并将查询结果写入localCache属性;
  • 第二次相同的查询,直接从localCache属性中读取缓存的查询结果。

二级缓存流程分析

开启二级缓存

添加配置到mybatis-config.xml文件:

<settings>
  <!-- 二级缓存-->
  <setting name="cacheEnabled" value="true"/>
</settings>

修改SysRoleMapper.xml文件:

<?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="cn.lsj.seckill.SysRoleMapper">

	<!-- 表示此namespace开启二级缓存 -->
  <cache/>

  <select id="getById" resultType="cn.lsj.seckill.SysRole" >
    select * from sys_role where role_id = #{id}
  </select>
  
</mapper>

流程分析

当我们开启二级缓存之后,查询过程就变成:二级缓存->一级缓存->数据库

二级缓存的验证代码:


  @Test
  public void test1() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
    SysRoleMapper mapper1 = sqlSession1.getMapper(SysRoleMapper.class);
    SysRole role = mapper1.getById(2);
    System.out.println(role);

    // 提交事务二级缓存数据才生效
    sqlSession1.commit();


    SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
    SysRoleMapper mapper2 = sqlSession2.getMapper(SysRoleMapper.class);
    SysRole role2 = mapper2.getById(2);
    System.out.println(role2);

    System.out.println(mapper1.getById(2));

  }

在前面的CachingExecutor#query方法中,我们看到了二级缓存的代码:

    @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) {
            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;
            }
        }
		return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

总结

看到这里,我们回顾一下,在之前的分析中,我们看到装饰者模式出现得比较频繁;此外还是用到动态代理技术。

整个分析下来,相信你收获的不止这些,源码阅读能力应该能得到一些提升,对设计模式、动态代理的理解也会有一些加深。

好了,如果你感兴趣的话,可以进一步深入分析缓存如何刷新、生效,如何做到缓存会话级别、Mapper级别的隔离的。

最后,留下一些思考问题:

  • 开启二级缓存之后,为什么sqlSession1.commit();之后二级缓存才生效?

附:JDBC编程Demo

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class JDBCDemo {
    public static void main(String[] args) {
        // MySQL服务器的JDBC URL、用户名和密码
        String url = "jdbc:mysql://localhost:3306/你的数据库名";
        String user = "你的用户名";
        String password = "你的密码";

        try {
            // 加载JDBC驱动程序
            Class.forName("com.mysql.cj.jdbc.Driver");

            // 建立数据库连接
            Connection connection = DriverManager.getConnection(url, user, password);

            // 创建SQL语句
            String sql = "SELECT * FROM 你的表名";
            PreparedStatement preparedStatement = connection.prepareStatement(sql);

            // 执行查询
            ResultSet resultSet = preparedStatement.executeQuery();

            // 处理结果集
            while (resultSet.next()) {
                int id = resultSet.getInt("id");
                String name = resultSet.getString("name");
                String email = resultSet.getString("email");

                System.out.println("ID: " + id + ", Name: " + name + ", Email: " + email);
            }

            // 关闭资源
            resultSet.close();
            preparedStatement.close();
            connection.close();

        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/245379.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[MySQL] MySQL中的索引

文章目录 一、初识索引 1、1 索引的概念 1、2 索引案例 二、认识磁盘 2、1 磁盘结构 2、2 操作系统与磁盘的数据交互 2、3 磁盘随机访问与连续访问 2、4 MySQL与磁盘的数据交互 三、索引的理解 3、1 建立测试表 3、2 为何MySQL与磁盘IO交互是 Page 3、3 理解Page 3、3、1 页目录…

【WSL】Windows下的Linux子系统使用方法指南

▒ 目录 ▒ &#x1f6eb; 导读需求开发环境 1️⃣ WSL安装启用或关闭windows功能安装分发命令行启动Linux 2️⃣ WSL 的基本命令显示帮助列出可用的 Linux 发行版列出已安装的 Linux 发行版检查 WSL 版本更新 WSL通过 PowerShell 或 CMD 运行特定的 Linux 发行版关闭WSL全部服…

Centos7防火墙及端口开启

1、防火墙 1.1、查看防火墙是否开启 systemctl status firewalld 1.2、开启防火墙 firewall-cmd --list-ports 1.3、重启防火墙 firewall-cmd --reload 2、端口 2.1、查看所有已开启的端口号 firewall-cmd --list-ports 2.2、手动开启端口 启动防火墙后&#xff0c;默认没有开…

智能优化算法应用:基于共生生物算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于共生生物算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于共生生物算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.共生生物算法4.实验参数设定5.算法结果6.…

1、springboot项目运行报错

问题1&#xff1a;获取不到配置文件的参数 我的配置文件获取的参数如下&#xff1a; public class Configures{Value("${configmdm.apk.apkName}")private static String apkName;private void setApkName(String apkName) {Configures.apkName apkName;}private …

【亚马逊云科技】通过高性能低延迟对象存储 S3实现网站资源托管

本篇文章授权活动官方亚马逊云科技文章转发、改写权&#xff0c;包括不限于在 亚马逊云科技开发者社区, 知乎&#xff0c;自媒体平台&#xff0c;第三方开发者媒体等亚马逊云科技官方渠道 文章目录 前言1 S3 介绍1.1 优点 2 使用步骤2.1 注册账户2.2 创建存储桶2.2.1 打开控制…

C/C++常见面试题(二)

接前面C/C常见面试题&#xff08;一&#xff09;&#xff0c;继续巩固 目录 1 sizeof和strlen的区别 2 宏定义的陷阱 3 不使用sizeof计算出类型或者变量所占的内存的字节数 4 给定一个数判断是否其是2的N次幂 5 C/C打印所在文件、行号、函数、日期&#xff0c;时间、遵循的…

每日分享,以元旦为题的诗词

元旦佳节即将来临&#xff0c;相信大家都会在朋友圈表达一下自己的情感&#xff0c;不管大家以前是怎么表达的&#xff0c;今天小编给你分享几首以元旦为题的几首诗&#xff0c;喜欢的朋友可以自取&#xff0c;想要更多免费的诗词&#xff0c;请自行百度或小程序搜索&#xff1…

智能守护,数据安全稳中求胜!上海迅软DSE助力家具家电行业引领潮流!

随着中国经济的蓬勃发展&#xff0c;家具家电企业正迎来“精品制造”的时代&#xff0c;业内竞争日益激烈。为了提升产品竞争力、扩大市场占有率&#xff0c;企业亟需加强对自主品牌的安全建设&#xff0c;确保品牌的自主知识产权、产品生产资料以及销售信息等核心数据不受泄漏…

C++二维数组(2)

图形相似度 题目描述&#xff1a; 给出两幅相同大小的黑白图像&#xff08;用0-1矩阵&#xff09;表示&#xff0c;求它们的相似度。 说明&#xff1a;若两幅图像在相同位置上的像素点颜色相同&#xff0c;则称它们在该位置具有相同的像素点。 两幅图像的相似度定义为相同像素…

为什么阿里云不能免费帮用户无限抵御DDoS攻击

DDoS防御需要成本&#xff0c;其中最大的成本就是带宽费用。带宽是阿里云向电信、联通、移动等运营商购买&#xff0c;运营商计算带宽费用时不会把DDoS攻击流量清洗掉&#xff0c;而是直接收取阿里云的带宽费用。阿里云云盾在控制成本的情况下会尽量为阿里云用户免费防御 DDoS攻…

PXI总线测试模块-6935B 微波本振源

6935B 微波本振源 PXI总线测试模块 650MHz~10GHz 01 产品综述 6935B微波本振源采用3U 2槽PXIe结构形式&#xff0c;具有频率范围宽、相位噪声低、频率分辨力高、体积小等优点。其优异的性能使其可适用于通信以及导航设备等众多领域产品的研发、生产、检测与维护中&#xff0c…

neuq-acm预备队训练week 9 P3367 【模板】并查集

题目描述 如题&#xff0c;现在有一个并查集&#xff0c;你需要完成合并和查询操作。 输入格式 解题思路 并查集的用法 AC代码 #include <bits/stdc.h> using namespace std; #define Max 1000001 int zi,xi[Max],yi[Max],Fa[Max]; int find(int x); bool qu(int u,…

垃圾回收 (GC) 在 .NET Core 中是如何工作的?

提起GC大家肯定不陌生&#xff0c;但是让大家是说一下GC是怎么运行的&#xff0c;可能大多数人都不太清楚&#xff0c;这也很正常&#xff0c;因为GC这东西在.NET基本不用开发者关注&#xff0c;它是依靠程序自动判断来释放托管堆的&#xff0c;我们基本不需要主动调用Collect(…

【十】python复合模式

10.1 复合模式简介 在前面的栏目中我们了解了各种设计模式。正如我们所看到的&#xff0c;设计模式可分为三大类:结构型、创建型和行为型设计模式。同时&#xff0c;我们还给出了每种类型的相应示例。然而&#xff0c;在软件实现中&#xff0c;模式并是不孤立地工作的。对于所…

智能故障诊断期刊推荐【中文期刊】

控制与决策 http://kzyjc.alljournals.cn/kzyjc/home 兵工学报 http://www.co-journal.com/CN/1000-1093/home.shtml 计算机集成制造系统 http://jsjjc.soripan.net/ 机械工程学报 http://www.cjmenet.com.cn/CN/0577-6686/home.shtml 太阳能学报 https://www.tynxb.org.c…

EM的理论基础

1 EM定义​ 电迁移(Electro-Migration)是指在外加电场下,电子和金属原子之间的动量转移导致材料的运动。这种动量传递导致金属原子(比如Cu原子)从其原始位置移位,如图7-1。这种效应随着导线中电流密度的增加而增加,并且在更高的温度下,动量传递变得更加严重。因此,在先…

TSINGSEE青犀城市道路积水AI检测算法视频智能监管解决方案

近年来&#xff0c;由于城市区域内涝频发&#xff0c;遇到强降水天气出现路面严重积水的情况时有发生&#xff0c;影响交通通行甚至引发事故。所以&#xff0c;对下穿隧道、下沉式道路等路面积水情况的监测显得尤为重要。传统的监管方式很难及时发现道路积水情况&#xff0c;那…

ES分词查询

全文检索介绍 全文检索的发展过程&#xff1a; 数据库使用SQL语句&#xff1a;select * from table where data like “%检索内容%”出现lucene全文检索工具&#xff08;缺点&#xff1a;暴露的接口相对复杂&#xff0c;且没有效率&#xff09;出现分布式检索服务框架solr&am…

小白也能懂的同城外卖跑腿小程序开发入门指南

当下&#xff0c;开发一款同城外卖跑腿小程序成为许多初学者关注的焦点。本文将为你提供一份小白也能理解的同城外卖跑腿小程序开发入门指南&#xff0c;带你一步步进入这个令人兴奋的领域。 一、了解市场需求与用户痛点 在开始开发同城外卖跑腿小程序之前&#xff0c;首先要深…