缓存就是内存中的一个对象,用于对数据库查询结果的保存,用于减少与数据库的交互次数从而降低数据库的压力,进而提高响应速度。
什么是MyBatis中的缓存?
一、mybatis缓存介绍
参考:https://www.cnblogs.com/cxuanBlog/p/11324034.html
如下图,是mybatis一级缓存和二级缓存的区别图解:
-
Mybatis一级缓存的作用域是同一个SqlSession,在同一个sqlSession中两次执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。当一个sqlSession结束后该sqlSession中的一级缓存也就不存在了。Mybatis默认开启一级缓存。
-
Mybatis二级缓存是多个SqlSession共享的,其作用域是mapper的同一个namespace,不同的sqlSession两次执行相同namespace下的sql语句且向sql中传递参数也相同即最终执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。Mybatis默认没有开启二级缓存需要在setting全局参数中配置开启二级缓存。
二、一级缓存
2.1、原理
下图是根据id查询用户的一级缓存图解
-
-
每次查询会先从缓存区域找,如果找不到从数据库查询,查询到数据将数据写入缓存。
-
Mybatis内部存储缓存使用一个HashMap,key为hashCode+sqlId+Sql语句。value为从查询出来映射生成的java对象
-
2.2、测试
模拟思路: 既然每个 SqlSession 都会有自己的一个缓存,那么我们用同一个 SqlSession 是不是就能感受到一级缓存的存在呢?调用多次 getMapper
方法,生成对应的SQL语句,判断每次SQL语句是从缓存中取还是对数据库进行操作,下面的例子来证明一下
-
测试一:证明一级缓存
@Test public void test_Method00() { //获取session SqlSession sqlSession = sqlSessionFactory.openSession(); //获限mapper接口实例 UserMapperDao dao = sqlSession.getMapper(UserMapperDao.class); //第一次查询 User user = dao.queryUserById(26); System.out.println("user = " + user); //第二次查询,由于是同一个session则不再向数据发出语句直接从缓存取出 User user1 = dao.queryUserById(26); System.out.println("user = " + user1); //关闭session sqlSession.close(); }
-
测试二:insert、update、delete等操作commit提交后会清空缓存区域。
@Test public void test_Method00() { //获取session SqlSession sqlSession = sqlSessionFactory.openSession(); //获限mapper接口实例 UserMapperDao dao = sqlSession.getMapper(UserMapperDao.class); //第一次查询 User user = dao.queryUserById(26); System.out.println("user = " + user); //插入操作,一级缓存失效 User user1 = new User(); user1.setUsername("ws"); user1.setAddress("陕西西安"); user1.setSex("1"); user1.setBirthday(new Date()); boolean result = dao.insertUser(user1); //第二次查询,由于经过更新操作之后一级缓存失效,所有从新查询数据库 User user2 = dao.queryUserById(26); System.out.println("user = " + user2); //关闭session sqlSession.close(); }
可以看出两次查询返回对象不是同一个,且查询的SQL语句执行了两次。
-
测试三:手动清理缓存对一级缓存
@Test public void test_Method01() { //获取session SqlSession sqlSession = sqlSessionFactory.openSession(); //获限mapper接口实例 UserMapperDao dao = sqlSession.getMapper(UserMapperDao.class); //第一次查询 User user = dao.queryUserById(26); System.out.println("user = " + user); //手动清理一级缓存 sqlSession.clearCache(); //第二次查询,由于是同一个session则不再向数据发出语句直接从缓存取出 User user2 = dao.queryUserById(26); System.out.println("user = " + user2); //关闭session sqlSession.close(); }
三、二级缓存
参考:https://www.cnblogs.com/cxuanBlog/p/11333021.html
3.1、原理
下图是多个sqlSession请求UserMapper的二级缓存图解。
-
二级缓存区域是根据mapper的namespace划分的,相同namespace的mapper查询数据放在同一个区域,如果使用mapper代理方法每个mapper的namespace都不同,此时可以理解为二级缓存区域是根据mapper划分 。也就是二级缓存被多个 SqlSession 共享,是一个全局的变量。当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
-
二级缓存默认是不开启的,需要手动开启二级缓存,实现二级缓存的时候,MyBatis要求返回的POJO必须是可序列化的
-
Mybatis内部存储缓存使用一个HashMap,key为hashCode+sqlId+Sql语句。value为从查询出来映射生成的java对象
-
sqlSession执行insert、update、delete等操作commit提交后会清空缓存区域。
3.2、开启二级缓存
在核心配置文件SqlMapConfig.xml中加入 <setting name="cacheEnabled" value="true"/>
描述 | 允许值 | 默认值 | |
---|---|---|---|
cacheEnabled | 对在此配置文件下的所有cache 进行全局性开/关设置。 | true/false | false |
要在你的Mapper映射文件中添加一行: <cache /> ,表示此mapper开启二级缓存。
cache 标签有多个属性,一起来看一些这些属性分别代表什么意义
-
eviction
: 缓存回收策略,有这几种回收策略-
LRU - 最近最少回收,移除最长时间不被使用的对象(默认)
-
FIFO - 先进先出,按照缓存进入的顺序来移除它们
-
SOFT - 软引用,移除基于垃圾回收器状态和软引用规则的对象
-
WEAK - 弱引用,更积极的移除基于垃圾收集器和弱引用规则的对象
-
-
flushinterval
缓存刷新间隔,缓存多长时间刷新一次,默认不清空,设置一个毫秒值 -
readOnly
: 是否只读;true 只读,MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。MyBatis 为了加快获取数据,直接就会将数据在缓存中的引用交给用户。不安全,速度快。读写(默认):MyBatis 觉得数据可能会被修改 -
size
: 缓存存放多少个元素 -
type
: 指定自定义缓存的全类名(实现Cache 接口即可) -
blocking
: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。
3.3、实现序列化
二级缓存需要查询结果映射的pojo对象实现java.io.Serializable接口实现序列化和反序列化操作,注意如果存在父类、成员pojo都需要实现序列化接口。
public class User implements Serializable
@Test public void test_Method01() { //获取session SqlSession sqlSession1 = sqlSessionFactory.openSession(); //获限mapper接口实例 UserMapperDao dao1 = sqlSession1.getMapper(UserMapperDao.class); //使用session1执行第一次查询 User user1 = dao1.queryUserById(26); System.out.println("user = " + user1); sqlSession1.commit(); //关闭session sqlSession1.close(); //使用session2执行第二次查询,由于开启了二级缓存这里从缓存中获取数据不再向数据库发出sql SqlSession sqlSession2 = sqlSessionFactory.openSession(); //获限mapper接口实例 UserMapperDao dao2 = sqlSession2.getMapper(UserMapperDao.class); //第二次查询,由于是同一个session则不再向数据发出语句直接从缓存取出 User user2 = dao2.queryUserById(26); System.out.println("user = " + user2); //关闭session sqlSession1.close(); }
3.5、二级缓存失效
(1)、第一次SqlSession 未提交
SqlSession 在未提交的时候,SQL 语句产生的查询结果还没有放入二级缓存中,这个时候 SqlSession2 在查询的时候是感受不到二级缓存的存在的,
@Test public void test_Method02() { //获取session SqlSession sqlSession1 = sqlSessionFactory.openSession(); //获限mapper接口实例 UserMapperDao dao1 = sqlSession1.getMapper(UserMapperDao.class); //使用session1执行第一次查询 User user1 = dao1.queryUserById(26); System.out.println("user = " + user1); //使用session2执行第二次查询,由于开启了二级缓存这里从缓存中获取数据不再向数据库发出sql SqlSession sqlSession2 = sqlSessionFactory.openSession(); //获限mapper接口实例 UserMapperDao dao2 = sqlSession2.getMapper(UserMapperDao.class); //第二次查询,由于是同一个session则不再向数据发出语句直接从缓存取出 User user2 = dao2.queryUserById(26); }
(2)、更新对二级缓存影响
与一级缓存一样,更新操作很可能对二级缓存造成影响,下面用三个 SqlSession来进行模拟,第一个 SqlSession 只是单纯的提交,第二个 SqlSession 用于检验二级缓存所产生的影响,第三个 SqlSession 用于执行更新操作。
@Test public void test_Method03() { SqlSession sqlSession1 = sqlSessionFactory.openSession(); UserMapperDao dao1 = sqlSession1.getMapper(UserMapperDao.class); User user1 = dao1.queryUserById(26); System.out.println("user1 = " + user1); sqlSession1.commit(); SqlSession sqlSession2 = sqlSessionFactory.openSession(); UserMapperDao dao2 = sqlSession2.getMapper(UserMapperDao.class); User user2 = dao2.queryUserById(26); System.out.println("user2 = " + user2); SqlSession sqlSession3 = sqlSessionFactory.openSession(); UserMapperDao dao3 = sqlSession3.getMapper(UserMapperDao.class); User user = new User(); user.setSex("0"); user.setId(28); boolean result = dao3.updateUser(user); System.out.println("result = " + result); sqlSession3.commit(); dao2 = sqlSession2.getMapper(UserMapperDao.class); user2 = dao2.queryUserById(26); System.out.println("user2 = " + user2); }
(3)、禁用当前select语句的二级缓存
在statement中设置useCache=false可以禁用当前select语句的二级缓存,即每次查询都会发出sql去查询,默认情况是true,即该sql使用二级缓存。
<!-- 根据id获取用户信息 --> <select id="queryUserById" parameterType="int" resultType="user" useCache="false"> select <include refid="sqlColumnsForUserTable"/> from user where id = #{id} </select>
3.6、刷新缓存
在mapper的同一个namespace中,如果有其它insert、update、delete事物操作数据后需要刷新缓存,如果不执行刷新缓存会出现脏读。
设置statement配置中的flushCache=“true” 属性,默认情况下为true即刷新缓存
,如果改成false则不会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。 如下:
<insert id="insertUser" parameterType="cn.itcast.mybatis.po.User" flushCache="true">
3.7、Mybatis Cache参数
-
flushInterval
(刷新间隔)可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。 -
size
(引用数目)可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的可用内存资源数目。默认值是1024。 -
readOnly
(只读)属性可以被设置为true或false。只读的缓存会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是false。
如下例子:
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
这个更高级的配置创建了一个 FIFO 缓存,并每隔 60 秒刷新,存数结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此在不同线程中的调用者之间修改它们会导致冲突。可用的收回策略有, 默认的是 LRU:
-
LRU – 最近最少使用的:移除最长时间不被使用的对象。
-
FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
-
SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
-
WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
四、mybatis整合ehcache
EhCache 是一个纯Java的进程内缓存框架,是一种广泛使用的开源Java分布式缓存,具有快速、精干等特点,是Hibernate中默认的CacheProvider。
4.1、mybatis整合ehcache原理
mybatis提供二级缓存Cache接口:org.apache.ibatis.cache.Cache
它的默认实现类:org.apache.ibatis.cache.impl.PerpetualCache
通过实现Cache接口可以实现mybatis缓存数据通过其它缓存数据库整合,mybatis的特长是sql操作,缓存数据的管理不是mybatis的特长,为了提高缓存的性能将mybatis和第三方的缓存数据库整合,比如ehcache、memcache、redis等。
4.2、引入缓存的依赖包
maven坐标:
<dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.2.0</version> </dependency>
4.3、引入缓存配置文件
classpath下添加:ehcache.xml内容如下:
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd"> <diskStore path="F:developehcache" /> <defaultCache maxElementsInMemory="1000" maxElementsOnDisk="10000000" eternal="false" overflowToDisk="false" timeToIdleSeconds="120" timeToLiveSeconds="120" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> </defaultCache> </ehcache>
属性说明:
-
diskStore:指定数据在磁盘中的存储位置。
-
defaultCache:当借助CacheManager.add(“demoCache”)创建Cache时,EhCache便会采用<defalutCache/>指定的的管理策略
以下属性是必须的:
-
maxElementsInMemory - 在内存中缓存的element的最大数目
-
maxElementsOnDisk - 在磁盘上缓存的element的最大数目,若是0表示无穷大
-
eternal - 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断
-
overflowToDisk - 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上 以下属性是可选的:
-
timeToIdleSeconds - 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置时间无穷大
-
timeToLiveSeconds - 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大 diskSpoolBufferSizeMB 这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认是30MB.每个Cache都应该有自己的一个缓冲区.
-
diskPersistent - 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。
-
diskExpiryThreadIntervalSeconds - 磁盘缓存的清理线程运行间隔,默认是120秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作
-
memoryStoreEvictionPolicy - 当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出)
4.4、开启ehcache缓存
EhcacheCache是ehcache对Cache接口的实现:
修改mapper.xml文件,在cache中指定EhcacheCache。
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
根据需求调整缓存参数:
<cache type="org.mybatis.caches.ehcache.EhcacheCache" > <property name="timeToIdleSeconds" value="3600"/> <property name="timeToLiveSeconds" value="3600"/> <!-- 同ehcache参数maxElementsInMemory --> <property name="maxEntriesLocalHeap" value="1000"/> <!-- 同ehcache参数maxElementsOnDisk --> <property name="maxEntriesLocalDisk" value="10000000"/> <property name="memoryStoreEvictionPolicy" value="LRU"/> </cache>
4.5、应用场景
-
对于访问多的查询请求且用户对查询结果实时性要求不高,此时可采用mybatis二级缓存技术降低数据库访问量,提高访问速度,业务场景比如:耗时较高的统计分析sql、电话账单查询sql等。
-
实现方法如下:通过设置刷新间隔时间,由mybatis每隔一段时间自动清空缓存,根据数据变化频率设置缓存刷新间隔flushInterval,比如设置为30分钟、60分钟、24小时等,根据需求而定。
4.6、局限性
mybatis二级缓存对细粒度的数据级别的缓存实现不好,比如如下需求:对商品信息进行缓存,由于商品信息查询访问量大,但是要求用户每次都能查询最新的商品信息,此时如果使用mybatis的二级缓存就无法实现当一个商品变化时只刷新该商品的缓存信息而不刷新其它商品的信息,因为mybaits的二级缓存区域以mapper为单位划分,当一个商品信息变化会将所有商品信息的缓存数据全部清空。解决此类问题需要在业务层根据需求对数据有针对性缓存。