# Mybatis(八) - 缓存

## 1. 一级缓存

### 1.1 一级缓存的使用

一级缓存的使用相当的简单，MyBatis 本身就开启一级缓存（通常我们也不会控制它关闭），所以我们可以直接拿来用。

#### 1.1.1 简单使用

一级缓存基于 `SqlSession` ，所以我们可以直接创建 `SqlSessionFactory` ，并从中开启一个新的 `SqlSession` ，默认情况下它会自动开启事务，所以一级缓存会自动使用。

下面我们编写一个简单的示例，这里面我们调用两次 `DepartmentMapper` 的 `findAll` 方法，由于使用一级缓存，所以第二次 `findAll` 方法会直接使用一级缓存的数据，而不会再次向数据库发送 SQL 语句：

```java
public class Level1Application {
    
    public static void main(String[] args) throws Exception {
        InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);
        SqlSession sqlSession = sqlSessionFactory.openSession();
    
        DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
        System.out.println("第一次执行findAll......");
        departmentMapper.findAll();
        System.out.println("第二次执行findAll......");
        departmentMapper.findAll();
    
        sqlSession.close();
    }
}
```

执行 `main` 方法，观察控制台的打印，会发现第一次执行 `findAll` 方法时，它会开启一个 jdbc 的连接，并且发送 SQL 语句到数据库，但第二次再调用时，它没有再次发送 SQL ：（控制台没有打印，完事后直接关闭 jdbc 连接了）

```java
第一次执行findAll......
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Opening JDBC Connection 
[main] DEBUG source.pooled.PooledDataSource  - Created connection 2130772866. 
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@7f010382] 
[main] DEBUG apper.DepartmentMapper.findAll  - ==>  Preparing: select * from tbl_department 
[main] DEBUG apper.DepartmentMapper.findAll  - ==> Parameters:  
[main] DEBUG apper.DepartmentMapper.findAll  - <==      Total: 4 
第二次执行findAll......
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@7f010382] 
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@7f010382] 
[main] DEBUG source.pooled.PooledDataSource  - Returned connection 2130772866 to pool.
```

这就是一级缓存最基本的使用。

#### 1.1.2 清空一级缓存

前面在第 8 章中，我们讲到 statement 的定义里面，`flushCache` 这个属性时提到过，它可以清空一级缓存和它所属的 namespace 下的二级缓存，当清空后，再次调用 `findAll` 时 MyBatis 就会重新发送 SQL 到数据库执行查询了。

一个简单的示例如下：

```java
    public static void main(String[] args) throws Exception {
        InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);
        SqlSession sqlSession = sqlSessionFactory.openSession();
    
        DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
        System.out.println("第一次执行findAll......");
        departmentMapper.findAll();
        System.out.println("第二次执行findAll......");
        departmentMapper.findAll();
        System.out.println("清空一级缓存......");
        departmentMapper.cleanCache();
        System.out.println("清空缓存后再次执行findAll......");
        departmentMapper.findAll();
    
        sqlSession.close();
    }
```

以此法编写的 `main` 方法运行结果如下：

```
第一次执行findAll......
[main] DEBUG apper.DepartmentMapper.findAll  - ==>  Preparing: select * from tbl_department 
[main] DEBUG apper.DepartmentMapper.findAll  - ==> Parameters:  
[main] DEBUG apper.DepartmentMapper.findAll  - <==      Total: 4 
第二次执行findAll......
清空一级缓存......
[main] DEBUG er.DepartmentMapper.cleanCache  - ==>  Preparing: select count(id) from tbl_department 
[main] DEBUG er.DepartmentMapper.cleanCache  - ==> Parameters:  
[main] DEBUG er.DepartmentMapper.cleanCache  - <==      Total: 1 
清空缓存后再次执行findAll......
[main] DEBUG apper.DepartmentMapper.findAll  - ==>  Preparing: select * from tbl_department 
[main] DEBUG apper.DepartmentMapper.findAll  - ==> Parameters:  
[main] DEBUG apper.DepartmentMapper.findAll  - <==      Total: 4
```

可见，清空一级缓存后，`findAll` 方法又重新发送 SQL 查询数据库了。

#### 1.1.3 一级缓存失效的情景

虽说一级缓存确实很好，不过由于一些使用不当，或者意外情况，一级缓存会失效，失效的表现肯定是重复发送同样的 SQL 了。下面我们就来看看，哪些情况会导致一级缓存的失效，以及无法使用到一级缓存的情况。

**1.1.3.1 跨SqlSession的一级缓存不共享**

这个很好理解，一级缓存本身就是 `SqlSession` 级别的缓存，这些缓存只在本 `SqlSession` 内有效，不同的 `SqlSession` 一级缓存不共享。下面我们可以来验证一下。（下面的示例来自 `Level1InvalidApplication` ）

```java
    public static void main(String[] args) throws Exception {
        InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        
        // 跨SqlSession的一级缓存不共享
        DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
        DepartmentMapper departmentMapper2 = sqlSession2.getMapper(DepartmentMapper.class);
        
        departmentMapper.findAll();
        departmentMapper2.findAll();
        
        sqlSession.close();
        sqlSession2.close();
    }
```

看，这样开启两个 `SqlSession` ，在执行 `findAll` 查询时，观察控制台的日志打印，会发现开启了两个全新的 jdbc `Connection` ，并且也发送了两次相同的 SQL 。

```
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Opening JDBC Connection 
[main] DEBUG source.pooled.PooledDataSource  - Created connection 2130772866. 
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@7f010382] 
[main] DEBUG apper.DepartmentMapper.findAll  - ==>  Preparing: select * from tbl_department 
[main] DEBUG apper.DepartmentMapper.findAll  - ==> Parameters:  
[main] DEBUG apper.DepartmentMapper.findAll  - <==      Total: 4 
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Opening JDBC Connection 
[main] DEBUG source.pooled.PooledDataSource  - Created connection 1861781750. 
[main] DEBUG ansaction.jdbc.JdbcTransaction  - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@6ef888f6] 
[main] DEBUG apper.DepartmentMapper.findAll  - ==>  Preparing: select * from tbl_department 
[main] DEBUG apper.DepartmentMapper.findAll  - ==> Parameters:  
[main] DEBUG apper.DepartmentMapper.findAll  - <==      Total: 4
```

**1.1.3.2 两次相同的查询间有DML操作**

DML 操作，也就是**增删改**了，我们前面学习第 8 章时都知道，insert 、update 、delete 标签的 `flushCache` 默认为 true ，执行它们时，必然会导致一级缓存的清空，从而引发之前的一级缓存不能继续使用。这个效果的演示，小册就不重复演示了，与上面 1.2 节的效果是完全一样的。

**1.1.3.3 手动清空了一级缓存**

可能有的小伙伴注意到了，`SqlSession` 有一个 `clearCache` 方法，调用它会直接清空一级缓存，非常简单有效 \~ 下面我们也来试一下效果。

```java
DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
departmentMapper.findAll();
System.out.println("重复调用findAll方法......");
departmentMapper.findAll();
System.out.println("手动清空SqlSession的缓存......");
sqlSession.clearCache();
System.out.println("清空缓存后重新调用findAll方法......");
departmentMapper.findAll();
System.out.println("--------------------------------");
```

这样编写好后，第二次重复调用 `findAll` 方法时，控制台不会发送新的 SQL 语句，但是手动清空后，再调用，控制台就可以看到 SQL 语句的打印了：

```
重复调用findAll方法......
手动清空SqlSession的缓存......
清空缓存后重新调用findAll方法......
[main] DEBUG apper.DepartmentMapper.findAll  - ==>  Preparing: select * from tbl_department 
[main] DEBUG apper.DepartmentMapper.findAll  - ==> Parameters:  
[main] DEBUG apper.DepartmentMapper.findAll  - <==      Total: 4 
--------------------------------
```

**1.1.3.4 与Spring整合时没有开启事务**

默认情况下我们拿到的 `SqlSession` 都是开启了事务的，即便是在用 `SqlSessionFactory` 获取 `SqlSession` 时，传入的参数为 true （ `sqlSessionFactory.openSession(true)` ，意味着不开启事务），连续查询两次 `findAll` 方法时一级缓存也会生效。不过可能有的小伙伴遇到过一个特殊的情况：**用 SpringFramework / SpringBoot 整合 MyBatis 时，当 Service 的方法没有标注** `**@Transactional**` **注解，或者没有被事务增强器的通知切入时，两次查询同一条数据时，会发送两次 SQL 到数据库，这样看上去像是一级缓存失效了**！这种情况出现的原因，小册在这里作一个补充性的讲解。

SpringFramework / SpringBoot 整合 MyBatis 后，Service 方法中没有开启事务时，每次调用 Mapper 查询数据时，底层都会**创建一个全新的** `**SqlSession**` **去查数据库**，而一级缓存本身就是基于 `SqlSession` 的，每次都开启全新的，那不就相当于上面的 1.3.1 节提到的，跨 `SqlSession` 的一级缓存不共享了嘛。

有关这部分原理，小伙伴们可以参照我之前写过的一篇文章理解：[MyBatis的一级缓存竟然还会引来麻烦？](https://juejin.cn/post/6844904201244377095)

#### 1.1.4 使用一级缓存要注意的

一级缓存固然好用，但小心一个比较危险的东西：**一级缓存是存放到 SqlSession 中，如果我们在查询到数据后，直接在数据对象上作修改，修改之后又重新查询相同的数据，虽然此时一级缓存可以生效，但因为存放的数据其实是对象的引用，导致第二次从一级缓存中查询到的数据，就是我们刚刚改过的数据，这样可能会发生一些错误**。

可能这样只用文字说不好理解，我们配一段代码演示一下：

```java
public class Level1ReferenceApplication {
    
    public static void main(String[] args) throws Exception {
        InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        
        DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
        Department department = departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
        System.out.println("department: " + department);
        department.setName("哈哈哈哈");
        System.out.println("department: " + department);
        
        Department department2 = departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
        System.out.println("department2: " + department2);
        System.out.println(department == department2);
    }
}
```

看这段代码，上面先查询一次 `id` 为 `18ec781fbefd727923b0d35740b177ab` 的部门，查询出来以后打印一下，随后将其 `name` 改为 `"哈哈哈哈"` ，修改后再次查询 `id` 为 `18ec781fbefd727923b0d35740b177ab` 的部门，由于此时一级缓存生效，会把缓存中的数据拿出来，最后我们对比一下两个 `Department` 对象的引用是否一致（即判断两个对象是否为同一个）。

运行 `main` 方法，控制台的打印结果如下：

```
department: Department{id='18ec781fbefd727923b0d35740b177ab', name='开发部', tel='123'}
department: Department{id='18ec781fbefd727923b0d35740b177ab', name='哈哈哈哈', tel='123'}
department2: Department{id='18ec781fbefd727923b0d35740b177ab', name='哈哈哈哈', tel='123'}
true
```

危险的现象出现了：由于我们修改了第一次查询的结果，而这个结果本身就是一级缓存中存放的数据库查询结果，导致我们修改了其中的 `name` 属性后，第二次再查询时，取出来的数据是我们刚刚修改了的！这样就有可能引发一些不必要的麻烦和错误了。

如何避免这种情况呢？本质的目的是为了将之前的一级缓存失效掉。要么，用全新的 SqlSession ，要么，查询前清一下一级缓存。不过上面提到的那篇文章中还提到了另外一种方案：**全局配置中，设置** `**local-cache-scope**` **属性为** `**statement**` ，不过这种设置的方法是针对全局了，不是很合适，所以小册在此不展开，有关 `localCacheScope` 配置的描述，小伙伴们可以参照 MyBatis 的文档描述（即下图）。

![img](https://image.ldbmcs.com/xT5Qak.jpg)

### 1.2 一级缓存的设计原理

掌握了一级缓存的使用以及要注意的，下面我们深入源码中，探究一下一级缓存的设计，以及在查询中如何发挥其作用的。

#### 1.2.1 缓存模型的设计

首先我们先来了解一下 MyBatis 的缓存模型，其实缓存的本质是一个类似于 `**Map**` 的东西，有 key 有 value 。MyBatis 中专门设计了一个 `**Cache**` 接口来模仿 `Map` ，定义缓存最基本的增删改查方法。

**1.2.1.1 Cache接口与实现类**

```java
public interface Cache {
    // 每个缓存都有id
    String getId();
    // 放缓存
    void putObject(Object key, Object value);
    // 取缓存
    Object getObject(Object key);
    // 删缓存
    Object removeObject(Object key);
    // 清缓存
    void clear();
    // 查大小
    int getSize();
}
```

有接口那就一定有实现，借助 IDEA ，可以发现 `Cache` 接口的实现类还是很多的：

![img](https://image.ldbmcs.com/oOSg9D.jpg)

注意观察包名！绝大多数 `Cache` 实现类的包名，最后有个 **decorators** ，这很明显是**装饰者**的意思呀！只有一个 `PerpetualCache` ，包名的最后是 impl ，那得了，这分明就是**一个实现类 + 好多个装饰者的设计**了。可是为什么 MyBatis 要将缓存模型设计为一堆装饰者呢？

**1.2.1.2 Cache实现类的装饰者设计意义**

究其根源，我们要先提一点 MyBatis 二级缓存的东西了，MyBatis 中的二级缓存本身是占用应用空间的，换句话说，**MyBatis 中的二级缓存实际使用的是 JVM 的内存**，那默认情况来讲，占用的内存太多不利于应用本身的正常运行，所以 MyBatis 会**针对缓存的各种特性和过期策略**，设计了一些能够修饰原本缓存件的**装饰者**，以此达到**动态拼装缓存实现**的目的。

这个设计到后面的设计模式章节还会再展开讲，毕竟现在我们还没有讲到二级缓存，可能小伙伴们没概念，下一章我们把二级缓存过完之后，在回过头来，可能理解起来会更容易一点。

**1.2.1.3 PerpetualCache的设计**

既然大部分都是装饰者，那我们先看看这个本身 `Cache` 接口的最初实现 `PerpetualCache` ，它是一个没有任何修饰的、最单纯的缓存实现：

```java
public class PerpetualCache implements Cache {

    private final String id;

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

噗。。。这设计也忒不走心了吧，直接套一个 `HashMap` 就完事了？哎，还真就套一层就完事了！因为缓存本身就是 `Map` 类型的设计，直接拿现成的岂不美哉？

至于其它的嘛，现在一级缓存阶段暂时涉及不到，小册先不展开了，小伙伴们也不要在这个阶段去翻看，避免本末倒置。

#### 1.2.2 一级缓存的设计位置

既然有了 `PerpetualCache` ，那它一定是组合到某个位置，从而形成一级缓存的吧！小册先不讲，小伙伴们来猜一下，这个 `PerpetualCache` 能放在哪里呢？

emmmmm，大概率是 `SqlSession` 的实现类中吧！我们翻开 `SqlSession` 接口的默认实现 `DefaultSqlSession` ：

```java
public class DefaultSqlSession implements SqlSession {

    private final Configuration configuration;
    private final Executor executor;

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

很抱歉 \~ 这里并没有缓存的设计呢 \~ （手动狗头）

哎，但是小伙伴们不要急，观察一下这几个成员，你觉得 `PerpetualCache` 最有可能放在这里面的谁里头呢？

连想都不用想，肯定是 `Executor` ，`Configuration` 本身是全局的配置，不适合放 `SqlSession` 实例相关的东西，下面 3 个很明显都放不下，那只剩下 `Executor` 了。OK ，下面我们进去看看 `Executor` 吧：

```java
public interface Executor { ... }
```

呃。。。又是接口。。。这让我咋找呢？诶，别急，我们继续往下看实现类就是啦！借助 IDEA ，可以发现 `Executor` 有如下几个实现类：

![img](https://image.ldbmcs.com/7Jotbb.jpg)

这看类名，第一直觉肯定是 `CachingExecutor` 吧！这类名上都明摆着写着缓存了！我们赶紧点进去看看：

```java
public class CachingExecutor implements Executor {

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

emmmmmm。。。搁这逗我呢？这里面咋是套了一层代理呢？到底真正干活的是谁？

好吧，其实最底层的还是要看 `BaseExecutor` ，这个类名的设计比较类似于 `AbstractXXX` ，它本身也是一个抽象类，是它里面组合了 `PerpetualCache` ：

```java
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;
    // 【！！！看这里！！！】
    protected PerpetualCache localCache;
    protected PerpetualCache localOutputParameterCache;
    protected Configuration configuration;
}
```

可能小伙伴读到这里会有一些许的不爽，“明明一开始直接告诉我不就得了？为什么非要跟耍猴似的折腾我呢？”

小伙伴们不要生气不要着急，阿熊我本身一开始翻这部分源码的时候，就是这个心路历程，很多小伙伴想知道我平时读源码的思路，觉得我在小册里没有讲解很多，所以阿熊也在想着如何能把我的一些思路通过比较容易接受的方式，跟小伙伴们分享一下。思来想去，这样似乎是不错的，于是乎这里就设计了这样的一个你们可能感觉有些 “耍猴” 的环节，希望小伙伴们能理解啦。

OK ，了解了一级缓存的设计位置，下面我们再来看看，如果一个 select 查询被执行时，一级缓存是如何工作的。

#### 1.2.3 一级缓存的生效原理

我们以上面 1.1 的简单示例来测试，在此之前我们找到 `BaseExecutor` 这个类的 `query` 方法（ MyBatis 3.5.5 版本在第 141 行），在这个方法体的第 152 行打一个断点，然后我们以 Debug 的方式运行 `Level1Application` ，当程序停在断点时，我们观察一下一级缓存是如何生效和工作的。

**1.2.3.1 query方法概览**

我们先看看 query 方法的核心逻辑吧，先大概有个思路：

```java
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, 
                         ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // ......
    // 如果statement指定了需要刷新缓存，则清空一级缓存
    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) {
        // ......
        // 全局localCacheScope设置为statement，则清空一级缓存
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            // issue #482
            clearLocalCache();
        }
    }
    return list;
}
```

可以发现，一级缓存起作用的位置，是在向数据库发起查询之前，先拦截检查一下，如果一级缓存中有数据，则直接从缓存中取数据并返回，否则才查询数据库。

理清楚思路后，我们来 Debug 运行。

**1.2.3.2 第一次进入BaseExecutor**

```java
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);
}
```

第一次执行 `departmentMapper.findAll()` 方法，此时可以发现 `localCache` 中是空的，一级缓存干干净净：

![img](https://image.ldbmcs.com/pq7orr.jpg)

OK ，没有数据，那就必须走数据库查询了，进入 `queryFromDatabase` 方法：

```java
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);
    if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
    }
    return list;
}
```

可以发现，`queryFromDatabase` 方法中主要干的事情，就是**查询数据，并放入缓存**了。由于查询到了结果，放入了缓存，所以返回到外层的 `query` 方法后，`localCache` 中就有数据了：

![img](https://image.ldbmcs.com/BxIptZ.jpg)

**1.2.3.3 第二次进入BaseExecutor**

第二次执行 `departmentMapper.findAll()` 方法，因为此时缓存中已经有数据了，所以上面的判断会走 if 的分支而不是 else ：

![img](https://image.ldbmcs.com/9xlRfi.jpg)

这个方法就没啥意思了，主要是对存储过程有输出资源的一些处理，我们不关心，重要的是，`list` 这个变量有值了，`query` 方法的最底下就是返回 `list` 这个变量，所以第二次查询走一级缓存这个特性就得以体现了。

**1.2.3.4 清空一级缓存**

执行了两次 `findAll` 方法后，接下来要清空一级缓存了，还记得 query 方法一开始的源码吗：

```java
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, 
                         ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // ......
    // 如果statement指定了需要刷新缓存，则清空一级缓存
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache();
    }
    List<E> list;
    // ......
}
```

如果 statement 指定了需要刷新缓存，则一级缓存会被清空。而 `cleanCache` 对应的 mapper.xml 中就是指定了 `flushCache=true` ：

```xml
<select id="cleanCache" resultType="int" flushCache="true">
    select count(id) from tbl_department
</select>
```

那这里 Debug 的时候，借助 IDEA 可以发现此处返回 true ：

![img](https://image.ldbmcs.com/cMIjvy.jpg)

`clearLocalCache` 方法被执行，一级缓存也就清空了。

OK ，以上就是一级缓存的生效和工作原理了，整体来看比较简单易懂，小伙伴们可以跟着源码 Debug 走一下，体会一下一级缓存的设计。

## 2. 二级缓存

### 2.1 二级缓存的使用

#### 2.1.1 简单使用

最简单的使用方式，只需要在 mapper.xml 上打一个 `<cache />` 标签，就算开启二级缓存了：

```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="com.linkedbear.mybatis.mapper.DepartmentMapper">
    <cache />
    <!-- ...... -->
</mapper>
```

对应的 Mapper 接口，则需要用 `@CacheNamespace` 注解开启：

```java
@CacheNamespace
public interface DepartmentMapper {
    // ......
}
```

另外，不要忘记给实体类实现 `Serializable` 接口，否则二级缓存也是不能用的。

然后我们就可以编写测试代码了，这个测试代码完全可以基于之前的一级缓存测试代码扩展。我们知道，使用二级缓存时，必须关闭 `SqlSession` 时，一级缓存的数据才会写入二级缓存，所以此处我们需要在查询动作完成后，关闭 `sqlSession` ，并重新开启一个新的 `SqlSession` ：

```java
public class Level2Application {
    
    public static void main(String[] args) throws Exception {
        InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);
        SqlSession sqlSession = sqlSessionFactory.openSession();
    
        DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
        System.out.println("第一次执行findAll......");
        departmentMapper.findAll();
        System.out.println("第二次执行findAll......");
        departmentMapper.findAll();
        
        sqlSession.close();
    
        // 开启一个新的SqlSession，测试二级缓存
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        DepartmentMapper departmentMapper2 = sqlSession2.getMapper(DepartmentMapper.class);
        System.out.println("sqlSession2执行findAll......");
        departmentMapper2.findAll();
        
        sqlSession2.close();
    }
}
```

编写好测试代码后，运行 `main` 方法，观察控制台的 SQL 日志打印：

```
第一次执行findAll......
[main] DEBUG .mapper.DepartmentMapper  - Cache Hit Ratio [com.linkedbear.mybatis.mapper.DepartmentMapper]: 0.0 
[main] DEBUG ion.jdbc.JdbcTransaction  - Opening JDBC Connection 
[main] DEBUG .pooled.PooledDataSource  - Created connection 2061347276. 
[main] DEBUG ion.jdbc.JdbcTransaction  - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection] 
[main] DEBUG DepartmentMapper.findAll  - ==>  Preparing: select * from tbl_department 
[main] DEBUG DepartmentMapper.findAll  - ==> Parameters:  
[main] DEBUG DepartmentMapper.findAll  - <==      Total: 4 
第二次执行findAll......
[main] DEBUG .mapper.DepartmentMapper  - Cache Hit Ratio [com.linkedbear.mybatis.mapper.DepartmentMapper]: 0.0 
[main] DEBUG ion.jdbc.JdbcTransaction  - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection] 
[main] DEBUG ion.jdbc.JdbcTransaction  - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@7adda9cc] 
[main] DEBUG .pooled.PooledDataSource  - Returned connection 2061347276 to pool. 
sqlSession2执行findAll......
[main] DEBUG .mapper.DepartmentMapper  - Cache Hit Ratio [com.linkedbear.mybatis.mapper.DepartmentMapper]: 0.3333333333333333
```

`sqlSession` 第一次执行 `findAll` 方法时，由于一级缓存和二级缓存中都没有数据，所以需要查询数据库，查询到数据后第二次执行 `findAll` 方法时，一级缓存中已经存在数据，所以无需再查询数据库。关闭 `sqlSession` 时，可以发现控制台中有 `Closing JDBC Connection` 的字眼，此时一级缓存中的数据已经保存至二级缓存。另外开启 `sqlSession2` 时，再执行 `findAll` 方法，控制台甚至连 `Connection` 都懒得打开了，因为 MyBatis 发现二级缓存中有现成的数据了，于是直接取出，返回。

从这段过程中，我们可以总结出几个细节：

* `SqlSession` 关闭时，一级缓存的数据进入二级缓存。
* 二级缓存中有数据时，直接取出，不会预先开启 `Connection` （**按需加载**的思想）

#### 1.1.2 二级缓存的配置

默认的二级缓存开启，其实背后都有一些 MyBatis 帮我们设定好的默认值，我们可以通过修改这些配置，达到自定义本地二级缓存的目的。

修改的载体必然是这个 `<cache>` 标签（对应 Mapper 接口的则是 `@CacheNamespace` 注解的属性），它有不少属性，下面我们先看看它都可以配置什么。

| 属性            | 描述               | 备注                                                           |
| ------------- | ---------------- | ------------------------------------------------------------ |
| eviction      | 缓存的回收策略          | 默认 LRU                                                       |
| type          | 二级缓存的实现          | 默认 `org.apache.ibatis.cache.impl.PerpetualCache` ，即本地内存的二级缓存 |
| size          | 缓存引用数量           | 默认值 1024                                                     |
| flushInterval | 缓存刷新间隔（定时清除时间间隔） | 默认无，即没有刷新间隔                                                  |
| readOnly      | 缓存是否只读           | 默认 false ，需要二级缓存对应的实体模型类需要实现 `Serializable` 接口               |
| blocking      | 阻塞获取缓存数据         | 若缓存中找不到对应的 key ，是否会一直 blocking ，直到有对应的数据进入缓存。默认 false        |

属性不算多，但我们接触的不是很多，下面小册就一些比较重要的属性展开讲解一下。

**1.1.2.1 eviction**

缓存的回收策略，它可以配置当缓存容量即将溢出时如何回收空间。MyBatis 的官方文档中提到的可用的清除策略有：

* `LRU` – 最近最少使用：移除最长时间不被使用的对象。
* `FIFO` – 先进先出：按对象进入缓存的顺序来移除它们。
* `SOFT` – 软引用：基于垃圾回收器状态和软引用规则移除对象。
* `WEAK` – 弱引用：更积极地基于垃圾收集器状态和弱引用规则移除对象。

默认情况下我们不需要多余配置，默认的 LRU 策略已经足够用。

**1.1.2.2 type**

缓存的实现载体，上一章我们在了解缓存模型中就知道，MyBatis 的缓存根接口是 `Cache` ，那有接口就要有实现类，默认的实现类与一级缓存一样，都是 `**PerpetualCache**` ，如果我们需要使用外置第三方缓存件，那这个 `type` 属性就需要指定了（比方说整合 ehcache 的 `org.mybatis.caches.ehcache.EhcacheCache` ）。如何整合 EhCache ，我们下面马上就会讲到。

**1.1.2.3 readOnly**

缓存是否只读，这个设置比较有趣。还记得上一章讲过的一级缓存吗，我们当时测试了一个场景，如果第一次查出数据后，直接修改该数据，之后第二次查询时，从一级缓存中查出来的数据是被修改过的，并非数据库的真实数据，原因是 **MyBatis 利用一级缓存是直接将数据的引用交出去，至于我们怎么利用，MyBatis 不管**。

二级缓存就不一样了，我们从二级缓存中查出来的数据那可是跨 SqlSession 的，谁知道你改不改数据（还不敢保证改的对不对），万一你改了那别人从二级缓存中拿的数据就是被你改过的，这样万一出点问题，那可就出大事了。MyBatis 自然帮我们考虑到了这一点，于是它给二级缓存设计了一个只读属性。这个只读属性如果设置为 false ，则**通过二级缓存查询的数据会执行一次基于 jdk 序列化的对象深拷贝**，这样就**可以保证拿到的数据不会对原二级缓存产生影响**（但**一次对象的深拷贝会导致性能降低**）；而 `readOnly` 设置为 true ，则**只读的缓存会像一级缓存那样，直接返回二级缓存本身**，虽然**可能不安全**，但好在**处理速度快**。

由此也就解释了，为什么默认情况下，开启 MyBatis 的二级缓存，需要实体模型类实现 `Serializable` 接口。

#### 1.1.3 整合EhCache

OK ，下面我们来回顾一下 MyBatis 如何整合外置第三方缓存，比较流行的缓存是 EhCache ，最近几年也有 MyBatis 整合 Redis 做二级缓存的了，整合的逻辑都是一样的。下面我们以整合 EhCache 为例回顾。

MyBatis 的官方文档中也有对 EhCache 整合的说明：[mybatis.org/ehcache-cac…](https://link.juejin.cn/?target=http%3A%2F%2Fmybatis.org%2Fehcache-cache%2F) 。

**1.1.3.1 导入依赖**

既然整合 EhCache ，那 jar 包必然少不了了，MyBatis 本身已经提供了整合的 jar 包，所以直接拿过来用就可以：

```xml
    <dependency>
        <groupId>org.mybatis.caches</groupId>
        <artifactId>mybatis-ehcache</artifactId>
        <version>1.2.1</version>
    </dependency>
```

**1.1.3.2 配置EhCache**

接下来，我们需要配置一下 EhCache 了，EhCache 的配置，需要在 `src/main/resources` 中放一个 `ehcache.xml` 的配置文件：

```xml
<?xml version="1.0" encoding="UTF-8" ?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
    <!--
        磁盘存储:将缓存中暂时不使用的对象,转移到硬盘,类似于Windows系统的虚拟内存
        path:指定在硬盘上存储对象的路径
     -->
    <diskStore path="C:\ehcache"/>

    <!--
        defaultCache:默认的缓存配置信息,如果不加特殊说明,则所有对象按照此配置项处理
        maxElementsInMemory:设置内存缓存的上限，最多存储多少个记录对象
        maxElementsOnDisk:设置硬盘缓存的上限，内存放不下时会向硬盘中缓存(0表示无上限)
        eternal:代表对象是否永不过期
        timeToIdleSeconds:最大的空闲时间(秒)(对象在多长时间没有被访问就会失效)
        timeToLiveSeconds:最大的存活时间(秒)(对象从创建到失效所需要的时间)
        overflowToDisk:是否允许对象被写入到磁盘
        memoryStoreEvictionPolicy:缓存清空策略
            * FIFO:先进先出
            * LFU:最少使用的清空
            * LRU:最近最少使用(即未被使用的时间最长)
     -->
    <defaultCache
            maxElementsInMemory="100"
            maxElementsOnDisk="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="true"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU"
    />
</ehcache>
```

这个配置文件不需要我们全部写，直接抄过来就 OK 。相关的配置在上面也有注释，小伙伴们可以根据自己的需要合理配置。

还有一点，不要忘记，哪里需要用二级缓存，哪里就配置上 EhCache 的缓存实现：

```xml
<mapper namespace="com.linkedbear.mybatis.mapper.DepartmentMapper">
    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
    <!-- ...... -->
</mapper>
```

**1.1.3.3 测试效果**

不需要任何多余的编写测试代码，我们直接重新运行一遍 `Level2Application` 即可。运行结束后，我们观察一下 C 盘有没有多一个 ehcache 文件夹，以及里面有没有缓存文件生成：

![img](https://image.ldbmcs.com/hyCmen.jpg)

可以发现，数据已经成功的借助 EhCache 缓存到磁盘上了，说明 EhCache 整合成功。

到这里，对于 MyBatis 的二级缓存也就讲解完毕了，接下来，又到了大家最“头疼”的原理分析环节。

### 2.2 二级缓存的设计原理

首先我们先回顾一下二级缓存的模型设计，以及里面涉及到的装饰者模式。

#### 2.2.1 Cache实现类的装饰者模式

上一章我们提到了 Cache 接口的那些实现类：

![img](https://image.ldbmcs.com/3gIafE.jpg)

这里面只有 `PerpetualCache` 是真正有缓存能力的实现类，其余的都是装饰者。**装饰者**最大的特征，是**在原有的功能上扩展新的特性**，多种装饰者的组合，可以保证任意增加新的功能行为而不用修改原有的基本代码。

读到这里，小伙伴们肯定会产生一个新的疑问：\*\*MyBatis 怎么根据我们的二级缓存的配置，构造对应的缓存实现呢？\*\*哎这个问题到位了，下面我们要填之前的坑了。

#### 2.2.2 二级缓存的初始化位置

我们之前提到了 MyBatis 解析 mapper.xml 和 Mapper 接口时，会处理 `<cache>` 标签和 `@CacheNamespace` 注解，当时我们把这部分跳过了，本章我们就回过头来看看那部分源码的实现。

**2.2.2.1 mapper.xml中的cache解析**

还记得解析 mapper.xml 的位置起源吧，是解析 MyBatis 的全局配置文件，里面会解析 `<mapper>` 标签，从而触发 mapper.xml 的解析，而解析 mapper.xml 的逻辑中有如下两行源码：

```java
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
```

上面是处理 `<cache-ref>` 的，我们暂且不关心，下面的 `cacheElement` 方法是处理 `<cache>` 标签。

**2.2.2.2 解析cache标签**

进入 `cacheElement` 方法：（关键注释已标注在源码中）

```java
private void cacheElement(XNode context) {
    if (context != null) {
        // 默认的类型是PERPETUAL，也即PerpetualCache
        String type = context.getStringAttribute("type", "PERPETUAL");
        Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
        // 默认的过期策略 LRU
        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();
        // 2.2.3 创建Cache对象
        builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}
```

从源码中可以很明显的发现，缓存实现类、过期策略都可以声明别名的，说明它们的底层其实都是对应的某些实现类，而这些实现类，早在第 7 章中我们就看过了，可能部分小伙伴忘记了它的位置，小册提醒一下，是在 `BaseBuilder` 的构造方法中：

```java
public abstract class BaseBuilder {
    protected final Configuration configuration;
    protected final TypeAliasRegistry typeAliasRegistry;
    protected final TypeHandlerRegistry typeHandlerRegistry;

    public BaseBuilder(Configuration configuration) {
        this.configuration = configuration;
        // 注意这个typeAliasRegistry
        this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
        this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
    }
```

注意这个 `typeAliasRegistry` ，这里面注册了 MyBatis 默认的一些内置别名，其中就有如下这样一段：

```java
    typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
    typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
    typeAliasRegistry.registerAlias("LRU", LruCache.class);
    typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
    typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
```

可以发现，这些别名对应的都是 `Cache` 接口的实现类！由此也能在一定程度上透漏着装饰者的味道了吧。

OK ，我们的重点不在这里，更为重要的是关心最下面的 `builderAssistant.useNewCache` 方法。

**2.2.2.3 创建Cache对象**

又用到 `MapperBuilderAssistant` 了，这家伙真有点 “无所不能” 的意思啊，我们看看它如何创建出 `Cache` 对象的吧：

```java
public Cache useNewCache(Class<? extends Cache> typeClass, 
        Class<? extends Cache> evictionClass,
        Long flushInterval, Integer size, boolean readWrite,
        boolean blocking, Properties props) {
    Cache cache = new CacheBuilder(currentNamespace)
                          .implementation(valueOrDefault(typeClass, PerpetualCache.class))
                          .addDecorator(valueOrDefault(evictionClass, LruCache.class))
                          .clearInterval(flushInterval)
                          .size(size)
                          .readWrite(readWrite)
                          .blocking(blocking)
                          .properties(props)
                          .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
}
```

注意看！它使用了一个 `CacheBuilder` 创建的 `Cache` 对象！这很明显是**建造器**的设计。仔细观察一下这段链式调用，可以发现 `<cache>` 标签中的属性，在这里全部都用到了，先不点进去看，光在外头，应该各位会产生一种强烈的感觉：**每一行调用都有可能外挂一个装饰者**！到底是不是这样呢？我们分解来看。

**CacheBuilder的成员**

我们先看看 `CacheBuilder` 本身的设计：

```java
public class CacheBuilder {
    private final String id;
    private Class<? extends Cache> implementation;
    private final List<Class<? extends Cache>> decorators;
    private Integer size;
    private Long clearInterval;
    private boolean readWrite;
    private Properties properties;
    private boolean blocking;
}
```

可以发现，这里面也是包含了 `<cache>` 标签的所有必备要素，这里面两个小细节：

* `implementation` 属性对应的是 `Cache` 接口的落地实现，`decorators` 代表要外挂的装饰者们；
* `properties` 属性意味着 `<cache>` 标签也有 `<property>` 子标签，可以传入配置。

了解了基本设计，下面我们就挑上面重要的方法来看。

**implementation**

注意看 `implementation(valueOrDefault(typeClass, PerpetualCache.class))` 这行代码，它的入参是一个 `Class` 对象，而且带默认值 `PerpetualCache` ，这很明显是为了确定 `Cache` 接口的落地实现，在没有任何整合的前提下，MyBatis 肯定会用 `PerpetualCache` 作为落地实现。

**addDecorator**

看方法名，就差扒拉着耳朵告诉你它要**加装饰者**了，这个方法的实现，就是向 `decorators` 这个集合中添加装饰者实现：

```java
private final List<Class<? extends Cache>> decorators;

public CacheBuilder addDecorator(Class<? extends Cache> decorator) {
    if (decorator != null) {
        this.decorators.add(decorator);
    }
    return this;
}
```

不过默认情况下，它只会添加一个 `LruCache` 的实现，难不成它意味着默认创建出来的缓存只有一层装饰者吗？带着这个疑问，我们继续往下看。

**readWrite**

剩下几个方法中，小册选了一个比较简单且有代表性的方法来看：

```java
public CacheBuilder readWrite(boolean readWrite) {
    this.readWrite = readWrite;
    return this;
}
```

看上去它只是把 “缓存是否只读” 记录到 `CacheBuilder` 中而已，没啥别的意思？但实际上不是这么简单（有伏笔），我们继续往下看。

**build**

最下面的动作是构建二级缓存了：（关键注释已标注在源码中）

```java
public Cache build() {
    // 兜底处理
    setDefaultImplementations();
    // 创建默认的PerpetualCache对象
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
    // 如果是PerpetualCache类，则用装饰者逐层包装
    if (PerpetualCache.class.equals(cache.getClass())) {
        for (Class<? extends Cache> decorator : decorators) {
            cache = newCacheDecoratorInstance(decorator, cache);
            setCacheProperties(cache);
        }
        // 2.2.3.5 包装完毕后，处理MyBatis的标准装饰者
        cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
        cache = new LoggingCache(cache);
    }
    return cache;
}
```

纵读整段源码，其实都不算难理解了，装饰者模式在此体现得淋漓尽致。

源码的中间偏下位置，在所有传入的装饰者都包装完成后，还有一个 `setStandardDecorators` 方法，它就是前面提到的伏笔了。

**setStandardDecorators**

进入 `setStandardDecorators` 方法中，我们在此又发现了好多装饰者：（注释已标全）

```java
private Cache setStandardDecorators(Cache cache) {
    try {
        MetaObject metaCache = SystemMetaObject.forObject(cache);
        // 缓存大小
        if (size != null && metaCache.hasSetter("size")) {
            metaCache.setValue("size", size);
        }
        // 定时清空二级缓存
        if (clearInterval != null) {
            cache = new ScheduledCache(cache);
            ((ScheduledCache) cache).setClearInterval(clearInterval);
        }
        // 读写缓存
        if (readWrite) {
            cache = new SerializedCache(cache);
        }
        // 外挂日志记录、同步缓存
        cache = new LoggingCache(cache);
        cache = new SynchronizedCache(cache);
        // 阻塞读取缓存
        if (blocking) {
            cache = new BlockingCache(cache);
        }
        return cache;
    } catch (Exception e) {
        throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
}
```

确实是伏笔的回应吧，上面记录的这些属性，在下面都有对应的装饰者 `Cache` ，所以最终这些配置都是以装饰者的身份，包装到最底层的 `PerpetualCache` 上了。

经过这一系列逻辑处理后，`Cache` 对象也就成功的创建了，二级缓存也就初始化完成了。

#### 2.2.3 二级缓存的生效原理

接下来是二级缓存的生效机制探究了，我们还是以 1.1 节的最简单的二级缓存使用作为测试代码，调试观察二级缓存的生效过程。

**2.2.3.1 准备断点**

跟上一章一样，我们先准备断点。这次我们把断点打在 `org.apache.ibatis.executor.CachingExecutor` 的第 96 行，即 `query` 方法的第 2 行：

```java
private final TransactionalCacheManager tcm = new TransactionalCacheManager();

public <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);
            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);
}
```

我们可以顺便看一下这个 `query` 方法的大体逻辑。一上来它会先去尝试获取二级缓存，如果没有二级缓存，则直接执行查询（跳到 `BaseExecutor` 中了）；如果有二级缓存，则会尝试从 `TransactionalCacheManager` 中拿着二级缓存和缓存的 key 取查询数据，如果获取到了，则直接返回，没有获取到，则查询到结果后放入二级缓存中。

**2.2.3.2 第一次进入断点**

之后我们就可以以 Debug 的方式运行 `Level2Application` 了，当程序停在断点时，我们观察二级缓存是如何生效和工作的。

断点落下，此时可以发现 `cache` 是有值的，而且确实是按照上面构建的方式一步一步套出来的：

![img](https://image.ldbmcs.com/w0jwNk.jpg)

然后，它要使用 `tcm.getObject(cache, key)` 方法，从二级缓存中取数据，显然刚开始运行，二级缓存是空的，所以必然返回的 list 为 null ：

![img](https://image.ldbmcs.com/1lO9Lh.jpg)

既然没有数据，那就查数据库咯，查询完成后使用 `tcm.putObject(cache, key, list);` 放入二级缓存。

不过请注意，这个 `putObject` 方法是**不会直接放入二级缓存**的，我们可以通过 Debug 的断点处查看数据：

![img](https://image.ldbmcs.com/vzOySu.jpg)

为什么缓存为空呢？我们要看一下 `putObject` 方法的实现：

```java
// TransactionalCacheManager
public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
}

// TransactionCache
public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
}
```

注意最下面它 `put` 的那个集合变量名：`entriesToAddOnCommit` ，**OnCommit** ？？？合着要等到事务提交咯？那当然啦，我们在学习 MyBatis 二级缓存的时候就知道，MyBatis 的二级缓存**需要在** `**SqlSession**` **关闭时，一级缓存中的数据才能写入二级缓存**，这里当然不能直接存进去。

这样经过一轮查询后，`SqlSession` 的一级缓存中就已经有数据了，第二次再进入断点时依然是上面的流程，不再重复。

**2.2.3.3 SqlSession关闭**

`SqlSession` 关闭时，一级缓存的数据要写入二级缓存，此时会触发 `Executor` 的关闭，我们找到 `CachingExecutor` 的 `close` 方法：（可能会有小伙伴不理解为什么又找到 `Executor` 而不是 `SqlSession` ，我们后面放到生命周期部分再详细讲解）

```java
public void close(boolean forceRollback) {
    try {
        // issues #499, #524 and #573
        if (forceRollback) {
            tcm.rollback();
        } else {
            tcm.commit();
        }
    } finally {
        delegate.close(forceRollback);
    }
}
```

注意看，它又调用了 `TransactionalCacheManager` 的 `commit` 方法。看到这里可能有部分小伙伴的脑子里问号越来越多了：**搞缓存就搞缓存嘛，为啥非要扯上事务呢？**

**TransactionalCacheManager的设计缘由**

仔细思考一下，二级缓存是跨 `SqlSession` 的，也就是跨 `Connection` 的，那既然是跨连接，就必须要考虑到事务了，否则会出现一些意外情况，比方说小册来举个例子：

![img](https://image.ldbmcs.com/37oCbG.jpg)

如果二级缓存的存放不需要考虑事务的话，那就有可能出现上面的问题：`sqlSession1` 先更新数据，后查询全部，此时查询出来的数据是**修改后的脏数据**，就这样直接放入二级缓存了，但随后 `sqlSession1` 执行了 **rollback** ，撤消了修改的数据，但**数据库里的数据可以撤销修改，但二级缓存没办法撤销**呀，这样就造成了隐患。`sqlSession1` 关闭后，重新开启一个新的 `SqlSession` ，并直接查询数据，此时**二级缓存中有被修改过的错误数据**，但 `sqlSession2` 并不知情，导致就这么把错误数据取出来了，从而引发错误。

由此我们就应该清楚，二级缓存应该是**基于事务提交**的，**只有事务提交后，数据库的数据确定没有问题，这个时候** `**SqlSession**` **中的一级缓存数据也是准确的，这样才能把一级缓存的数据写入到二级缓存中**，这也就是 `TransactionalCacheManager` 设计的意义。

**commit的动作**

搞明白 `TransactionalCacheManager` 的良苦用心，那我们就看看 `commit` 的动作中都干了什么吧：

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

呦，这是把它里面的所有缓存都取出来，挨个 `commit` 呀，刚才上面我们也看到了，`TransactionalCache` 里面有那个 `entriesToAddOnCommit` 集合，那是不是 `commit` 了之后，相应的这些集合的数据也就都写入到二级缓存呢？答案是肯定的，进入到 `TransactionalCache` 中，源码的逻辑非常简单：

```java
public void commit() {
    if (clearOnCommit) {
        delegate.clear();
    }
    // 刷新等待写入的缓存
    flushPendingEntries();
    reset();
}

private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
        // 写入最底层的缓存中
        delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
        if (!entriesToAddOnCommit.containsKey(entry)) {
            delegate.putObject(entry, null);
        }
    }
}
```

可以发现，它还是一层装饰者，最里层的 `delegate` 肯定还是 `PerpetualCache` ，`commit` 的动作就是将 `entriesToAddOnCommit` 中的数据写入最内层的二级缓存中。

相应的，如果是回滚的话，只需要把这些集合全部清空即可：

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

private void reset() {
    clearOnCommit = false;
    // 直接清空所有要写入的缓存
    entriesToAddOnCommit.clear();
    entriesMissedInCache.clear();
}
```

经过这样一番操作之后，一级缓存的数据就写入到二级缓存中了。

**2.2.3.4 第三次进入断点**

第三次进入断点，此时 `sqlSession` 已经关闭，`sqlSession2` 开启后进入。此时只凭观察 Cache 对象的属性，就已经知道二级缓存真实的存在了：

![img](https://image.ldbmcs.com/DslgdZ.jpg)

注意看 `HashMap` 中的 value ，是一个字节数组，这也就说明了**二级缓存在写入时已经执行了一次基于 jdk 的序列化动作**，**每次从二级缓存取数据时，会再执行一次反序列化，将字节数组转为缓存数据对象**。

至此，我们也就对整个二级缓存的生效原理也研究明白了。
