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.pswp.cn/news/215560.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/215560.shtml
英文地址,請注明出處:http://en.pswp.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控件包含了許多開發人員友好的…

rsyslog配置以及原理

rsyslog 日志由程序產生&#xff0c;在內存中產生。通過Rsyslog來將內存中程序產生的日志持久化到硬盤&#xff0c;并且支持udp、tcp等協議來進行不同服務器的日志同步。 /var/log/messages:大多數系統日志信息紀錄在此/var/log/secure&#xff1a;安全和身份認證相關的消息和…

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;期末總結以及各種演講。在…

HTML---基礎

文章目錄 前言一、pandas是什么&#xff1f;二、使用步驟 1.引入庫2.讀入數據總結 前言 一.HTML概述 HTML&#xff08;超文本標記語言&#xff09;是一種用于創建網絡頁面的標記語言。它以標記的形式編寫&#xff0c;該標記描述了文檔的結構和內容。HTML文件由一系列標記&#…

六級高頻詞組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;小型的文件存儲系統監…

寄存器、緩存、內存、硬盤、存儲器的理解

https://blog.csdn.net/heixiaolong7/article/details/51226378 只要能存儲數據的器件都可以稱之為存儲器&#xff0c;它的含義覆蓋了寄存器&#xff0c;緩存&#xff0c;內存&#xff0c;硬盤。cpu訪問快慢的速度依次為 寄存器-> 緩存->內存->硬盤 寄存器是中央處…

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界面編程單窗口顯示多圖片鼠標事件鍵盤事件滑動條事件 四、實驗…

唯創知音WTN6080-8S語音芯片在咖啡機中的應用:增添聲音魅力,提升用戶體驗

在快節奏的現代生活中&#xff0c;咖啡機已成為許多家庭和辦公室的必備設備&#xff0c;為人們提供了便捷和高品質的咖啡享受。然而&#xff0c;對于很多用戶來說&#xff0c;操作咖啡機可能是一項復雜而棘手的任務。為了解決這一難題&#xff0c;唯創知音WTN6080-8S語音芯片被…

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;當模擬…