前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MyBatis 缓存(5)

MyBatis 缓存(5)

作者头像
兜兜毛毛
发布2021-04-19 16:19:20
4890
发布2021-04-19 16:19:20
举报
文章被收录于专栏:兜兜毛毛兜兜毛毛

MyBatis有必要使用缓存吗?为什么?

一般的ORM框架都会提供缓存功能来提升查询效率、减少数据库的压力。跟Hibernate一样,Mybatis也有一级缓存、二级缓存,并预留了集成第三方的缓存接口。

在Mybatis中,与缓存相关的类都在cache包中,其中有一个Cache接口,只有一个默认的实现类PerpetualCache,它是用HashMap实现的。

PerpetualCache这个对象是一定会创建的,所以是基础缓存。但是缓存又可以有很多额外的功能,比如回收策略、日志记录、定时刷新等等,如果需要的话,就可以给基础缓存加上这些功能。

除了基础缓存之外,MyBatis也定义了很多装饰器,同样实现了Cache接口,通过这些装饰器可以额外实现很多功能。

所有缓存可以分为三大类:基本缓存、淘汰算法缓存、装饰器缓存。

类型

缓存实现类

描述

作用

装饰条件

基本缓存

PerpetualCache

缓存基本实现类

默认是PeretualCache也可以自定义比如RedisCache、EhCache等,具备基本能功能的缓存

淘汰算法缓存

LruCache

LRU淘汰策略的缓存

当缓存达到上限时,删除最少使用的缓存(Laste Recently Use)

eviction="LRU"(默认)

FifoCache

FIFO策略缓存

当缓存达到上限时,删除最先入队的缓存

evication="FIFO"

SoftCache \ WeakCache

带清理策略的缓存

通过JVM的软引用和弱引用来实现缓存,当JVM内存不足时,会自动清理这些缓存。

evication="SOFT" \ evication="WEAK"

装饰器缓存

LoggingCache

带日志功能的缓存

如可以输出缓存命中率

基本

SynchronnizedCache

同步缓存

基于synchronized关键字实现,解决并发问题

基本

BlockingCache

阻塞缓存

通过在get/put方式中加锁,保证只有一个线程操作缓存,基于java重入锁实现

bloking=true

SerializedCache

支持序列化的缓存

将对象序列化以后存到缓存中,取出时反序列化

readOnly=false(默认)

ScheduledCache

定时调度的缓存

在进行get 、put、remove、getSize等操作前,判断缓存时间是否超过了设置的最长缓存时间(默认一小时),如果是则清空缓存——每隔一段时间清空一次缓存

flushInterval不为空

TransactionnalCache

事务缓存

在二级缓存中使用,可一次存入多个缓存,移除多个缓存

在TransactionalCacheManager中用Map维护对应关系

一级缓存

一级缓存也叫本地缓存(Local Cache),MyBatis的一级缓存是在绘画(SqlSession)层进行缓存的。MyBatis一缓存默认是开启的,不需要任何配置(localCacheScope=STATEMENT相当于关闭一级缓存)。

可以在BaseExecutor的query()方法中找到localCacheScope清除逻辑:

代码语言:javascript
复制
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
    // issue #482
    clearLocalCache();
}

在MyBatis执行流程里面,涉及到这么多对象,那么缓存PerpetualCache应该放在哪个对象里面去呢?

如果要在同一个会话里共享一级缓存,最好的办法是在SqlSession里创建的,作为SqlSession的一个属性,跟SqlSession生命周期相同,这样就不需要为SqlSession编号、再根据编号查找对应缓存了。

DefaultSqlSession里只有两个对象属性:Configuration和Executor。

Configuration是全局的,不属于SqlSession,所以缓存只可能放在Executor里,因为Executor是一个接口,实际上他是在基本执行器中(SimpleExecutor\ReuseExecutor\BatchExecutor的父类BaseExecutor的构造函数中持有了PerpetualCache)。

在同一个会话里,多次执行相同SQL语句,会直接从内存取到缓存的结果,不要再发送SQL到数据库。但在不同的会话里,即使执行的SQL一样,也不能使用一级缓存(因为跨了Session)。

下边来通过实际例子来验证一级缓存,并通过分析源码了解实现原理。

代码语言:javascript
复制
    /**
     * 测试一级缓存作用域,是在SqlSession -> Executor -> localCache 中存储
     */
    @Test
    public void testFirestCache(){
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();

        try {
            //使用相同sqlsession执行查询方法,只打印了1次查询语句
            UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
            UserMapper userMapper2 = sqlSession1.getMapper(UserMapper.class);

            User user1 = userMapper1.byId(1L);
            User user2 = userMapper2.byId(1L);

            LoggerUtil.printThread("相同Session第1个结果:" + user1.toString());
            LoggerUtil.printThread("相同Session第2个结果:" + user2.toString());

            LoggerUtil.split();

            //使用不同session对象执行查询,会打印出2次查询语句
            UserMapper userMapper3 = sqlSession2.getMapper(UserMapper.class);
            User user3 = userMapper3.byId(1L);

            LoggerUtil.printThread("不同Session结果:" + user3.toString());
        }finally {
            sqlSession1.close();
            sqlSession2.close();
        }
    }

执行结果:

通过上边的测试代码可以看出,在使用sqlSession1的分别执行的两次查询,只输出一条执行sql语句,表示第二次查询时使用了缓存,未发送sql到数据库。

在sqlSession2执行的相同SQL时新输出了一条SQL表示没有使用缓存。

一级缓存的不足

使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个回话或者分布式环境下,会存在查到过时数据的问题(缓存脏读)。如下别的例子:

代码语言:javascript
复制
    /**
     * 缓存脏读,当有两个sqlsession同时操作一条数据时,会导致其中一个sqlsession不触发缓存清空,导致其中一个使用旧的缓存数据(已是脏数据)
     */
    @Test
    public void testCacheDirtyRead(){

        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();

        try {
            UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
            UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);

            User user1 = userMapper1.byId(1001L);

            LoggerUtil.printThread("session1 第1个结果:" + user1.toString());

            LoggerUtil.split();

            LoggerUtil.printThread("在session2 中更新数据。");

            User newUser = new User(1001L,"测试" + DateUtil.now(),20);
            userMapper2.update(newUser);
            sqlSession2.commit();
            LoggerUtil.printThread("session2 第1个结果(更新数据):" + userMapper2.byId(1001L).toString());

            LoggerUtil.split();

            //使用session1再次查询,使用缓存,导致使用旧数据
            User user3 = userMapper1.byId(1001L);

            LoggerUtil.printThread("session1 第2个结果(旧数据):" + user3.toString());
        }finally {
            sqlSession1.close();
            sqlSession2.close();
        }
    }

执行结果:

可以看到,在session2时已把用户名称修改为最新时间,但因为session1中使用一级缓存,不知道其他session的变更,所以导致缓存脏读。

如果要结果这个问题,就需要用到工作范围更广的二级缓存。

二级缓存

二级缓存是用来解决一级缓存不能跨会话共享问题的,范围是namespace级别的,可以被多个SqlSession共享(只要是同一个接口里的相同方法,都可以共享),生命周期和应用同步。

如果开启了二级缓存,二级缓存应用是工作在一级缓存之前,还是一级缓存之后呢?

作为一个作用范围更广的缓存,它肯定是在SqlSession的外层,否则不可能被多个SqlSession共享。

而一级缓存是在SqlSession内部的,所以肯定是工作在一级缓存之前,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。

二级缓存是在哪里维护的呢?

要夸会话共享的话,SqlSession本身和它里面的BaseExecutor已经满足不了需求了,那我们应该在BaseExecutor之外创建一个对象。

但是二级缓存是不一定开启的。也就是说,开启了二级缓存,就启用这个对象,如果没有,就不用这个对象,我们应用怎么做呢?

实际上MyBatis用了一个装饰器的类来维护,就是CachingExecutor。如果启用了二级缓存,MyBatis在创建Executor的时候会对Executor进行装饰。

CachingExecutor对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派给真正的查询器Executor实现类,比如SimpleExecutor来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。

开启二级缓存的方法

在mybatis-config.xml中配置了(可以不配置,默认是true)

代码语言:javascript
复制
<setting  name="cacheEnabled" value="true" />

只要没显示的设置cacheEnabled=false,都会用CachingExecutor装饰基本的执行器(Simple、Reuse、Batch)。

二级缓存的总开关默认是开启的。但是每个Mapper的二级缓存开关是默认关闭的。一个Mapper要用到二级缓存,还要单独打开它自己的开关。

代码语言:javascript
复制
<!-- 在mapper.xml中声明这个namespace使用二级缓存 -->

<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
	   size="1024" <!--最多缓存对象个数,默认1024-->
	   eviction="LRU" <!--回收策略-->
	   flushInterval="120000" <!--自动刷新时间ms,未配置时只有调用时刷新-->
	   readOnly="false"<!--默认是false(安全),改为true可读写时,对象必须支持序列化-->
	   />

mapper.xml配置了<cache>之后,select()会被缓存。update()、delete()、insert()会刷新缓存。

如果二级缓存拿到结果了,就直接返回(最外层判断),否则再到一级缓存,最后到数据库。

如果一个Mapper需要开启二级缓存,但是这个里面的某些查询方法对数据的实时性要求很高,不需要二级缓存,怎么办?

我们可以在单个Statement ID上显示关闭二级缓存(默认是true):

代码语言:javascript
复制
<select id="byId" resultMap="BaseResultMap" useCache="false" >
  ...
</select>

什么场景适合使用二级缓存?

  1. 因为所有增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如历史交易、历史订单查询等(查多写少)。如果写多查少就失去了缓存的意义。
  2. 如果多个namespace中针对同一个表的操作,比如User表,如果在一个namespace中刷新了缓存,另一个namespace中没有刷新,就会出现脏数据的情况。

所以,推荐在一个Mapper里只操作单表的情况使用。

如果让多个namespace共享一个二级缓存,应该怎么做?

代码语言:javascript
复制
<cache-ref namespace="<其他命名空间>" />

cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个cache。在关联表比较少,或者按照业务可以对表进行分组的时候可以使用。

注意:这种情况下,多个Mapper操作都会引起缓存刷新,缓存的意义已经不大了。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一级缓存
    • 一级缓存的不足
    • 二级缓存
      • 开启二级缓存的方法
      相关产品与服务
      文件存储
      文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档