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: 1SysRole{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 {// 略@Overridepublic 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分支,详情分析请看代码:

  @Overridepublic 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️⃣}@Overridepublic 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 {// 略@Overridepublic <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; // 默认被装饰的实现类 SimpleExecutorprivate final TransactionalCacheManager tcm = new TransactionalCacheManager();public CachingExecutor(Executor delegate) {this.delegate = delegate;delegate.setExecutorWrapper(this);}// 略@Overridepublic <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);}@Overridepublic <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(一级缓存)具体的一个实现类// 略@Overridepublic <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 #601deferredLoads.clear();if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {// issue #482clearLocalCache();}}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方法:

  @Overridepublic <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中,这次直接拿到了缓存的数据:

@Overridepublic <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>

流程分析

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

二级缓存的验证代码:

@Testpublic 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方法中,我们看到了二级缓存的代码:

    @Overridepublic <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();}}
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OpenAI承认GPT-4变懒,即将发布修复方案提升性能

目录 1OpenAI承认GPT-4变懒&#xff0c;即将发布修复方案提升性能 2一文秒懂人工智能全球近况 1OpenAI承认GPT-4变懒&#xff0c;即将发布修复方案提升性能 **划重点:** 1. &#x1f92f; 用户反馈:GPT-4使用者抱怨OpenAI破坏了体验&#xff0c;称模型几乎“害怕”提供答案。…

Wireshark使用技巧

Wireshark作为网络数据软件&#xff0c;功能强大&#xff0c;本专栏介绍仅为冰山一角&#xff0c;仅仅是一个入门级别的介绍&#xff0c;大部分功能还需要在日常工作中进行挖掘。 总结Wireshark软件的使用技巧如下&#xff1a; 1.合理部署Wireshark的位置&#xff0c;从源头保障…

基于Java SSM框架实现电影售票系统项目【项目源码+论文说明】

基于java的SSM框架实现电影售票系统演示 摘要 21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的重要性已逐渐被人们所认识&#…

界面控件DevExpress WPF导航组件,助力升级应用程序用户体验!(下)

DevExpress WPF的Side Navigation&#xff08;侧边导航&#xff09;、TreeView、导航面板组件能帮助开发者在WPF项目中添加Windows样式的资源管理器栏或Outlook NavBar&#xff08;导航栏&#xff09;&#xff0c;DevExpress WPF NavBar和Accordion控件包含了许多开发人员友好的…

HTTP详解

1. web 1.1 web相关概念 软件架构 C /S&#xff1a;客户端/服务器端 需要安装客户端应用 B/S&#xff1a;浏览器/服务器端 不需要安装客户端应用&#xff0c;对于用户来说只需要记住域名访问就可以,高效,客户端零维护 资源分类 静态资源&#xff1a;所有用户访问后&#x…

数据库系统原理与实践 笔记 #12

文章目录 数据库系统原理与实践 笔记 #12事务管理和并发控制与恢复(续)并发控制SQL-92中的并发级别基于锁的协议基于锁的协议的隐患锁的授予封锁协议两阶段封锁协议多粒度粒度层次的例子意向锁类型相容性矩阵多粒度封锁模式基于时间戳的协议基于时间戳协议的正确性基于有效性检…

怎样在PPT中加入音频文件?记好这4个简单操作!

“我要制作一个比较专业的PPT来汇报工作成果&#xff0c;想在PPT里加一段音乐&#xff0c;但是不知道应该如何操作&#xff0c;有没有朋友可以指导一下呢&#xff1f;” PPT作为一种常用的文件形式&#xff0c;很多用户会将其用于工作汇报&#xff0c;期末总结以及各种演讲。在…

六级高频词组2

目录 词组 参考链接 词组 51. arise from&#xff08;be caused by&#xff09; 由…引起。 52. arrange for sb.sth. to do sth. 安排…做… 53. arrive on 到达&#xff1b; arrive at 到达某地&#xff08;小地方&#xff09;&#xff1b;得出&#xff0c;作出&#x…

zookeeper基础内容

文章目录 Zookeeper基础概述数据结构Zookeeper节点操作zookeeper节点操作命令数据模型 znode 结构 zookeeper java客户端ZooKeeper原生APICuratorzkClient对比总结 Zookeeper基础 概述 zookeeper&#xff08;分布式协调服务&#xff09; 本质&#xff1a;小型的文件存储系统监…

Springboot内置Tomcat线程数优化

Springboot内置Tomcat线程数优化 # 等待队列长度&#xff0c;默认100。队列也做缓冲池用&#xff0c;但也不能无限长&#xff0c;不但消耗内存&#xff0c;而且出队入队也消耗CPU server.tomcat.accept-count1000 # 最大工作线程数&#xff0c;默认200。&#xff08;4核8g内存…

Spring 的缓存机制【记录】

一、背景 在最近的业务需求开发过程中遇到了“传说中”的循环依赖问题&#xff0c;在之前学习Spring的时候经常会看到Spring是如何解决循环依赖问题的&#xff0c;所谓循环依赖即形成了一个环状的依赖关系&#xff0c;这个环中的某一个点产生不稳定变化都会导致整个链路产生不…

OpenCV-opencv下载安装和基本操作

文章目录 一、实验目的二、实验内容三、实验过程OpenCV-python的安装与配置python下载和环境配置PIP镜像安装Numpy安装openCV-python检验opencv安装是否成功 openCV-python的基本操作图像输入和展示以及写出openCV界面编程单窗口显示多图片鼠标事件键盘事件滑动条事件 四、实验…

Altman作了多少恶?排挤首席科学家出GPT5开发、离间董事会、PUA员工

在山姆奥特曼&#xff08;Sam Altman&#xff09;被OpenAI董事会突然解职后的几天里&#xff0c;这个消息在科技圈引发轰动&#xff0c;该公司内部员工和许多科技界人士甚至将此举比作一场政变。 奥特曼被解雇后立即传出的说法是&#xff0c;OpenAI的广大员工都很喜欢他&#x…

一入一出模拟量两线制无源 4-20mA隔离变送器

一入一出模拟量两线制无源 4-20mA隔离变送器 特征与应用&#xff1a; ◆薄体积&#xff0c;低成本&#xff0c;国际标准 DIN35mm 导轨安装方式 ◆两端隔离(输入、输出间相互隔离) ◆单通道输入单通道输出 ◆高精度等级(0.1%,0.2% F.S) ◆高线性度(0.1% F.S) ◆高隔离电压(3000…

32位ADC布局的指导方针

接地必须是一个低阻抗连接&#xff0c;以使回流电流不受干扰地流回各自的源。接地面连接尽量短且直。使用过孔连接接地线时&#xff0c;应并联多个过孔&#xff0c;以减小对地阻抗。 混合信号布局有时包含在一个位置捆绑在一起的单独的模拟和数字地平面;但是&#xff0c;当模拟…

活动回顾 | 菊风亮相 GTC2023 全球流量大会

2023年12月5日-12月6日&#xff0c;由白鲸出海主办的【GTC2023 全球流量大会】在中国深圳盛大召开。 本次大会荟聚海内外优质企业品牌&#xff0c;以专业的“展会”形式&#xff0c;全方位呈现跨境出海的成果&#xff0c;探索多元化的跨境商业模式&#xff0c;大会涵盖社交娱乐…

【Jeecg Boot 3 - 第二天】1.1、后端 docker-compose 部署 JEECGBOOT3

一、场景 二、实战 ▶ 2.1 修改配置文件 &#xff1e; 目的一&#xff1a;将 dev 变更为生产环境 prod &#xff1e; 目的二&#xff1a;方便spring项目调用docker同个network下的redis和mysql ▶ 2.2 编写dockerfile ▶ 2.3 编写docker-compose.yaml ▶ 2.4 打…

Qt/C++音视频开发59-使用mdk-sdk组件/原qtav作者力作/性能凶残/超级跨平台

一、前言 最近一个月一直在研究mdk-sdk音视频组件&#xff0c;这个组件是原qtav作者的最新力作&#xff0c;提供了各种各样的示例demo&#xff0c;不仅限于支持C&#xff0c;其他各种比如java/flutter/web/android等全部支持&#xff0c;性能上也是杠杠的&#xff0c;目前大概…

cadence中如何在更新原理图封装

cadence中如何在更新原理图封装 一、更改原理图封装 当原理图画好后&#xff0c;如果我们发现某个封装错了&#xff0c;需要改动&#xff0c;需要找到你最初画Library中器件封装文件打开&#xff0c;进行修改&#xff0c;修改好后保存。 二、更新封装 保存好后&#xff0c;…