Mybatis的缓存机制

使用

作为目前最常用的ORM框架之一,mybatis同样拥有提高查询效率的缓存机制。同样,它的缓存使用起来也非常简单,只需在Mapper.xml文件下加入如下配置即可。默认情况下,缓存是开启的

<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
               size="1024"
               eviction="LRU"
               flushInterval="120000"
               readOnly="false"/>

结构

接着,我们开看一下mybatis内在的缓存体系
在这里插入图片描述
mybatis跟缓存相关的类都在cache 包里面,有一个Cache 接口,并且有一个默认的基本实现类 PerpetualCache,只要你在mybaits中使用缓存,就会创建这个类

public class PerpetualCache implements Cache {

  private final String id;

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

缓存使用map结构实现
除开最基本的实现类,mybatis还使用了装饰器模式进行扩展,即decorators包下的所有实现类。接下来,我们来对所有的装饰类来简单的分一下类:
按照颜色一次分为:基本缓存、有淘汰机制的缓存、特殊功能的缓存
在这里插入图片描述

分类

按照工作范围分类,mybatis缓存可分为一级缓存、二级缓存

一级缓存(local cache)

一级缓存也叫本地缓存,它的作用域是session,也就是sqlSession。也就是说当你的查询使用的是同一个sqlSession对象时,当你的sql语句和之前的是一样的时候,它就会去缓存里拿数据。一级缓存是默认开启的
接着,我们思考一个问题:既然一级缓存的所用域是sqlSession,那么PerpetualCache 应该放在哪里去进行管理呢?
猜测:既然是基于sqlSession层面的,我们干脆把缓存放到sqlSession对象里
验证:我们去看一下mybatis对sqlSession对象的封装

DefaultSqlSession .java

public class DefaultSqlSession implements SqlSession {

  private final Configuration configuration;
  private final Executor executor;
  ...
 }

在DefaultSqlSession里面没有直接放一个缓存对象,只有Configuration 和Executor 对象,其中Configuration 对象是配置文件的所有数据,是全局的,且唯一的,那一级缓存放在里面显然是不合理的,那排除掉一个,一级缓存就只能放在Executor 对象里了
Executor 是一个interface接口,我们看一下它的基本实现类

BaseExecutor .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;
	...
}

发现里面果然有PerpetualCache 对象,因为一个sqlSession就有一个Executor 。从而实现了同一个会话共享一个一级缓存,如下图:
在这里插入图片描述
接下来,我们简单写个测试类来印证一下

public void testCache() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session1 = sqlSessionFactory.openSession();
        SqlSession session2 = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper0 = session1.getMapper(BlogMapper.class);
            BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
            Blog blog = mapper0.selectBlogById(1);
            System.out.println(blog);
            System.out.println("第二次查询,相同会话,获取到缓存了吗?");
            System.out.println(mapper1.selectBlogById(1));
        } finally {
            session1.close();
        }
    }

注意:在测试一级缓存的时候,需要把二级缓存关掉,因为默认是打开的,同时打开打印sql语句的开关,待会可以通过sql语句是否打印来判断是取的缓存还是到数据库去拿的数据

mybatis-config.xml

<!-- 打印查询语句 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
 <!-- 控制全局缓存(二级缓存),默认 true-->
<setting name="cacheEnabled" value="false"/>

运行,查询结果如下:
在这里插入图片描述
没看到,第一次查询是操作数据库去拿,而第二次去哪的时候明显是从缓存里去拿的,这是同一个sqlSession执行的结果,那不是同一个sqlSqssion呢?

public void testCache() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session1 = sqlSessionFactory.openSession();
        SqlSession session2 = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper0 = session1.getMapper(BlogMapper.class);
            Blog blog = mapper0.selectBlogById(1);
            System.out.println(blog);

            System.out.println("第二次查询,不同会话,获取到缓存了吗?");
            BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
            System.out.println(mapper2.selectBlogById(1));

        } finally {
            session1.close();
        }
    }

运行结果:
在这里插入图片描述
结果很明显看到,查询了两次数据库。
那什么时候一级缓存会清空呢?用过缓存组件的都知道,当值发生了变换,就会导致缓存被删掉,我们用代码测试一下:

public void testCacheInvalid() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper = session.getMapper(BlogMapper.class);
            System.out.println(mapper.selectBlogById(1));

            Blog blog = new Blog();
            blog.setBid(1);
            blog.setName("after modified 666");
            mapper.updateByPrimaryKey(blog);
            session.commit();

            // 相同会话执行了更新操作,缓存是否被清空?
            System.out.println("在[同一个会话]执行更新操作之后,是否命中缓存?");
            System.out.println(mapper.selectBlogById(1));

        } finally {
            session.close();
        }
    }

运行结果:
在这里插入图片描述
很明显,在经过更新操作后,同一个sqlSqssion下的同一条sql语句又去执行了数据库操作。
接下来思考一个问题:如果我们的应用是跨会话,那我在第二个会话里update数据之后,第一个会话查询的是缓存数据呢,还是到数据库里拿最新的数据呢?
同样,我们用代码测试一下:

public void testDirtyRead() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession session1 = sqlSessionFactory.openSession();
        SqlSession session2 = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
            System.out.println(mapper1.selectBlogById(1));
            // 会话2更新了数据,会话2的一级缓存更新
            Blog blog = new Blog();
            blog.setBid(1);
            blog.setName("after modified 333333333333333333");
            BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
            mapper2.updateByPrimaryKey(blog);
            session2.commit();

            // 其他会话更新了数据,本会话的一级缓存还在么?
            System.out.println("会话1查到最新的数据了吗?");
            System.out.println(mapper1.selectBlogById(1));
        } finally {
            session1.close();
            session2.close();
        }
    }

运行结果:
在这里插入图片描述
很明显,第一个会话还是拿的缓存的数据,也就是过时的数据,这个就出现了问题,因为一级缓存的作用域太小导致的,解决的办法就是二级缓存

二级缓存

二级缓存的作用是namespace,也就是命名空间。只要是同一个Mapper.xml的操作,就能读取到二级缓存。如果同时打开一级缓存,二级缓存,会先去二级缓存去拿取数据,如果拿到直接返回
一级缓存是Sqlsession级别的,所以放到DefaultSqlSession对象里面,而二级缓存是namespace级别的,那它应该放到哪里去管理呢?
猜测:因为二级缓存的作用域是namespace,那肯定是在sqlSession外面,同时,在一级缓存二级缓存都开启的情况下,会优先读取二级缓存,说明,二级缓存肯定在一级缓存外面,是不是有可能使用装饰器模式在Executor外面又装饰一层呢?
验证:我们去看一下Executor的装饰类

CachingExecutor.java

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    // cache 对象是在哪里创建的?  XMLMapperBuilder类 xmlconfigurationElement()
    // 由 <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) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // 写入二级缓存
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    // 走到 SimpleExecutor | ReuseExecutor | BatchExecutor
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

果然找到了CachingExecutor装饰类,查询时会先到二级缓存里去拿,如果没有,再接着往下查询,并将查询结果写入到二级缓存,如下图:
在这里插入图片描述

使用方法
  1. 在mybaits-config.xml中配置,默认是打开的,可以不用管,如果需要关掉,这需要配置cacheEnabled的值为false

    <setting name="cacheEnabled" value="true"/>
    
  2. 在Mapper.xml文件中加上< cache />标签

    <cache type="org.apache.ibatis.cache.impl.PerpetualCache"
                   size="1024"
                   eviction="LRU"
                   flushInterval="120000"
                   readOnly="false"/>
    

ps:如果你想单独关掉一个sql语句的二级缓存,可以在Mapper.xml对应的< select />里加上 userCache=false

印证

接下来,我们通过代码去测试一下二级缓存的作用

public void testCache() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session1 = sqlSessionFactory.openSession();
        SqlSession session2 = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
            System.out.println(mapper1.selectBlogById(1));
            System.out.println("第二次查询");
            BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
            System.out.println(mapper2.selectBlogById(1));
        } finally {
            session1.close();
        }
    }

运行结果:
在这里插入图片描述
发现,跨会话时,同样是查询了两次数据库,并没有用到二级缓存,这是为什么呢?我们看之前CachingExecutor装饰类中的代码,发现二级缓存都是放到事务里面去管理的,也就是说当你没有commit时,他是不会存放到二级缓存的
我们加上commit再测试一下:

public void testCache() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session1 = sqlSessionFactory.openSession();
        SqlSession session2 = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
            System.out.println(mapper1.selectBlogById(1));
            // 事务不提交的情况下,二级缓存会写入吗?
            session1.commit();

            System.out.println("第二次查询");
            BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
            System.out.println(mapper2.selectBlogById(1));
        } finally {
            session1.close();
        }
    }

运行结果:
在这里插入图片描述
结果发现,果然二级缓存和事务有关
为什么二级缓存和事务有关,而一级缓存没有呢?
因为怕回滚,当你有两个回话的时候,你马上写入缓存,当时当commit是出现异常, 就不会提交,从而导致数据库与缓存数据不一致的问题
而一级缓存作用域是session,也就是回话,当回话挂掉后,一级缓存随即清空,因为回滚时,事务发生异常时,回话也就随机挂掉,导致一级缓存就没有了

总结

一级缓存
  • 作用域:session
  • 开启方式:默认开启,如果需要关系,在mybatis-config.xml文件中< setting name=“localCacheScope” value=“STATEMENT”/>
  • 不足:作用域太小,导致不同的会话会出现过时的数据
二级缓存
  • 作用域:namespace
  • 开启方式:需要同时在mybatis-config.xml和Mapper.xml文件中配置,如果需要某一条sql不使用二级缓存,则在对应的< select />上设置userCache=false
  • 特点:和事务相关
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值