理解Mybatis一級緩存,以及如何真正使用到一級緩存

閱讀這篇文章,你將會了解
1.什麼是會話(SqlSession)、執行器(Executor)
2.什麼是Mybatis一級緩存
3.一級緩存的生命週期
4.一級緩存的CacheKey生成策略
5.在日常開發時,怎麼才能用到一級緩存(通過事務)

一.類關係圖:

二.什麼是會話(SqlSession)

  1. 在Mybatis中,SqlSession可以理解爲數據庫訪問的最小粒度,每次的數據庫訪問,都建立在SqlSession的基礎上。

  2. SqlSession的實現類有3種:
    (1)SqlSessionTemplate:與Spring整合時,用作代理的SqlSession,底層實際上還是使用DefaultSqlSession。
    (1)SqlSessionManager:在沒有和Spring整合時,用作代理的SqlSession,底層實際上還是使用DefaultSqlSession。
    (3)DefaultSqlSession:默認的會話實現類,即後續所講的執行期、緩存都是與DefaultSqlSession相關。

  3. 關於SqlSessionTemplate和SqlSessionManager的區別,可以看這篇文章:
    https://blog.csdn.net/u010841296/article/details/89367296

  4. 每個DefaultSqlSession都會實例化一個專屬自己的Executor對象,以及一個Configuration對象。

public class DefaultSqlSession implements SqlSession {
	//配置信息
	private Configuration configuration;
	//執行器
	private Executor executor;
public class Configuration {
	protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
  1. Configuration對象裏面記錄了Mybatis解析xml、掃描mapper獲取到的信息。
  2. 在DefaultSqlSession中,雖然定義了很多sql操作的基礎接口,但是sql操作具體的邏輯並不是在SqlSession完成,而是交託給執行器(Executor)完成。
  3. DefaultSqlSession負責從mappedStatements中拿到用戶調用方法全名(statement)對應的MappedStatement,這個MappedStatement實際上就是用戶自定義的新增該查的Mapper方法。這個是有Mybatis啓動時進行掃描綁定的。
    例如用戶定義了一個方法Dao接口:com.jenson.pratice.mapper.TestMapper.selectByID,
    那麼在mappedStatements中就會有一個鍵值對(“com.jenson.pratice.mapper.TestMapper.selectByID”,mappedStatement)

三.什麼是執行器(Executor)

  1. Executor是DefaultSqlSession中的一個類屬性,定義了基礎的sql操作,比如事務的獲取、提交、回滾,數據的插入、查詢、更改、刪除等基本操作。
  2. Executor不負責創建事務,但是Executor裏面會有事務引用。
  3. 執行器裏面會實例化一級緩存對象。
  4. 相比與DefaultSqlSession,Executor的職責是拿到MappedStatement後,進行具體的數據庫訪問邏輯。包括一級緩存的維護,也是在Executor中完成的。
public abstract class BaseExecutor implements Executor {
	//事務引用
	protected Transaction transaction;
	//緩存對象
	protected PerpetualCache localCache;

四.什麼是Mybatis一級緩存(PerpetualCache)

Mybatis一級緩存實際上就是一個依賴於SqlSession的緩存對象,PerpetualCache裏面的結構很簡單,通過一個k-v結構的cache維護緩存數據。

public class PerpetualCache implements Cache {
  private final String id;
  private Map<Object, Object> cache = new HashMap<Object, Object>();

一級緩存的生命週期:

  1. PerpetualCache的生命週期是和SqlSession相關的,即只有在同一個SqlSession中,一級緩存纔會用到。如果會話介紹,則緩存會清空;
  2. 如果SqlSession調用了close()方法,會釋放掉一級緩存PerpetualCache對象,一級緩存將不可用;
  3. 如果SqlSession調用了clearCache(),會清空PerpetualCache對象中的數據,但是該對象仍可使用;
  4. SqlSession中執行了任何一個update操作(update()、delete()、insert()) ,都會清空PerpetualCache對象的數據,但是該對象可以繼續使用;

Mybatis在什麼時候使用一級緩存:

  1. Mybatis在查詢的時候會先生成一個CacheKey,判斷當前會話的PerpetualCache中是否有這個key的緩存,如果有則返回,沒有則執行查詢。查詢完成後會把結果集放入緩存。
  2. 在CachingExecutor、BaseExecutor中都有維護緩存的邏輯。其中CachingExecutor針對的是二級緩存,BaseExecutor針對的是一級緩存。
public abstract class BaseExecutor implements Executor {
  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 {
      	//如果一級緩存沒有命中,則從數據庫中查詢,並把結果集設置到一級緩存中
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
  1. 實際使用中,查詢的調用鏈可能是這樣的:

DefaultSqlSession --> CacheExecutor --> BaseExecutor --> SimpleExecutor

一級緩存如何生成CacheKey:

在BaseExecutor中,定義了一個方法(createCacheKey)去生成一個CacheKey。通過這個CacheKey判斷2次查詢是否相同,能否使用緩存。

這個CacheKey的值由下面幾個部分組成:

  1. ms.getId():MappedStatement的ID
  2. 分頁查詢的參數offset、limit:這個是指MyBatis自身提供的分頁功能是通過RowBounds來實現的,它通過rowBounds.offset和rowBounds.limit來過濾查詢出來的結果集,這種分頁功能是基於查詢結果的再過濾,而不是進行數據庫的物理分頁。所以不要把他和sql語句中的分頁混淆了。
  3. boundSql.getSql():這個sql字符串仍未處理參數的填充
  4. parameterMappings:會遍歷這個列表,得到實際需要填充進sql的參數,然後放到CacheKey中
  5. configuration.getEnvironment().getId():一個環境id,解決issue #176的bug。
public abstract class BaseExecutor implements Executor {
   @Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        //省略一堆代碼。。。。。
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

開發時,如何使用到一級緩存:

我們經常在某個方法中進行多次數據庫查詢,在實際場景中,每次的數據庫查詢都會開啓一個新的會話(SqlSession)。這種情況下我們是沒有用到一級緩存的,因爲根本就沒有複用到SqlSession。

例如這樣:

@Service("testService")
public class TestService {
    @Autowired
    private TestMapper testMapper;
    public void get(){
        System.out.println(testMapper.selectByID(1));
        System.out.println(testMapper.selectByID(1));
    }
}

get start
2019-05-02 17:44:34.097 DEBUG 98729 — [ main] c.j.p.mapper.TestMapper.selectByID : ==> Preparing: select * from test where id = ?
2019-05-02 17:44:34.121 DEBUG 98729 — [ main] c.j.p.mapper.TestMapper.selectByID : > Parameters: 1(Integer)
2019-05-02 17:44:34.140 DEBUG 98729 — [ main] c.j.p.mapper.TestMapper.selectByID : <
Total: 1
2019-05-02 17:44:34.142 DEBUG 98729 — [ main] c.j.p.mapper.TestMapper.selectByID : ==> Preparing: select * from test where id = ?
2019-05-02 17:44:34.142 DEBUG 98729 — [ main] c.j.p.mapper.TestMapper.selectByID : > Parameters: 1(Integer)
2019-05-02 17:44:34.143 DEBUG 98729 — [ main] c.j.p.mapper.TestMapper.selectByID : <
Total: 1
get end

那麼我們怎樣控制程序複用SqlSession,使get()能用到一級緩存呢?
其中一種辦法就是開啓一個事務:

@Service("testService")
public class TestService {
    @Autowired
    private TestMapper testMapper;
    
    //開啓事務
    @Transactional(rollbackFor = Exception.class)
    public void get(){
        System.out.println("get start");
        testMapper.selectByID(1);
        testMapper.selectByID(1);
        System.out.println("get end");
    }
}

get start
2019-05-02 17:46:31.689 DEBUG 98744 — [ main] c.j.p.mapper.TestMapper.selectByID : ==> Preparing: select * from test where id = ?
2019-05-02 17:46:31.715 DEBUG 98744 — [ main] c.j.p.mapper.TestMapper.selectByID : > Parameters: 1(Integer)
2019-05-02 17:46:31.734 DEBUG 98744 — [ main] c.j.p.mapper.TestMapper.selectByID : <
Total: 1
get end

爲什麼開啓了事務後,就可以複用SqlSession呢?

這裏有2個關注點

1.TransactionInterceptor攔截器會對事務方法進行切面。

大致的調用鏈路是:
TransactionInterceptor.invoke
->TransactionAspectSupport.invokeWithinTransaction
->TransactionAspectSupport.createTransactionIfNecessary
->PlatformTransactionManager.getTransaction(txAttr)
->AbstractPlatformTransactionManager.getTransaction
->AbstractPlatformTransactionManager.prepareSynchronization
->TransactionSynchronizationManager.initSynchronization()

最終在TransactionSynchronizationManager.initSynchronization()中設置事務激活。

public abstract class TransactionSynchronizationManager {
	private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
			new NamedThreadLocal<>("Transaction synchronizations");
			
	public static void initSynchronization() throws IllegalStateException {
		if (isSynchronizationActive()) {
			throw new IllegalStateException("Cannot activate transaction synchronization - already active");
		}
		logger.trace("Initializing transaction synchronization");
		synchronizations.set(new LinkedHashSet<>());
	}

2.SqlSessionUtil.registerSessionHolder

在SqlSessionUtil.registerSessionHolder方法中,只有當TransactionSynchronizationManager.isSynchronizationActive()爲true的時候,纔會在對應ThreadLocal中註冊SqlSessionHolder(包裝了SqlSession)。這時候在一個事務中的數據庫訪問都會複用同一個SqlSession,所以可以用上一級緩存。

整個鏈路的入口是在SqlSessionTemplate中,SqlSessionTemplate中定義了一個攔截器SqlSessionInterceptor,在getSqlSession()的時候,會先查看是否有綁定SqlSession,如果沒有則開啓一個新會話,有則複用會話。

調用鏈路:
->SqlSessionInterceptor.invoke
->SqlSessionUtil.getSqlSession
->SqlSessionUtils.registerSessionHolder

  public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
	//是否有可複用的SqlSession
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Creating a new SqlSession");
    }
	//沒有則打開一個會話
    session = sessionFactory.openSession(executorType);

    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }

只有事務激活的時候,才能註冊SqlSessionHolder,複用SqlSession

  private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {
    SqlSessionHolder holder;
    //只有事務激活的時候,才能註冊SqlSessionHolder,複用SqlSession
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
      Environment environment = sessionFactory.getConfiguration().getEnvironment();

      if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Registering transaction synchronization for SqlSession [" + session + "]");
        }

        holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
        TransactionSynchronizationManager.bindResource(sessionFactory, holder);
        TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
        holder.setSynchronizedWithTransaction(true);
        holder.requested();

判斷事務是否激活

	/**
	 * Return if transaction synchronization is active for the current thread.
	 * Can be called before register to avoid unnecessary instance creation.
	 * @see #registerSynchronization
	 */
	public static boolean isSynchronizationActive() {
		return (synchronizations.get() != null);
	}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章