天天看點

從根上了解Mybatis的一級、二級緩存

1\. 寫在前頭

這篇文章主要講一級緩存,它的作用範圍和源碼分析

(本來想把一二級緩存合在一起,發現太長了)

2\. 準備工作

2.1 兩個要用的實體類

public class Department {

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

    private String id;

    /**
     * 部門名稱
     */
    private String name;

    /**
     * 部門電話
     */
    private String tel;

    /**
     * 部門成員
     */
    private Set<User> users;
}      
public class User {

    private String id;

    private String name;

    private Integer age;

    private LocalDateTime birthday;

    private Department department;
}      

2.2 Mapper.xml檔案中要用的SQL

  • DepartmentMapper.xml,兩條SQL,一條根據ID比對,一條清除緩存,注意fulshCache标簽
<select id="findById" resultType="Department">
        select * from department
        where id = #{id}
    </select>

    <!-- flushCache 所有namespace 的一級緩存 和 目前namespace 的二級緩存均會清除 預設是false-->
    <select id="cleanCathe" resultType="int" flushCache="true">
        select count(department.id) from department;
    </select>      
  • UserMapper.xml,簡簡單單的查詢所有的user
<select id="findAll" resultMap="userMap">
        select u.*, td.id, td.name as department_name
        from user u
        left join department td
        on u.department_id = td.id
    </select>      

3\. 一級緩存

  • 一級緩存是基于SQLSession的,同一條SQL執行第二遍的時候會直接從緩存中取,測試下看看
public static void main(String[] args) throws IOException {
        InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        // 開啟二級緩存需要在同一個SqlSessionFactory下,二級緩存存在于 SqlSessionFactory 生命周期,如此才能命中二級緩存
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(xml);

        SqlSession sqlSession = sqlSessionFactory.openSession();
        DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
        System.out.println("----------department第一次查詢 ↓------------");
        departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
        System.out.println("----------department一級緩存生效,控制台看不見SQL ↓------------");
        departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");

    }      
  • 可以發現控制台在第二次查詢的時候,一級緩存生效,沒有出現SQL
  • 從根上了解Mybatis的一級、二級緩存
  • 我們清空下一級緩存再試試
xml檔案中flushCache标簽 會清除所有namespace 的一級緩存 和 目前namespace 的二級緩存均會清除 預設是false
public static void main(String[] args) throws IOException {
        InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        // 開啟二級緩存需要在同一個SqlSessionFactory下,二級緩存存在于 SqlSessionFactory 生命周期,如此才能命中二級緩存
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(xml);

        SqlSession sqlSession = sqlSessionFactory.openSession();
        DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
        System.out.println("----------department第一次查詢 ↓------------");
        departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
        System.out.println("----------department一級緩存生效,控制台看不見SQL ↓------------");
        departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
        System.out.println("----------清除一級緩存 ↓------------");
        departmentMapper.cleanCathe();
        System.out.println("----------清除後department再一次查詢,SQL再次出現 ↓------------");
        departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
    }      
  • 控制台日志很清晰,清除緩存後又重新查了一遍

3.1 一級緩存失效的情況

3.1.1 不同SQLSession下同一條SQL一級緩存不生效
  • 建立一個新的sqlSession1執行相同的SQL,發現不同SQLSession下不共享一級緩存
SqlSession sqlSession = sqlSessionFactory.openSession();
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
        DepartmentMapper departmentMapper1 = sqlSession1.getMapper(DepartmentMapper.class);
        System.out.println("----------department第一次查詢 ↓------------");
        departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
        System.out.println("----------sqlSession1下department執行相同的SQL,控制台出現SQL ↓------------");
        departmentMapper1.findById("18ec781fbefd727923b0d35740b177ab");      
從根上了解Mybatis的一級、二級緩存
3.1.2 兩次相同查詢SQL間有Insert、Delete、Update語句出現
  • 因為Insert、Delete、Update的flushCache标簽 預設為 true ,執行它們時,必然會導緻一級緩存的清空,進而引發之前的一級緩存不能繼續使用的情況(這跟我們上邊清除一級緩存的SQL例子一緻)
3.1.3 調用sqlSession.clearCache()方法
  • 這個方***将一級緩存清除,效果是一樣的

3.2 一級緩存源碼:緩存被儲存在了哪裡?

3.2.1 該如何找打它的位置
  • Mybatis頂層的緩存是接口Cache,檢視它的實作類
  • 從根上了解Mybatis的一級、二級緩存
  • 發現大部分實作類的包都是decorators(裝飾器),隻有PerpetualCache是Impl,是以我們确定的說,它就是我們要找的緩存實作類,點進去看看,發現隻是組合了HashMap...
public class PerpetualCache implements Cache {

  private final String id;

  // 看這裡
  private final Map<Object, Object> cache = new HashMap<>();

  ...
}
複制代碼      
  • 那這個PerpetualCache被放在哪裡呢? 我們想到了一級緩存是基于SQLSession,那我們去DefaultSQLSession,它預設的實作類裡看看
public class DefaultSqlSession implements SqlSession {

  private final Configuration configuration;
  private final Executor executor;

  private final boolean autoCommit;
  private boolean dirty;
  private List<Cursor<?>> cursorList;

  ...
}
複制代碼      
  • 發現并沒有哇!DefaultSqlSession還有兩個東西,Configuration是全局的配置,這裡邊兒應該是沒有,那我們隻能再去Executor裡看看了
  • 從根上了解Mybatis的一級、二級緩存
  • 發現它是個接口,實作類有一個CachingExecutor!立馬點進去!
public class CachingExecutor implements Executor {

  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  ...
}
複制代碼      
  • 發現還是沒有???
  • 從根上了解Mybatis的一級、二級緩存
  • 但是Executor還有一個BaseExecutor,最後一家了,再在沒有關了Idea睡覺了
public abstract class BaseExecutor implements Executor {

  private static final Log log = LogFactory.getLog(BaseExecutor.class);

  protected Transaction transaction;
  protected Executor wrapper;

  protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
  // o??!! 不就在這呢嘛,小老弟
  protected PerpetualCache localCache;
  protected PerpetualCache localOutputParameterCache;
  protected Configuration configuration;

  ...
}      
  • 它來了,原來在這藏着呢呀,行了,這把知道它的位置了,我們直接看SQL執行的時候是怎麼存的,怎麼取的吧!
3.2.2 query()方法
  • BaseExecutor的query()方法,看看注釋,很簡單
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);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      deferredLoads.clear();
      // 全局localCacheScope設定為statement,則清空一級緩存
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache();
      }
    }
    return list;
  }      
3.2.3 寫兩條Sql,Debug看一下
System.out.println("----------department第一次查詢 ↓------------");
        departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
        System.out.println("----------department一級緩存生效,控制台看不見SQL ↓------------");
        departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
複制代碼      
  • 哎,很對,第一次果然去資料庫裡查了
  • 從根上了解Mybatis的一級、二級緩存
  • 哎,更對了,第二次果然取得緩存
  • 好嘛,真簡單呀

3.3 注意:一級緩存的查詢結果被修改後,竟然...

  • 竟然會對之後取出的一級緩存有影響,測試下看看
System.out.println("----------department第一次查詢 ↓------------");
        Department department = departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
        System.out.println(department);
        department.setName("方圓把名字改了");

        System.out.println("----------department一級緩存生效,控制台看不見SQL ↓------------");
        System.out.println(departmentMapper.findById("18ec781fbefd727923b0d35740b177ab"));      
  • 第一次查詢結果name為null,之後我們修改它的name,第二次查詢取緩存的結果是更改name結果之後的
  • 這是因為存放的資料其實是對象的引用,導緻第二次從一級緩存中查詢到的資料,就是我們剛剛改過的資料

3.4 文末

  • 一級緩存是基于SQLSession的,不同SQLSession間不共享一級緩存
  • 執行Insert、Delete、Update語句會使一級緩存失效
  • 一級緩存在底層被存放在了BaseExecutor中,本質上就是個HashMap
  • 一級緩存存放的資料其實是對象的引用,若對它進行修改,則之後取出的緩存為修改後的資料