網頁

2019/2/22

MyBatis的一級快取local cache原始碼分析

本篇從MyBatis的原始碼來理解Mybatis的第一級快取(local cache)的機制。

MyBatis有兩種快取,分別為local cache(第一級快取)及second level cache(第二及快取)。

每一個SqlSession都有自己的local cache,彼此獨立不共享。每一次執行SQL都會將結果存入local cache中,之後若做同樣的查詢便可從local cache中取得資料而不用從資料庫取得。

而local cache的快取資料會在SqlSession進行update,commit,rollback及close時被清除,因為這樣才能讓下一次查詢取得最新的資料。

但local cache的問題是其作用範圍僅限於SqlSession實例本身,每一個SqlSession有各自的local cache,所以可能會造成資料不一致的問題,因此在分散式系統中應避免使用。

例如有兩個SqlSession分別為s1及s2。當s1第一次查詢時,MyBatis便會將查詢結果存入local cache;接著s2更新同一筆資料;s1再進行同樣的查詢便會從自己的local cache中取得快取資料,但卻是s2修改前的舊資料,而非從資料庫取得被s2修改過的資料。

SqlSessionlocalCacheScope預設為SESSION,若要避免上述問題,應該將localCacheScope設為STATEMENNT,表示local cache的存續期間僅會在一個SQL敘述間,當SQL執行結束後local cache便會被清除,也就是在同一個SqlSession中不同的SQL執行間不會共用local cache。

菜鳥工程師-肉豬

MyBatis是透過SqlSession來存取資料庫,例如使用SqlSession.selectOne()從資料庫查詢一個User如下:

String resource = "idv/matt/config/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource); // mybatis配置檔位置
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 取得SqlSessionFactory

SqlSession session = sqlSessionFactory.openSession(); // 開啟(建立)一個SqlSession
try {
    User user = session.selectOne("idv.matt.mapper.UserMapper.selectByName", "John"); // 查詢User
} finally {
    session.close(); // 關閉SqlSession,且清除快取
}

SqlSessionFactoryBuilder().build()建立SqlSessionFactory的實例DefaultSqlSessionFactory

在呼叫DefaultSqlSessionFactory.openSession()的過程中,會先建立Executor的實例SimpleExecutor(繼承BaseExecutor)並傳入DefaultSqlSession的建構式參數。

public class DefaultSqlSessionFactory implements SqlSessionFactory {
    ...
    @Override
    public SqlSession openSession() {
        return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
    }
    ...
    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);
            final Executor executor = configuration.newExecutor(tx, execType); // 預設建立SimpleExecutor
            return new DefaultSqlSession(configuration, executor, autoCommit); // 建立Session的實例
        }
        ...
    }
}

SqlSession.selectOne()會轉呼叫selectList()最後會呼叫Executor的實例BaseExecutor.query()來查詢,節錄原始碼如下

public class DefaultSqlSession implements SqlSession {
    ...
    private final Executor executor;
    ...
    public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
        this.configuration = configuration;
        this.executor = executor; // SqlSessionFactory在開啟Session時便會建立新的Executor實例
        this.dirty = false;
        this.autoCommit = autoCommit;
    }
    
    @Override
    public <T> T selectOne(String statement, Object parameter) {
        // Popular vote was to return null on 0 results and throw exception on too many.
        List<T> list = this.<T>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;
        }
    }
    ...
    @Override
    public <E> List<E> selectList(String statement, Object parameter) {
        return this.selectList(statement, parameter, RowBounds.DEFAULT);
    }
    ...
    @Override
    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        try {
            MappedStatement ms = configuration.getMappedStatement(statement);
            return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); // 交由Executor執行
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }
    ...
}

BaseExecutor.query()會先呼叫createCacheKey()並以Mapper id,查詢偏移數(offset),筆數限制(limit),SQL語句,條件參數計算雜湊值做為local cache的key(CacheKey)。實際進入資料庫查詢前會先以key去local cache找是否有資前查詢過儲存的快取資料,若有返回快取中的資料,若無則進資料庫查詢。

若進資料庫查詢後,會以剛剛的key將查詢結果放入local cache中,所以之後在同個SqlSession下進行同樣的查詢,則會從local cache取出資料。

public abstract class BaseExecutor implements Executor {

    ...
    protected PerpetualCache localCache; // 這就是local cache 一級快取
    protected PerpetualCache localOutputParameterCache; // 若呼叫StoredProcedure,用來儲存StoredProcedure的OUT參數
    ...
    protected BaseExecutor(Configuration configuration, Transaction transaction) { // BaseExecutor建構式
        this.transaction = transaction;
        this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>(); // 執行緒安全Queue
        this.localCache = new PerpetualCache("LocalCache");
        this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
        this.closed = false;
        this.configuration = configuration;
        this.wrapper = this;
    }
  
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); // 建立local cache的key
        return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
    
    @SuppressWarnings("unchecked")
    @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; // 進資料庫查詢前先看local cache中是否有資料
            if (list != null) {
              handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); // 處理StoredProcedure的OUT參數快取
            } 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) { // localCacheScope若為STATEMENT,則查詢結束後清除local cache
                // issue #482
                clearLocalCache(); // 清除local cache
            }
        } 
        return list;
    }
    
    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 {
            list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            localCache.removeObject(key);
        }
        localCache.putObject(key, list); // 將查詢結果放入local cache
        if (ms.getStatementType() == StatementType.CALLABLE) {
            localOutputParameterCache.putObject(key, parameter);
        }
        return list;
    }

} 

而儲存快取的localCache類別為PerpetualCache,此類別負責維護以HashMap<Object, Object>儲存的快取資料。

public class PerpetualCache implements Cache {
    ...
    private Map<Object, Object> cache = new HashMap<Object, Object>(); // local cache真正的儲存空間
    
    public PerpetualCache(String id) {
      this.id = id;
    }   
    ...
    // 存放快取資料
    @Override
    public void putObject(Object key, Object value) {
      cache.put(key, value);
    }
    // 取得快取資料
    @Override
    public Object getObject(Object key) {
      return cache.get(key);
    }
}

菜鳥工程師-肉豬

至於Spring與MyBatis整合後,SqlSession的實例改由SqlSessionTemplate產生SqlSession的動態代理sqlSessionProxy負責執行SQL。建構sqlSessionProxy時傳入SqlSessionInterceptor的實例中,會在每次執行SQL後close session來清除local cache,所以不會有上述的髒資料的問題。這邊的邏輯是例用代理模式(Proxy)搭配反射(Reflection)來實現如同AOP的方法切入。

public class SqlSessionTemplate implements SqlSession, DisposableBean {
    ...
    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) 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 {
            SqlSession sqlSession = getSqlSession(
                  SqlSessionTemplate.this.sqlSessionFactory,
                  SqlSessionTemplate.this.executorType,
                  SqlSessionTemplate.this.exceptionTranslator);
            try {
                Object result = method.invoke(sqlSession, args); // 呼叫SqlSession的執行方法
                if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
                    // force commit even on non-dirty sessions because some databases require
                    // a commit/rollback before calling close()
                    sqlSession.commit(true);
                }
                return result;
            } catch (Throwable t) {
                Throwable unwrapped = unwrapThrowable(t);
                if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
                    // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
                    closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                    sqlSession = null;
                    Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
                    if (translated != null) {
                        unwrapped = translated;
                    }
                }
                throw unwrapped;
            } finally {
                if (sqlSession != null) {
                    closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory); // 關閉SqlSession
                }
            }
        }
    }  
}  

整體結構大概如下



這是我第一次比較認真的去看框架的原始碼,而原本我只是想知道怎麼把local cache關掉而已。


參考:

沒有留言:

張貼留言