# MyBatis  缓存

## MyBatis 一级缓存

MyBatis 一级缓存默认开启，其作用域是每一个 SqlSession 对象，每一个 SqlSession 对象的查询结果都会被放入缓存，当这个 SqlSession 对象以后再次查询时就能直接从缓存中读取。特别的是当执行完 sqlSession.commit()操作以后，一级缓存会被清空。

在 MyBatis\src\test\java\MyBatisTest.java 添加下面这个方法：

```java
    //一级缓存
    @Test
    public void testLv1Cache() throws Exception{
        SqlSession sqlSession=null;
        try {
            sqlSession=MyBatisUtils.openSession();
            Goods goods1=sqlSession.selectOne("goods.selectById",1050);
            Goods goods2=sqlSession.selectOne("goods.selectById",1050);
            System.out.println("goods1.hashCode : "+goods1.hashCode());
            System.out.println("goods2.hashCode : "+goods2.hashCode());
        }catch (Exception e){
            e.printStackTrace();
            throw e;
        }finally {
            MyBatisUtils.closeSession(sqlSession);
        }

        try {
            sqlSession=MyBatisUtils.openSession();
            Goods goods1=sqlSession.selectOne("goods.selectById",1050);
            Goods goods2=sqlSession.selectOne("goods.selectById",1050);
            System.out.println("goods1.hashCode : "+goods1.hashCode());
            System.out.println("goods2.hashCode : "+goods2.hashCode());
        }catch (Exception e){
            e.printStackTrace();
            throw e;
        }finally {
            MyBatisUtils.closeSession(sqlSession);
        }

        try {
            sqlSession=MyBatisUtils.openSession();
            Goods goods1=sqlSession.selectOne("goods.selectById",1050);
            sqlSession.commit();
            Goods goods2=sqlSession.selectOne("goods.selectById",1050);
            System.out.println("goods1.hashCode : "+goods1.hashCode());
            System.out.println("goods2.hashCode : "+goods2.hashCode());
        }catch (Exception e){
            e.printStackTrace();
            throw e;
        }finally {
            MyBatisUtils.closeSession(sqlSession);
        }
    }

```

这个方法尝试用一个 SqlSession 对象查询两次 商品ID为1050 的商品。随后关闭这个 SqlSession, 再次查询两次 商品ID为1050 的商品。接着，再开一个新的 SqlSession, 查询一次商品ID为1050的商品后，执行 commit() 方法 再查询一次商品ID为1050的商品。每一次查询后，都打印查询对象的 hashCode, 若hashCode 一样，则认为是同一个对象。

运行结果:

```
10:06:10:348 [main]  DEBUG goods.selectById -==>  Preparing: select * from t_goods WHERE goods_id=?  
10:06:10:382 [main]  DEBUG goods.selectById -==> Parameters: 1050(Integer) 
10:06:10:401 [main]  DEBUG goods.selectById -<==      Total: 1 
goods1.hashCode : 1383524016
goods2.hashCode : 1383524016
10:06:10:402 [main]  DEBUG o.a.i.t.jdbc.JdbcTransaction -Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@38c5cc4c] 
10:06:10:403 [main]  DEBUG o.a.i.t.jdbc.JdbcTransaction -Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@38c5cc4c] 
10:06:10:403 [main]  DEBUG o.a.i.d.pooled.PooledDataSource -Returned connection 952486988 to pool. 
10:06:10:403 [main]  DEBUG o.a.i.t.jdbc.JdbcTransaction -Opening JDBC Connection 
10:06:10:403 [main]  DEBUG o.a.i.d.pooled.PooledDataSource -Checked out connection 952486988 from pool. 
10:06:10:403 [main]  DEBUG o.a.i.t.jdbc.JdbcTransaction -Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@38c5cc4c] 
10:06:10:404 [main]  DEBUG goods.selectById -==>  Preparing: select * from t_goods WHERE goods_id=?  
10:06:10:404 [main]  DEBUG goods.selectById -==> Parameters: 1050(Integer) 
10:06:10:405 [main]  DEBUG goods.selectById -<==      Total: 1 
goods1.hashCode : 1637061418
goods2.hashCode : 1637061418
10:06:10:406 [main]  DEBUG o.a.i.t.jdbc.JdbcTransaction -Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@38c5cc4c] 
10:06:10:406 [main]  DEBUG o.a.i.t.jdbc.JdbcTransaction -Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@38c5cc4c] 
10:06:10:406 [main]  DEBUG o.a.i.d.pooled.PooledDataSource -Returned connection 952486988 to pool. 
10:06:10:406 [main]  DEBUG o.a.i.t.jdbc.JdbcTransaction -Opening JDBC Connection 
10:06:10:407 [main]  DEBUG o.a.i.d.pooled.PooledDataSource -Checked out connection 952486988 from pool. 
10:06:10:407 [main]  DEBUG o.a.i.t.jdbc.JdbcTransaction -Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@38c5cc4c] 
10:06:10:407 [main]  DEBUG goods.selectById -==>  Preparing: select * from t_goods WHERE goods_id=?  
10:06:10:407 [main]  DEBUG goods.selectById -==> Parameters: 1050(Integer) 
10:06:10:409 [main]  DEBUG goods.selectById -<==      Total: 1 
10:06:10:409 [main]  DEBUG goods.selectById -==>  Preparing: select * from t_goods WHERE goods_id=?  
10:06:10:409 [main]  DEBUG goods.selectById -==> Parameters: 1050(Integer) 
10:06:10:411 [main]  DEBUG goods.selectById -<==      Total: 1 
goods1.hashCode : 2024453272
goods2.hashCode : 98394724
```

运行结果和我们预想的一样，每一个 SqlSession 查询一次都会先去缓存里面查找，如果缓存里面存在则直接从缓存里面取出来。而缓存又是和每一个 SqlSession 相捆绑的。这就是为什么第一个 SqlSession，第二个 SqlSession 查询都只执行了一次 SELECT 语句，且每一个SqlSession查询同样的商品ID都返回的是同一个对象。而每当SqlSession.commit() 以后就会清空缓存，这就是为什么第三个SqlSession 执行了两次 SELECT 语句而且查询出来的对象hashCode 不同。

## MyBatis 二级缓存

MyBatis 二级缓存可以作用于多个 SqlSession 对象之间，起作用范围是同一个 namespace。在MyBatis\src\main\resources\mappers\goods.xml 下的  \<mapper namespace="goods"> 添加 \<cache> 标签。

```
    <!--    开启二级缓存-->
    <cache eviction="LRU" flushInterval="600000" size="512" readOnly="true" />
    <!--
    eviction 清除策略, 和页面置换算法类似, 当缓存对象达到上限后, 自动触发清除
        1.LRU 清除最近最久未使用的对象, 默认
        2.LFU, 清除最近访问频率最低的对象
        3.FIFO 先进先出, 清除最先进来的对象
        4.SOFT 软引用, 清除基于 垃圾回收期状态和软引用规则的对象, 不推荐
        5.WEAK 弱引用, 更积极地清除基于 垃圾回收期状态和软引用规则的对象, 不推荐

    flushInterval 每隔多少毫秒清除缓存
    size 缓存多少个对象或者集合(一个集合算一个对象)
    readOnly 设置为 true, 代表返回只读缓存, 每次从缓存对象本身, 这种执行效率较高
             设置为false, 可读可写, 代表每次取出的是缓存对象的"副本", 每一次取出的对象是不同的,安全性较高
    -->
```

在 MyBatis\src\test\java\MyBatisTest.java 添加下面这个方法：

```java
    //二级缓存
    @Test
    public void testLv2Cache() throws Exception{
        SqlSession sqlSession=null;
        try {
            sqlSession=MyBatisUtils.openSession();
            Goods goods1=sqlSession.selectOne("goods.selectById",1050);
            Goods goods2=sqlSession.selectOne("goods.selectById",1050);
            System.out.println("goods1.hashCode : "+goods1.hashCode());
            System.out.println("goods2.hashCode : "+goods2.hashCode());
        }catch (Exception e){
            e.printStackTrace();
            throw e;
        }finally {
            MyBatisUtils.closeSession(sqlSession);
        }

        try {
            sqlSession=MyBatisUtils.openSession();
            Goods goods1=sqlSession.selectOne("goods.selectById",1050);
            Goods goods2=sqlSession.selectOne("goods.selectById",1050);
            System.out.println("goods1.hashCode : "+goods1.hashCode());
            System.out.println("goods2.hashCode : "+goods2.hashCode());
        }catch (Exception e){
            e.printStackTrace();
            throw e;
        }finally {
            MyBatisUtils.closeSession(sqlSession);
        }
    }
```

这个方法也很简单，就是开启二级缓存后，在两个不同的 SqlSession 内两次查询同一个商品。

运行结果:&#x20;

```
10:58:33:377 [main]  DEBUG goods.selectById -==>  Preparing: select * from t_goods WHERE goods_id=?  
10:58:33:417 [main]  DEBUG goods.selectById -==> Parameters: 1050(Integer) 
10:58:33:436 [main]  DEBUG goods.selectById -<==      Total: 1 
10:58:33:437 [main]  DEBUG goods -Cache Hit Ratio [goods]: 0.0 
goods1.hashCode : 2130772866
goods2.hashCode : 2130772866
10:58:33:437 [main]  DEBUG o.a.i.t.jdbc.JdbcTransaction -Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@65d6b83b] 
10:58:33:438 [main]  DEBUG o.a.i.t.jdbc.JdbcTransaction -Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@65d6b83b] 
10:58:33:438 [main]  DEBUG o.a.i.d.pooled.PooledDataSource -Returned connection 1708570683 to pool. 
10:58:33:438 [main]  DEBUG goods -Cache Hit Ratio [goods]: 0.3333333333333333 
10:58:33:438 [main]  DEBUG goods -Cache Hit Ratio [goods]: 0.5 
goods1.hashCode : 2130772866
goods2.hashCode : 2130772866
```

运行结果也符合预期，只执行了一次 SELECT 语句，而且两次查询出来的是相同的对象。特别的是在日志中打印输出了 Cache Hit Ratio\[goods] 这表示在 goods 命名空间下直接从缓存中取出数据占缓存数据的比例，比例越大，说明缓存的使用率越高。

#### 二级缓存使用场景：

二级缓存一旦开启就默认整个 namespace 生效。但某些操作并不适合使用二级缓存，比如 查询出来的的结果很大，比如 SELECTALL, 这时候则需要使用 useCache="false" 参数。

```xml
<!-- 如果不用LIMIT, 对于查询结果的数量比较大, 返回的是一个list的, 一般不用二级缓存, 因为这样命中率不高-->
<select id="selectAll" resultType="indi.chester.mybatis.entity.Goods" useCache="false">
    select * from  t_goods ORDER BY  goods_id DESC LIMIT 10
</selectx
```

某些情况下，需要执行完操作立即清空缓存。这时候则需要 flushCache="true" 参数。

```xml
<!-- 希望执行完这一句之后立刻清空缓存, 而不是等待commit 或者关闭 SqlSession 对象-->
<insert id="insertflush" parameterType="indi.chester.mybatis.entity.Goods" flushCache="true">
    INSERT INTO t_goods(title, sub_title, original_cost, current_price, discount, is_free_delivery,category_id)
    VALUES (#{title}, #{subTitle}, #{originalCost}, #{currentPrice}, #{discount}, #{isFreeDelivery}, #{categoryId})
</insert>
```
