缓存是Mybatis中非常重要的特性,Mybatis的一级缓存基于SqlSession实现,二级缓存基于Mapper实现。
一、缓存的使用
一级缓存默认开启,Mybatis提供了一个配置参数localCacheScope来控制一级缓存的级别,该参数的取值可以是session、statement等,当取值为session时,缓存对整个sqlsession有效,只有执行DML(更新)语句时缓存才会被清除,当取值为statement是,缓存只对当前执行的语句生效,当语句执行完后缓存会被清空。
Mybatis的一级缓存默认开启,且用户只能改变其级别,而不能进行关闭。所以我们重点看看Mybatis的二级缓存的使用:
二、Mybatis的缓存实现类
Mybatis的缓存基于JVM堆内存来实现的,即所有缓存对象都放在java对象中,并且通过Cache接口定义缓存对象的行为,Cache接口代码如下:
Mybatis的缓存类使用装饰者模式,Cache接口只有一个基本的实现类,即PrepetualCache类,该类通过一个HasshMap存放缓存对象。需要注意的是,PrepetualCache类重写了Object类的equals方法(所以当两个缓存对象的id一样时(一般缓存对象的Id为mapper.xml中的命名空间名称,即全限定类名),则认为缓存对象相同)。
mybatis还对PerpetualCache类进行了增强,提供了一些缓存的装饰类,具体如下:
这些装饰类相应的功能如下:
另外缓存对象的创建是mybatis提供的CacheBuilder类通过生成器模式创建的,例如下面使用该类创建一个缓存对象:
三、一级缓存的实现原理
上面讲的缓存实现类,是mybatis一级缓存、二级缓存的基础,现在我们先来讲下一级缓存的实现。
Mybatis的一级缓存是SqlSession级别的缓存,在介绍核心组件的时候有提过SqlSession提供了面向用户的api,而真正执行sql的是Executor组件,Executor采用模板方法设计模式,BaseExecutor类用于处理一些通用的逻辑,其中一级缓存的逻辑就是在BaseExecutor类中完成的,
接下来我看看主要实现,一级缓存在BaseExecutor类中使用PerpetualCache实例实现的,在该类中维护了两个PerpetualCache属性,代码如下:
其中localCache用来缓存Mybatis查询的结果,另一个用来缓存存储过程调用的结果。这两个属性在BaseExecutor构造方法中进行初始化的,代码如下:
Mybatis通过CacheKey对象来描述缓存的key值(即存进PerpetualCache实例的是一个key-value结构,可以为CacheKey,value为缓存对象),在执行查询操作时,先创建了Cachekey对象,如果两次查询操作的Cachekey对象相同,则认为这两次查询执行的是相同的SQL语句。CacheKey对象是通过BaseExecutor类的createKey方法创建的,代码如下:
BaseExecutor类的query方法(即查询api)的具体实现:
可以看到,query方法中,先根据key去缓存中获取缓存对象,如果没有则调用queryFromDatabase方法从数据库中获取数据,然后再将数据写入缓存中。
需要注意的是,如果localCacheScope属性设置为statement时,每次查询操作完成后,都会调用clearLocalCache方法清空缓存,另外mybatis也会在每次执行更新操作前清空缓存,具体代码可以看BaseExecutor的update方法:
可以看到,在调用doUpdate方法前都会先清空缓存。
注意:(这里后面验证一下)
四、Mybatis二级缓存实现原理
默认情况下二级缓存是关闭的,可通过摄者cacheEnabled参数值为true来开启二级缓存,前面说过Sqlsession将执行的逻辑委托给Executor组件完成,而Executor接口有几种不同的实现,分别为SimpleExecutor、BatchExecutor、ReuseExecutor,另外还有一个比较特殊的CachingExecutor,CachingExecutor采用了装饰器模式,在其他Executor的基础上增加了二级缓存功能。
Executor实例创建是通过工厂模式创建,Configuration类提供一个工厂方法newExecutor方法用来创建返回一个Executor对象,我们可以看看这个方法的实现:
可以看到,Configuration类的newExecutor工厂方法根据defaultExecutorType参数知道的Executor类型创建对应的Executor实例。
如果cacheEnabled属性值为true(开启二级缓存),则使用CachingExecutor对普通的Executor对象进行装饰,CachingExecutor在普通的Executor的基础上增加了二级缓存,我们接下看看CachingExecutor的实现,下面先看看该类的属性信息:
我们看看这个TransactionCacheManager(用于管理所以的二级缓存对象):
在TransactionalCacheManager类中,通过一个HashMap去维护二级缓存实例对应的 TransactionalCache对象,并提供获取缓存等方法。那么下面我们直接去看CachingExecutor的query实现方法:
可以看到先调用了createCacheKey 创建缓存key,然后调用MapperStatement对象的getCache获取MapperStatement对象中维护的二级缓存对象(我们知道一个MapperStatement对应一个Mappper接口),然后从二级缓存对象中获取缓存结果,如果获取不到则调用Executor的query方法从数据库获取数据,在将数据添加到二级缓存,如果有更新操作则同一命名空间下的二级缓存会被清空,下面看下CachingExecutor的update方法:
上面我们说维护二级缓存的实例是从MappedStatement中获取的,那我们下面看看创建MapppedStatement是怎么去创建这些二级缓存实例的:
五、二级缓存集成redis
Redis实现Mybatis二级缓存_redis的二级缓存_悠然予夏的博客-CSDN博客
Mybatis的二级缓存、使用Redis做二级缓存
六、一级缓存与二级缓存的区别,以及二级缓存的弊端
一级缓存的CacheKey是Mapper的命名空间+<select|update|insert|delete>的id组成的,且是基于SqlSession级别进行操作的(一般情况下,当我们使用Mybatis进行数据库的操作时候,会创建一个SqlSession来进行一次数据库的会话,会话结束则关闭SqlSession对象)
注意:两次查询须在同一个sqlsession中完成,否则将不会走mybatis的一级缓存。
在mybatis与spring进行整合开发时,事务控制在service中进行,重复调用两次servcie将不会走一级缓存,因为在第二次调用时session方法结束,SqlSession就关闭了。(同一个事务查询两次才会用到缓存)
(所以一般不用设置为Statement???)
而二级缓存则是基于Mapper级别进行的,即所有SqlSession的缓存进行共享(管理二级缓存额实例是存在MappedStatement的,而一个MappedStatement对应一个Mapper(即同一个命名空间对应一个二级缓存))
二级缓存是存在一个问题的,当对一个表的增删改查不在同一个namespace(即同一个接口中)时,会出现脏读现象,因为二级缓存是Mapper级别的(即一个Mapper接口层面),如果同一个表的修改在其他namespace(即其他Mapper)执行,此时第一个Mapper的二级缓存无法感知到第二个Mapper的二级缓存的改变,所以导致读到未刷新的数据。因此我们一般不建议使用二级缓存,如果需要使用二级缓存可以集成第三方缓存例如redis。
Mybatis二级缓存的缺陷_mybatis缓存的坏处_龙域、白泽的博客-CSDN博客
Mybatis一级缓存与二级缓存的区别你知道吗_mybatis的一级缓存和二级缓存的区别_Java小叮当的博客-CSDN博客
Mybatis 中的一级缓存与二级缓存_mybatis一级缓存和二级缓存_头真的好重好重Y的博客-CSDN博客