温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

MyBatis有必要使用缓存吗

发布时间:2021-08-19 18:21:04 来源:亿速云 阅读:263 作者:chen 栏目:编程语言

本篇内容介绍了“MyBatis有必要使用缓存吗”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

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

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

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

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

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

MyBatis有必要使用缓存吗

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

类型缓存实现类描述作用装饰条件
基本缓存PerpetualCache缓存基本实现类默认是PeretualCache也可以自定义比如RedisCache、EhCache等,具备基本能功能的缓存
淘汰算法缓存LruCacheLRU淘汰策略的缓存当缓存达到上限时,删除最少使用的缓存(Laste Recently Use)eviction="LRU"(默认)

FifoCacheFIFO策略缓存当缓存达到上限时,删除最先入队的缓存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清除逻辑:

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)。

MyBatis有必要使用缓存吗

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

    /**
     * 测试一级缓存作用域,是在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();
        }
    }

执行结果:

MyBatis有必要使用缓存吗

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

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

一级缓存的不足

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

    /**
     * 缓存脏读,当有两个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();
        }
    }

执行结果:

MyBatis有必要使用缓存吗

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

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

二级缓存

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

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

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

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

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

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

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

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

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

MyBatis有必要使用缓存吗

开启二级缓存的方法

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

<setting  name="cacheEnabled" value="true" />

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

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

<!-- 在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):

<select id="byId" resultMap="BaseResultMap" useCache="false" >
  ...
</select>

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

  1. 因为所有增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如历史交易、历史订单查询等(查多写少)。如果写多查少就失去了缓存的意义。

  2. 如果多个namespace中针对同一个表的操作,比如User表,如果在一个namespace中刷新了缓存,另一个namespace中没有刷新,就会出现脏数据的情况。

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

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

<cache-ref namespace="<其他命名空间>" />

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

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

“MyBatis有必要使用缓存吗”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注亿速云网站,小编将为大家输出更多高质量的实用文章!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI