早在去年12月份的一篇Blog中【MySQL数据库原理 一】MySQL架构及查询语句执行流程探索MySQL的执行步骤中就提到过查询缓存这一个概念:
并且提到过其实不建议使用查询缓存,正因为如此,我们才不把缓存做到数据库,这样作为服务端的数据库缓存了各个客户端大量查询结果能用的比例却比较低,性价比不高;反之大多数应用都把缓存做到了应用逻辑层,简单的如一个map的MyBatis,由客户端自己定义策略。
缓存的基本概念
什么是缓存,缓存就是存储在内存中的临时数据。将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题。使用缓存的目的就是减少和数据库的交互次数,减少系统开销,提高系统效率,但是一定要注意,缓存存储的只能是:经常查询并且不经常改变的数据
MyBatis两级缓存
MyBatis包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率,MyBatis默认定义了两级缓存:一级缓存和二级缓存
- 一级缓存,SqlSession级别的缓存,也称为本地缓存,默认情况下,只有一级缓存开启。
- 二级缓存,基于namespace级别的缓存,也称为全局缓存,需要手动开启和配置。
其中,为了提高扩展性,MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存,整体的结构图如下:
我们使用之前的Person进行相关的测试,看看一次会话期间的一级缓存是如何实现的。PersonDao查询语句如下
Person getPersonByIdResult(@Param("id")int id);
xml中的SQL配置如下:
<select id="getPersonByIdResult" resultMap="PersonBankAccountByResultMap"> select p.id id,p.username,username,p.password password,p.age age,p.phone phone,p.email email,p.hobby hobby,ba.id bid,ba.bank_name bank_name,ba.account_name account_name,ba.person_id person_id from person p,bank_Account ba where p.id=ba.person_id and p.id=#{id} </select> <resultMap id="PersonBankAccountByResultMap" type="com.example.MyBatis.dao.model.Person"> <!-- id为主键 --> <id column="id" property="id"/> <result column="username" property="username"/> <result column="password" property="password"/> <result column="age" property="age"/> <result column="phone" property="phone"/> <!-- column写错会因为查到数据匹配不上,给默认值, type写错会报错,找不到属性--> <result column="email" property="email"/> <!-- column是数据库表的列名 , property是对应实体类的属性名 --> <result column="hobby" property="interests"/> <collection property="bankAccounts" javaType="ArrayList" ofType="com.example.MyBatis.dao.model.BankAccount"> <result column="bid" property="id" /> <result column="bank_name" property="bankName"/> <result column="account_name" property="accountName"/> <result column="person_id" property="personId"/> </collection> </resultMap>
MyBatis一级缓存
一级缓存表示会话缓存,也就是SqlSession缓存,在一次会话中的查询数据会被缓存下来。
一级缓存测试
我们编写测试类来查看下一级缓存是否生效
@Test public void testGetPersonByIdResultFirstCache() { //1.获取SqlSession对象 SqlSession sqlSession = MybatisUtils.getSqlSession(); //2.执行SQL PersonDao personDao = sqlSession.getMapper(PersonDao.class); Person person = personDao.getPersonByIdResult(1); System.out.println(person); Person person2 = personDao.getPersonByIdResult(1); System.out.println(person2); //关闭sqlSession sqlSession.close(); }
日志打印结果如下:
查询同样的数据,sql语句只被执行了一次,第二次是从缓存中获取的。
一级缓存失效情况
一级缓存是SqlSession级别的缓存,是一直开启的,我们关闭不了它。但有以下几种情况一级缓存会自动失效:
1 SqlSession不同导致缓存失效
两次请求的sqlSession不同导致缓存失效,这个很容易理解,因为一级缓存的作用域就在一次会话内嘛
@Test public void testGetPersonByIdResultFirstCache() { //1.获取SqlSession对象 SqlSession sqlSession1 = MybatisUtils.getSqlSession(); SqlSession sqlSession2 = MybatisUtils.getSqlSession(); //2.执行SQL PersonDao personDao1 = sqlSession1.getMapper(PersonDao.class); PersonDao personDao2 = sqlSession2.getMapper(PersonDao.class); Person person = personDao1.getPersonByIdResult(1); System.out.println(person); Person person2 = personDao2.getPersonByIdResult(1); System.out.println(person2); //关闭sqlSession sqlSession1.close(); sqlSession2.close(); }
从日志可以看到进行了两次查询
2 查询条件不同导致缓存失效
查询条件不同导致缓存失效,这个也很容易理解,因为两次查询的数据不同,第二次查询获取到的数据没有出现在第一次查询后缓存里
@Test public void testGetPersonByIdResultFirstCache() { //1.获取SqlSession对象 SqlSession sqlSession = MybatisUtils.getSqlSession(); //2.执行SQL PersonDao personDao = sqlSession.getMapper(PersonDao.class); Person person = personDao.getPersonByIdResult(1); System.out.println(person); Person person2 = personDao.getPersonByIdResult(2); System.out.println(person2); //关闭sqlSession sqlSession.close(); }
从日志可以看到进行了两次查询
3 两次查询间执行增删改操作导致缓存失效
同MySQL之前的查询缓存一样,只要表在两次查询之间发生了变更,那么这个变更语句会清空查询缓存,这也是MySQL弃用查询缓存的原因:
@Test public void testGetPersonByIdResultFirstCache() { //1.获取SqlSession对象 SqlSession sqlSession = MybatisUtils.getSqlSession(); //2.执行SQL PersonDao personDao = sqlSession.getMapper(PersonDao.class); Person person = personDao.getPersonByIdResult(1); System.out.println(person); int result= personDao.deletePerson(0); System.out.println(result); Person person2 = personDao.getPersonByIdResult(1); System.out.println(person2); //关闭sqlSession sqlSession.close(); }
从日志可以看到进行了两次查询
4 手动清除SqlSession缓存导致缓存失效
这个也很容易理解,当我们手动clear掉SqlSession缓存时,缓存自然而然也被处理掉了:
@Test public void testGetPersonByIdResultFirstCache() { //1.获取SqlSession对象 SqlSession sqlSession = MybatisUtils.getSqlSession(); //2.执行SQL PersonDao personDao = sqlSession.getMapper(PersonDao.class); Person person = personDao.getPersonByIdResult(1); System.out.println(person); sqlSession.clearCache(); Person person2 = personDao.getPersonByIdResult(1); System.out.println(person2); //关闭sqlSession sqlSession.close(); }
从日志可以看到进行了两次查询
MyBatis二级缓存
二级缓存也叫全局缓存,一级缓存作用域太低了,所以诞生了二级缓存,基于namespace级别的缓存,一个命名空间,对应一个二级缓存,也就是一个Mapper对应一个二级缓存。工作机制:
- 一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中
- 如果当前会话关闭了,这个会话对应的一级缓存就没了,一级缓存中的数据被保存到二级缓存中
- 新的会话查询信息,可以从二级缓存中获取内容;
- 不同的mapper查出的数据会放在自己对应的缓存(map)
我们来看下二级缓存的实现方式:
1 开启全局缓存
首先需要在全局配置文件mybatis-config.xml
中打开全局缓存,也就是二级缓存:
<settings> <setting name="logImpl" value="log4j"/> <setting name="cacheEnabled" value="true"/> //打开全局缓存 </settings>
2 PersonMapper中开启全局缓存
在personMapper.xml
的配置文件中加入:
<!--创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用, 而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突--> <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
3 测试二级缓存
只有在一级缓存会话关闭或者提交后,缓存数据才会被提交到二级缓存,我们模拟下这个实现:
@Test public void testGetPersonByIdResultSecondCache() { //1.获取SqlSession对象 SqlSession sqlSession1 = MybatisUtils.getSqlSession(); SqlSession sqlSession2 = MybatisUtils.getSqlSession(); //2.执行SQL PersonDao personDao1 = sqlSession1.getMapper(PersonDao.class); PersonDao personDao2 = sqlSession2.getMapper(PersonDao.class); Person person = personDao1.getPersonByIdResult(1); System.out.println(person); //会话1关闭,会话2执行查询 sqlSession1.close(); Person person2 = personDao2.getPersonByIdResult(1); System.out.println(person2); //关闭sqlSession sqlSession1.close(); sqlSession2.close(); }
查看日志发现只进行了一次查询,全局缓存生效
EhCache实现二级缓存
Ehcache是一种广泛使用的java分布式缓存,用于二级全局缓存。要在应用程序中使用Ehcache,需要引入依赖的jar包,输入如下Maven坐标引入:
<dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.2.1</version> </dependency>
然后在对应的Mapper中,我们这里是personMapper.xml文件中加入该缓存支持,同时注释原有二级缓存配置:
<!--创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突--> <!--<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>--> <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
代码测试结果同原有二级缓存测试。
总结一下
MySQL其实不建议使用查询缓存,同时8.X以后版本也移除了该模块。正因为如此,我们才不把缓存做到数据库,作为服务端的数据库缓存了各个客户端大量查询结果只要进行一次全表更新就失效,能用的比例非常低,性价比不高;反之大多数应用都把缓存做到了应用逻辑层,简单的如一个map的MyBatis,由客户端自己定义策略,缓存由框架自己实现,MySQL没有负担,客户端也能灵活定义自己的配置。