天天看點

【源碼級】MyBatis緩存政策(一級和二級緩存)

緩存就是記憶體中的資料,常常來自對資料庫查詢結果的儲存。使用緩存,我們可以避免頻繁的與資料庫進行互動,進而提高響應速度MyBatis也提供了對緩存的支援,分為一級緩存和二級緩存,可以通過下圖來了解:

【源碼級】MyBatis緩存政策(一級和二級緩存)

①、一級緩存是SqlSession級别的緩存。在操作資料庫時需要構造sqlSession對象,在對象中有一個資料結構(HashMap)用于存儲緩存資料。不同的sqlSession之間的緩存資料區域(HashMap)是互相不影響的。

②、二級緩存是mapper級别的緩存,多個SqlSession去操作同一個Mapper的sql語句,多個SqlSession可以共用二級緩存,二級緩存是跨SqlSession的

一級緩存

預設是開啟的

①、我們使用同一個sqlSession,對User表根據相同id進行兩次查詢,檢視他們發出sql語句的情況

@Test
  public void firstLevelCacheTest() throws IOException {

    // 1. 通過類加載器對配置檔案進行加載,加載成了位元組輸入流,存到記憶體中 注意:配置檔案并沒有被解析
    InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

    // 2. (1)解析了配置檔案,封裝configuration對象 (2)建立了DefaultSqlSessionFactory工廠對象
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

    // 3.問題:openSession()執行邏輯是什麼?
    // 3. (1)建立事務對象 (2)建立了執行器對象cachingExecutor (3)建立了DefaultSqlSession對象
    SqlSession sqlSession = sqlSessionFactory.openSession();

    // 4. 委派給Executor來執行,Executor執行時又會調用很多其他元件(參數設定、解析sql的擷取,sql的執行、結果集的封裝)
    User user = sqlSession.selectOne("com.itheima.mapper.UserMapper.findByCondition", 1);
    User user2 = sqlSession.selectOne("com.itheima.mapper.UserMapper.findByCondition", 1);

    System.out.println(user == user2);

    sqlSession.close();

  }
           

檢視控制台列印情況:

【源碼級】MyBatis緩存政策(一級和二級緩存)

② 同樣是對user表進行兩次查詢,隻不過兩次查詢之間進行了一次update操作。

@Test
  public void test3() throws IOException {

    // 1. 通過類加載器對配置檔案進行加載,加載成了位元組輸入流,存到記憶體中 注意:配置檔案并沒有被解析
    InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

    // 2. (1)解析了配置檔案,封裝configuration對象 (2)建立了DefaultSqlSessionFactory工廠對象
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

    // 3.問題:openSession()執行邏輯是什麼?
    // 3. (1)建立事務對象 (2)建立了執行器對象cachingExecutor (3)建立了DefaultSqlSession對象
    SqlSession sqlSession = sqlSessionFactory.openSession();

    // 4. 委派給Executor來執行,Executor執行時又會調用很多其他元件(參數設定、解析sql的擷取,sql的執行、結果集的封裝)
    User user = sqlSession.selectOne("com.itheima.mapper.UserMapper.findByCondition", 1);

    User user1 = new User();
    user1.setId(1);
    user1.setUsername("zimu");
    sqlSession.update("com.itheima.mapper.UserMapper.updateUser",user1);
    sqlSession.commit();
    User user2 = sqlSession.selectOne("com.itheima.mapper.UserMapper.findByCondition", 1);

    System.out.println(user == user2);
    System.out.println(user);
    System.out.println(user2);
    System.out.println("MyBatis源碼環境搭建成功....");

    sqlSession.close();

  }
           

檢視控制台列印情況:

【源碼級】MyBatis緩存政策(一級和二級緩存)

③、總結

1、第一次發起查詢使用者id為1的使用者資訊,先去找緩存中是否有id為1的使用者資訊,如果沒有,從 資料庫查詢使用者資訊。得到使用者資訊,将使用者資訊存儲到一級緩存中。

2、 如果中間sqlSession去執行commit操作(執行插入、更新、删除),則會清空SqlSession中的 一級緩存,這樣做的目的為了讓緩存中存儲的是最新的資訊,避免髒讀。

3、 第二次發起查詢使用者id為1的使用者資訊,先去找緩存中是否有id為1的使用者資訊,緩存中有,直 接從緩存中擷取使用者資訊

一級緩存原理探究與源碼分析

問題1:一級緩存 底層資料結構到底是什麼?

問題2:一級緩存的工作流程是怎樣的?

一級緩存 底層資料結構到底是什麼?

之前說

不同SqlSession的一級緩存互不影響

,是以我從SqlSession這個類入手

【源碼級】MyBatis緩存政策(一級和二級緩存)

可以看到,

org.apache.ibatis.session.SqlSession

中有一個和緩存有關的方法——

clearCache()

重新整理緩存的方法,點進去,找到它的實作類

DefaultSqlSession

@Override
  public void clearCache() {
    executor.clearLocalCache();
  }
           

再次點進去

executor.clearLocalCache()

,再次點進去并找到其實作類

BaseExecutor

@Override
  public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  
           

進入

localCache.clear()

方法。進入到了

org.apache.ibatis.cache.impl.PerpetualCache

類中

package org.apache.ibatis.cache.impl;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;
/**
 * @author Clinton Begin
 */
public class PerpetualCache implements Cache {
  private final String id;

  private Map<Object, Object> cache = new HashMap<Object, Object>();

  public PerpetualCache(String id) {
    this.id = id;
  }

  //省略部分...
  @Override
  public void clear() {
    cache.clear();
  }
  //省略部分...
}

           

我們看到了

PerpetualCache

類中有一個屬性

private Map<Object, Object> cache = new HashMap<Object, Object>()

,很明顯它是一個HashMap,我們所調用的

.clear()

方法,實際上就是調用的Map的clear方法

【源碼級】MyBatis緩存政策(一級和二級緩存)

得出結論:

一級緩存的資料結構确實是HashMap

【源碼級】MyBatis緩存政策(一級和二級緩存)

一級緩存的執行流程

我們進入到

org.apache.ibatis.executor.Executor

看到一個方法

CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql)

,見名思意是一個建立CacheKey的方法

找到它的實作類和方法

org.apache.ibatis.executor.BaseExecuto.createCacheKey

【源碼級】MyBatis緩存政策(一級和二級緩存)

我們分析一下建立CacheKey的這塊代碼:

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    //初始化CacheKey
    CacheKey cacheKey = new CacheKey();
    //存入statementId
    cacheKey.update(ms.getId());
    //分别存入分頁需要的Offset和Limit
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    //把從BoundSql中封裝的sql取出并存入到cacheKey對象中
    cacheKey.update(boundSql.getSql());
    //下面這一塊就是封裝參數
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();

    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value);
      }
    }
    //從configuration對象中(也就是載入配置檔案後存放的對象)把EnvironmentId存入
        /**
     *     <environments default="development">
     *         <environment id="development"> //就是這個id
     *             <!--目前事務交由JDBC進行管理-->
     *             <transactionManager type="JDBC"></transactionManager>
     *             <!--目前使用mybatis提供的連接配接池-->
     *             <dataSource type="POOLED">
     *                 <property name="driver" value="${jdbc.driver}"/>
     *                 <property name="url" value="${jdbc.url}"/>
     *                 <property name="username" value="${jdbc.username}"/>
     *                 <property name="password" value="${jdbc.password}"/>
     *             </dataSource>
     *         </environment>
     *     </environments>
     */
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    //傳回
    return cacheKey;
  }
           

我們再點進去

cacheKey.update()

方法看一看

public class CacheKey implements Cloneable, Serializable {
  private static final long serialVersionUID = 1146682552656046210L;
  public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();
  private static final int DEFAULT_MULTIPLYER = 37;
  private static final int DEFAULT_HASHCODE = 17;

  private final int multiplier;
  private int hashcode;
  private long checksum;
  private int count;
  //值存入的地方
  private transient List<Object> updateList;
  //省略部分方法......
  //省略部分方法......
  public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); 
    count++;
    checksum += baseHashCode;
    baseHashCode *= count;
    hashcode = multiplier * hashcode + baseHashCode;
    //看到把值傳入到了一個list中
    updateList.add(object);
  }
 
  //省略部分方法......
}
           

我們知道了那些資料是在CacheKey對象中如何存儲的了。下面我們傳回

createCacheKey()

方法。

【源碼級】MyBatis緩存政策(一級和二級緩存)

我們進入

BaseExecutor

,可以看到一個

query()

方法:

【源碼級】MyBatis緩存政策(一級和二級緩存)

這裡我們很清楚的看到,在執行

query()

方法前,

CacheKey

方法被建立了

我們可以看到,建立CacheKey後調用了query()方法,我們再次點進去:

【源碼級】MyBatis緩存政策(一級和二級緩存)

在執行SQL前如何在一級緩存中找不到Key,那麼将會執行sql,我們來看一下執行sql前後會做些什麼,進入

list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

【源碼級】MyBatis緩存政策(一級和二級緩存)

分析一下:

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    //1. 把key存入緩存,value放一個占位符
	localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      //2. 與資料庫互動
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      //3. 如果第2步出了什麼異常,把第1步存入的key删除
      localCache.removeObject(key);
    }
      //4. 把結果存入緩存
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }
           

一級緩存源碼分析結論:

  1. 一級緩存的資料結構是一個

    HashMap<Object,Object>

    ,它的value就是查詢結果,它的key是

    CacheKey

    CacheKey

    中有一個list屬性,

    statementId,params,rowbounds,sql

    等參數都存入到了這個list中
  2. 先建立

    CacheKey

    ,會首先根據

    CacheKey

    查詢緩存中有沒有,如果有,就處理緩存中的參數,如果沒有,就執行sql,執行sql後執行sql後把結果存入緩存

二級緩存

注意:Mybatis的二級緩存不是預設開啟的,是需要經過配置才能使用的

啟用二級緩存

分為三步走:

1)開啟映射器配置檔案中的緩存配置:

<settings>
    <setting name="cacheEnabled" value="true"/>
 </settings>
           
  1. 在需要使用二級緩存的Mapper配置檔案中配置<cache>标簽
<!--type:cache使用的類型,預設是PerpetualCache,這在一級緩存中提到過。
      eviction: 定義回收的政策,常見的有FIFO,LRU。
      flushInterval: 配置一定時間自動重新整理緩存,機關是毫秒。
      size: 最多緩存對象的個數。
      readOnly: 是否隻讀,若配置可讀寫,則需要對應的實體類能夠序列化。
      blocking: 若緩存中找不到對應的key,是否會一直blocking,直到有對應的資料進入緩存。
      -->  
<cache></cache>
           

3)在具體CURD标簽上配置 useCache=true

<select id="findById" resultType="com.itheima.pojo.User" useCache="true">
       select * from user where id = #{id}
   </select>
           

** 注意:實體類要實作Serializable接口,因為二級緩存會将對象寫進硬碟,就必須序列化,以及相容對象在網絡中的傳輸

具體實作

/**
   * 測試一級緩存
   */
  @Test
  public void secondLevelCacheTest() throws IOException {

    InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

    // 2. (1)解析了配置檔案,封裝configuration對象 (2)建立了DefaultSqlSessionFactory工廠對象
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

    // 3.問題:openSession()執行邏輯是什麼?
    // 3. (1)建立事務對象 (2)建立了執行器對象cachingExecutor (3)建立了DefaultSqlSession對象
    SqlSession sqlSession1 = sqlSessionFactory.openSession();

    // 發起第一次查詢,查詢ID為1的使用者
    User user1 = sqlSession1.selectOne("com.itheima.mapper.UserMapper.findByCondition", 1);

    // ***必須調用sqlSession1.commit()或者close(),一級緩存中的内容才會重新整理到二級緩存中
    sqlSession1.commit();// close();
    // 發起第二次查詢,查詢ID為1的使用者
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    User user2 = sqlSession2.selectOne("com.itheima.mapper.UserMapper.findByCondition", 1);

    System.out.println(user1 == user2);
    System.out.println(user1);
    System.out.println(user2);

    sqlSession1.close();
    sqlSession2.close();


  }
           
【源碼級】MyBatis緩存政策(一級和二級緩存)

二級緩存源碼分析

問題:

① cache标簽如何被解析的(二級緩存的底層資料結構是什麼?)?

② 同時開啟一級緩存二級緩存,優先級?

③ 為什麼隻有執行sqlSession.commit或者sqlSession.close二級緩存才會生效

④ 更新方法為什麼不會清空二級緩存?

标簽 < cache/> 的解析

二級緩存和具體的命名空間綁定,一個Mapper中有一個Cache, 相同Mapper中的MappedStatement共用同一個Cache

根據之前的mybatis源碼剖析,xml的解析工作主要交給XMLConfigBuilder.parse()方法來實作

// XMLConfigBuilder.parse()
  public Configuration parse() {
      if (parsed) {
          throw new BuilderException("Each XMLConfigBuilder can only be used once.");
      }
      parsed = true;
      parseConfiguration(parser.eval("/configuration"));// 在這裡
      return configuration;
  }
  
 // parseConfiguration()
 // 既然是在xml中添加的,那麼我們就直接看關于mappers标簽的解析
 private void parseConfiguration(XNode root) {
     try {
         Properties settings = settingsAsPropertiess(root.eval("settings"));
         propertiesElement(root.eval("properties"));
         loadCustomVfs(settings);
         typeAliasesElement(root.eval("typeAliases"));
         pluginElement(root.eval("plugins"));
         objectFactoryElement(root.eval("objectFactory"));
         objectWrapperFactoryElement(root.eval("objectWrapperFactory"));
         reflectionFactoryElement(root.eval("reflectionFactory"));
         settingsElement(settings);
         // read it after objectFactory and objectWrapperFactory issue #631
         environmentsElement(root.eval("environments"));
         databaseIdProviderElement(root.eval("databaseIdProvider"));
         typeHandlerElement(root.eval("typeHandlers"));
         // 就是這裡
         mapperElement(root.eval("mappers"));
     } catch (Exception e) {
         throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
     }
 }
 
 
 // mapperElement()
 private void mapperElement(XNode parent) throws Exception {
     if (parent != null) {
         for (XNode child : parent.getChildren()) {
             if ("package".equals(child.getName())) {
                 String mapperPackage = child.getStringAttribute("name");
                 configuration.addMappers(mapperPackage);
             } else {
                 String resource = child.getStringAttribute("resource");
                 String url = child.getStringAttribute("url");
                 String mapperClass = child.getStringAttribute("class");
                 // 按照我們本例的配置,則直接走該if判斷
                 if (resource != null && url == null && mapperClass == null) {
                     ErrorContext.instance().resource(resource);
                     InputStream inputStream = Resources.getResourceAsStream(resource);
                     XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                     // 生成XMLMapperBuilder,并執行其parse方法
                     mapperParser.parse();
                 } else if (resource == null && url != null && mapperClass == null) {
                     ErrorContext.instance().resource(url);
                     InputStream inputStream = Resources.getUrlAsStream(url);
                     XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                     mapperParser.parse();
                 } else if (resource == null && url == null && mapperClass != null) {
                     Class<?> mapperInterface = Resources.classForName(mapperClass);
                     configuration.addMapper(mapperInterface);
                 } else {
                     throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                 }
             }
         }
     }
 } 
           

我們來看看解析Mapper.xml

// XMLMapperBuilder.parse()
public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        // 解析mapper屬性
        configurationElement(parser.eval("/mapper"));
        configuration.addLoadedResource(resource);
        bindMapperForNamespace();
    }
 
    parsePendingResultMaps();
    parsePendingChacheRefs();
    parsePendingStatements();
}
 
// configurationElement()
private void configurationElement(XNode context) {
    try {
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.equals("")) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        cacheRefElement(context.eval("cache-ref"));
        // 最終在這裡看到了關于cache屬性的處理
        cacheElement(context.eval("cache"));
        parameterMapElement(context.eval("/mapper/parameterMap"));
        resultMapElements(context.eval("/mapper/resultMap"));
        sqlElement(context.eval("/mapper/sql"));
        // 這裡會将生成的Cache包裝到對應的MappedStatement
        buildStatementFromContext(context.eval("select|insert|update|delete"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
}
 
// cacheElement()
private void cacheElement(XNode context) throws Exception {
    if (context != null) {
        //解析<cache/>标簽的type屬性,這裡我們可以自定義cache的實作類,比如redisCache,如果沒有自定義,這裡使用和一級緩存相同的PERPETUAL
        String type = context.getStringAttribute("type", "PERPETUAL");
        Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
        String eviction = context.getStringAttribute("eviction", "LRU");
        Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
        Long flushInterval = context.getLongAttribute("flushInterval");
        Integer size = context.getIntAttribute("size");
        boolean readWrite = !context.getBooleanAttribute("readOnly", false);
        boolean blocking = context.getBooleanAttribute("blocking", false);
        Properties props = context.getChildrenAsProperties();
        // 建構Cache對象
        builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}
           

先來看看是如何建構Cache對象的

MapperBuilderAssistant.useNewCache()

public Cache useNewCache(Class<? extends Cache> typeClass,
                         Class<? extends Cache> evictionClass,
                         Long flushInterval,
                         Integer size,
                         boolean readWrite,
                         boolean blocking,
                         Properties props) {
    // 1.生成Cache對象
    Cache cache = new CacheBuilder(currentNamespace)
         //這裡如果我們定義了<cache/>中的type,就使用自定義的Cache,否則使用和一級緩存相同的PerpetualCache
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    // 2.添加到Configuration中
    configuration.addCache(cache);
    // 3.并将cache指派給MapperBuilderAssistant.currentCache
    currentCache = cache;
    return cache;
}
           

我們看到一個Mapper.xml隻會解析一次<cache/>标簽,也就是隻建立一次Cache對象,放進configuration中,并将cache指派給MapperBuilderAssistant.currentCache

buildStatementFromContext(context.eval("select|insert|update|delete"));将Cache包裝到MappedStatement
// buildStatementFromContext()
private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}
 
//buildStatementFromContext()
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
        try {
            // 每一條執行語句轉換成一個MappedStatement
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
        }
    }
}
 
// XMLStatementBuilder.parseStatementNode();
public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    ...
 
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultType = context.getStringAttribute("resultType");
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);
 
    ...
    // 建立MappedStatement對象
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
                                        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
                                        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
                                        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
 
// builderAssistant.addMappedStatement()
public MappedStatement addMappedStatement(
    String id,
    ...) {
 
    if (unresolvedCacheRef) {
        throw new IncompleteElementException("Cache-ref not yet resolved");
    }
 
    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    //建立MappedStatement對象
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        ...
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);// 在這裡将之前生成的Cache封裝到MappedStatement
 
    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
        statementBuilder.parameterMap(statementParameterMap);
    }
 
    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
}
           

我們看到将Mapper中建立的Cache對象,加入到了每個MappedStatement對象中,也就是同一個Mapper中所有的MappedStatement中的cache屬性引用的是同一個

有關于<cache/>标簽的解析就到這了。

查詢源碼分析

CachingExecutor
// CachingExecutor
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 建立 CacheKey
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
    // 從 MappedStatement 中擷取 Cache,注意這裡的 Cache 是從MappedStatement中擷取的
    // 也就是我們上面解析Mapper中<cache/>标簽中建立的,它儲存在Configration中
    // 我們在上面解析blog.xml時分析過每一個MappedStatement都有一個Cache對象,就是這裡
    Cache cache = ms.getCache();
    // 如果配置檔案中沒有配置 <cache>,則 cache 為空
    if (cache != null) {
        //如果需要重新整理緩存的話就重新整理:flushCache="true"
        flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
            ensureNoOutParams(ms, boundSql);
            // 通路二級緩存
            List<E> list = (List<E>) tcm.getObject(cache, key);
            // 緩存未命中
            if (list == null) {
                // 如果沒有值,則執行查詢,這個查詢實際也是先走一級緩存查詢,一級緩存也沒有的話,則進行DB查詢
                list = delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                // 緩存查詢結果
                tcm.putObject(cache, key, list);
            }
            return list;
        }
    }
    return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
           

如果設定了flushCache="true",則每次查詢都會重新整理緩存

<!-- 執行此語句清空緩存 -->
<select id="findbyId" resultType="com.itheima.pojo.user" useCache="true" flushCache="true" >
    select * from t_demo
</select>
           

如上,注意二級緩存是從 MappedStatement 中擷取的。由于 MappedStatement 存在于全局配置中,可以多個 CachingExecutor 擷取到,這樣就會出現線程安全問題。除此之外,若不加以控制,多個事務共用一個緩存執行個體,會導緻髒讀問題。至于髒讀問題,需要借助其他類來處理,也就是上面代碼中 tcm 變量對應的類型。下面分析一下。

TransactionalCacheManager
/** 事務緩存管理器 */
public class TransactionalCacheManager {

    // Cache 與 TransactionalCache 的映射關系表
    private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

    public void clear(Cache cache) {
        // 擷取 TransactionalCache 對象,并調用該對象的 clear 方法,下同
        getTransactionalCache(cache).clear();
    }

    public Object getObject(Cache cache, CacheKey key) {
        // 直接從TransactionalCache中擷取緩存
        return getTransactionalCache(cache).getObject(key);
    }

    public void putObject(Cache cache, CacheKey key, Object value) {
        // 直接存入TransactionalCache的緩存中
        getTransactionalCache(cache).putObject(key, value);
    }

    public void commit() {
        for (TransactionalCache txCache : transactionalCaches.values()) {
            txCache.commit();
        }
    }

    public void rollback() {
        for (TransactionalCache txCache : transactionalCaches.values()) {
            txCache.rollback();
        }
    }

    private TransactionalCache getTransactionalCache(Cache cache) {
        // 從映射表中擷取 TransactionalCache
        TransactionalCache txCache = transactionalCaches.get(cache);
        if (txCache == null) {
            // TransactionalCache 也是一種裝飾類,為 Cache 增加事務功能
            // 建立一個新的TransactionalCache,并将真正的Cache對象存進去
            txCache = new TransactionalCache(cache);
            transactionalCaches.put(cache, txCache);
        }
        return txCache;
    }
}
           

TransactionalCacheManager 内部維護了 Cache 執行個體與 TransactionalCache 執行個體間的映射關系,該類也僅負責維護兩者的映射關系,真正做事的還是 TransactionalCache。TransactionalCache 是一種緩存裝飾器,可以為 Cache 執行個體增加事務功能。下面分析一下該類的邏輯。

TransactionalCache
public class TransactionalCache implements Cache {
    //真正的緩存對象,和上面的Map<Cache, TransactionalCache>中的Cache是同一個
    private final Cache delegate;
    private boolean clearOnCommit;
    // 在事務被送出前,所有從資料庫中查詢的結果将緩存在此集合中
    private final Map<Object, Object> entriesToAddOnCommit;
    // 在事務被送出前,當緩存未命中時,CacheKey 将會被存儲在此集合中
    private final Set<Object> entriesMissedInCache;


    @Override
    public Object getObject(Object key) {
        // 查詢的時候是直接從delegate中去查詢的,也就是從真正的緩存對象中查詢
        Object object = delegate.getObject(key);
        if (object == null) {
            // 緩存未命中,則将 key 存入到 entriesMissedInCache 中
            entriesMissedInCache.add(key);
        }

        if (clearOnCommit) {
            return null;
        } else {
            return object;
        }
    }

    @Override
    public void putObject(Object key, Object object) {
        // 将鍵值對存入到 entriesToAddOnCommit 這個Map中中,而非真實的緩存對象 delegate 中
        entriesToAddOnCommit.put(key, object);
    }

    @Override
    public Object removeObject(Object key) {
        return null;
    }

    @Override
    public void clear() {
        clearOnCommit = true;
        // 清空 entriesToAddOnCommit,但不清空 delegate 緩存
        entriesToAddOnCommit.clear();
    }

    public void commit() {
        // 根據 clearOnCommit 的值決定是否清空 delegate
        if (clearOnCommit) {
            delegate.clear();
        }
        
        // 重新整理未緩存的結果到 delegate 緩存中
        flushPendingEntries();
        // 重置 entriesToAddOnCommit 和 entriesMissedInCache
        reset();
    }

    public void rollback() {
        unlockMissedEntries();
        reset();
    }

    private void reset() {
        clearOnCommit = false;
        // 清空集合
        entriesToAddOnCommit.clear();
        entriesMissedInCache.clear();
    }

    private void flushPendingEntries() {
        for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
            // 将 entriesToAddOnCommit 中的内容轉存到 delegate 中
            delegate.putObject(entry.getKey(), entry.getValue());
        }
        for (Object entry : entriesMissedInCache) {
            if (!entriesToAddOnCommit.containsKey(entry)) {
                // 存入空值
                delegate.putObject(entry, null);
            }
        }
    }

    private void unlockMissedEntries() {
        for (Object entry : entriesMissedInCache) {
            try {
                // 調用 removeObject 進行解鎖
                delegate.removeObject(entry);
            } catch (Exception e) {
                log.warn("...");
            }
        }
    }

}
           

存儲二級緩存對象的時候是放到了TransactionalCache.entriesToAddOnCommit這個map中,但是每次查詢的時候是直接從TransactionalCache.delegate中去查詢的,是以這個二級緩存查詢資料庫後,設定緩存值是沒有立刻生效的,主要是因為直接存到 delegate 會導緻髒資料問題

為何隻有SqlSession送出或關閉之後?

那我們來看下SqlSession.commit()方法做了什麼

SqlSession

@Override
public void commit(boolean force) {
    try {
        // 主要是這句
        executor.commit(isCommitOrRollbackRequired(force));
        dirty = false;
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}
 
// CachingExecutor.commit()
@Override
public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();// 在這裡
}
 
// TransactionalCacheManager.commit()
public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
        txCache.commit();// 在這裡
    }
}
 
// TransactionalCache.commit()
public void commit() {
    if (clearOnCommit) {
        delegate.clear();
    }
    flushPendingEntries();//這一句
    reset();
}
 
// TransactionalCache.flushPendingEntries()
private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
        // 在這裡真正的将entriesToAddOnCommit的對象逐個添加到delegate中,隻有這時,二級緩存才真正的生效
        delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
        if (!entriesToAddOnCommit.containsKey(entry)) {
            delegate.putObject(entry, null);
        }
    }
}
           

二級緩存的重新整理

我們來看看SqlSession的更新操作

public int update(String statement, Object parameter) {
    int var4;
    try {
        this.dirty = true;
        MappedStatement ms = this.configuration.getMappedStatement(statement);
        var4 = this.executor.update(ms, this.wrapCollection(parameter));
    } catch (Exception var8) {
        throw ExceptionFactory.wrapException("Error updating database.  Cause: " + var8, var8);
    } finally {
        ErrorContext.instance().reset();
    }

    return var4;
}

public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    this.flushCacheIfRequired(ms);
    return this.delegate.update(ms, parameterObject);
}

private void flushCacheIfRequired(MappedStatement ms) {
    //擷取MappedStatement對應的Cache,進行清空
    Cache cache = ms.getCache();
    //SQL需設定flushCache="true" 才會執行清空
    if (cache != null && ms.isFlushCacheRequired()) {
  this.tcm.clear(cache);
    }
}
           

MyBatis二級緩存隻适用于不常進行增、删、改的資料,比如國家行政區省市區街道資料。一但資料變更,MyBatis會清空緩存。是以二級緩存不适用于經常進行更新的資料。

總結:

在二級緩存的設計上,MyBatis大量地運用了裝飾者模式,如CachingExecutor, 以及各種Cache接口的裝飾器。

  • 二級緩存實作了Sqlsession之間的緩存資料共享,屬于namespace級别
  • 二級緩存具有豐富的緩存政策。
  • 二級緩存可由多個裝飾器,與基礎緩存組合而成
  • 二級緩存工作由 一個緩存裝飾執行器CachingExecutor和 一個事務型預緩存TransactionalCache 完成