1、MyBatis缓存综述
在Web系统中,最重要的操作就是查询数据库中的数据。但是有些时候查询数据的频率非常高,这是很耗费数据库资源的,往往会导致数据库查询效率极低,影响客户的操作体验。于是,我们可以将一些变动不大且访问频率高的数据,放置在一个缓存容器中,用户下一次查询时就从缓存容器中获取结果。MyBatis拥有自己的缓存结构,可以用来缓解数据库压力,加快查询速度。MyBatis提供一级缓存和二级缓存机制。
一级缓存是SqlSession级别的缓存。在操作数据库时,每个SqlSession类的实例对象中有一个数据结构(HashMap)可以用于存储缓存数据。不同的SqlSession类的实例对象缓存的数据区域(HashMap)是互不影响的。当一个SqlSession结束后,该SqlSession中的一级缓存也就不存在了。MyBatis默认一级缓存是开启状态的,且不能关闭。
二级缓存是Mapper级别的缓存,多个SqlSession类的实例对象操作同一个Mapper配置文件中的SQL语句,多个SqlSession类的实例对象可以共用二级缓存,二级缓存是跨SqlSession的。
综上,每个SqlSession类的实例对象自身有一个一级缓存,而查询同一个Mapper映射文件的SqlSession类的实例对象之间又共享同一个二级缓存。
2、搭建测试环境
2.1 创建数据表
在mybaits数据库中创建名为“book”的数据表用于演示测试MyBatis查询缓存案例的数据表。创建数据表与添加测试语句的SQL语句如下所示:
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for `book`
-- ----------------------------
DROP TABLE IF EXISTS `book`;
CREATE TABLE `book` (
`id` int(255) NOT NULL auto_increment,
`name` varchar(255) default NULL,
`author` varchar(255) default NULL,
`isbn` varchar(255) default NULL,
`publisher` varchar(255) default NULL,
`number` int(255) default NULL,
`price` double(255,0) default NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of book
-- ----------------------------
INSERT INTO `book` VALUES ('1', 'Java程序员开发指南', '张义,李剑编', '7-80172-267-001', '清华大学出版社', '10', '45');
INSERT INTO `book` VALUES ('2', '达芬奇密码', '丹·布朗', '7-80172-267-002', '兰登书屋', '10', '45');
INSERT INTO `book` VALUES ('3', '解忧杂货铺', '东野圭谷', '7-80172-267-003', '角川书店', '10', '45');
INSERT INTO `book` VALUES ('4', '为奴二十年', '所罗门·诺瑟普', '7-80172-267-004', '好莱坞', '10', '45');
INSERT INTO `book` VALUES ('5', '三体Ⅰ', '刘慈欣', '7-80172-267-005', '《科幻世界》杂志', '10', '45');
INSERT INTO `book` VALUES ('6', '冰与火之歌全集', '乔治·R·R·马丁(George R.R. Martin)', '7-80172-267-006', '《科幻世界》杂志', '10', '45');
2.2 创建实体类
在com.ccff.mybatis.model包下创建名为“Book”的实体类,提供get、set和toString方法,具体代码如下:
package com.ccff.mybatis.model;
public class Book {
private int id;
private String name;
private String author;
private String isbn;
private String publisher;
private int number;
private Double price;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getIsbn() {
return isbn;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
public String getPublisher() {
return publisher;
}
public void setPublisher(String publisher) {
this.publisher = publisher;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
@Override
public String toString() {
return "Book{" +
"id=" + id +
", name='" + name + '\'' +
", author='" + author + '\'' +
", isbn='" + isbn + '\'' +
", publisher='" + publisher + '\'' +
", number=" + number +
", price=" + price +
'}';
}
}
2.3 创建接口Dao文件
在com.ccff.mybatis.dao下创建名为“IBookDao”的接口文件,用于在该接口中添加相应的方法来操作数据库,具体代码如下:
package com.ccff.mybatis.dao;
import com.ccff.mybatis.model.Book;
public interface IBookDao {
}
2.4 创建SQL映射文件
在config/sqlmap文件夹中创建名为“BookMapper”的SQL映射文件,用于在其中添加测试的SQL语句片段,具体配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ccff.mybatis.dao.IBookDao">
</mapper>
创建完SQL映射文件后,需要在MyBatis全局配置文件中配置该SQL映射文件的路径,具体配置如下:
<!--配置SQL映射文件的位置-->
<mappers>
<mapper resource="sqlmap/UserMapper.xml"/>
<mapper resource="sqlmap/StudentMapper.xml"/>
<mapper resource="sqlmap/BasketballPlayerMapper.xml"/>
<mapper resource="sqlmap/FinacialMapper.xml"/>
<mapper resource="sqlmap/NewsLabelMapper.xml" />
<mapper resource="sqlmap/LazyLoadingMapper.xml" />
<mapper resource="sqlmap/BookMapper.xml" />
</mappers>
2.5 创建测试类
在com.ccff.mybatis.test包下创建名为“BookTest”的测试类,用于测试Dao接口文件的方法,具体代码如下:
package com.ccff.mybatis.test;
import com.ccff.mybatis.dao.IBookDao;
import com.ccff.mybatis.datasource.DataConnection;
import com.ccff.mybatis.model.Book;
import org.apache.ibatis.session.SqlSession;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
public class BookTest {
private SqlSession sqlSession;
private IBookDao bookDao;
@Before
public void init() {
DataConnection dataConnection = new DataConnection();
try {
sqlSession = dataConnection.getSqlSession();
bookDao = sqlSession.getMapper(IBookDao.class);
} catch (IOException e) {
e.printStackTrace();
}
}
@After
public void tearDown(){
if (sqlSession != null)
sqlSession.close();
}
}
3、一级查询缓存
3.1 一级缓存原理阐述
一级缓存的工作原理如下图所示,这里以查询用户的操作为例。
该图阐述了一个SqlSession类的实例对象下的一级缓存的工作原理。当第一次查询id为1的用户信息时,SqlSession类的实例对象sqlSession首先到一级缓存区域查询,如果没有相关查询,则从数据库查询。然后sqlSession将该查询结果保存到一级缓存区域。在下一次查询时,如果sqlSession执行了commit操作(即执行了增删改操作),则会清空它的一级缓存区域,以此来保证缓存中的信息是最新的,避免了脏读现象的发生。如果在此期间sqlSession一直没有执行commit操作修改数据,那么下次查询id为1的用户信息时,sqlSession在一级缓存中就会发现该信息,然后从缓存中获取用户信息。
3.2 一级缓存存在性证明
首先,在IBookDao接口中添加名为“findBookById”的方法,该方法可以根据图书的id查询相应的图书信息,具体代码如下:
package com.ccff.mybatis.dao;
import com.ccff.mybatis.model.Book;
public interface IBookDao {
//根据图书的id查询相应的图书信息
public Book findBookById(int id);
}
然后,在BookMapper.xmlSQL映射文件中添加id为“findBookById”的select标签,实现根据图书的id查询相应的图书信息的数据库操作,具体配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ccff.mybatis.dao.IBookDao">
<!--根据图书的id查询相应的图书信息-->
<select id="findBookById" parameterType="int" resultType="Book">
select * from book where id = #{id}
</select>
</mapper>
接着,在BookTest测试类中添加名为“TestFindBookById”的测试方法,具体代码如下:
package com.ccff.mybatis.test;
import com.ccff.mybatis.dao.IBookDao;
import com.ccff.mybatis.datasource.DataConnection;
import com.ccff.mybatis.model.Book;
import org.apache.ibatis.session.SqlSession;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
public class BookTest {
private SqlSession sqlSession;
private IBookDao bookDao;
@Before
public void init() {
DataConnection dataConnection = new DataConnection();
try {
sqlSession = dataConnection.getSqlSession();
bookDao = sqlSession.getMapper(IBookDao.class);
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void TestFindBookById(){
Book book = null;
//第一次查询
book = bookDao.findBookById(1);
System.out.println(book);
//第二次查询
book = bookDao.findBookById(1);
System.out.println(book);
}
@After
public void tearDown(){
if (sqlSession != null)
sqlSession.close();
}
}
最后,运行TestFindBookById测试方法,查询输出到控制台上的日志信息,发现查询了两次,但只执行了一次从DB中查询数据的操作,第二次查询数据是直接输出的。这就说明第二次是从SqlSession缓存中读取的。
3.3 从缓存读取数据的依据是SQL的id
一级缓存缓存的是相同SQL映射id的查询结果,而非相同SQL语句的查询结果。因为MyBatis内部对于查询缓存,无论是一级缓存还是二级缓存,其底层均使用一个HashMap实现;key为SQL的id相关内容,value为从数据库中查询出来的结果。
为验证从缓存读取数据的依据是SQL的id,首先,修改SQL映射文件,对之前添加的select标签进行完全复制,修改复制后的select标签的id。也就是说,这两个SQL映射除了id不同,其余均相同,即查询结果肯定也相同。具体配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ccff.mybatis.dao.IBookDao">
<!--根据图书的id查询相应的图书信息-->
<select id="findBookById" parameterType="int" resultType="Book">
select * from book where id = #{id}
</select>
<select id="findBookById2" parameterType="int" resultType="Book">
select * from book where id = #{id}
</select>
</mapper>
然后,修改Dao接口文件,在接口文件中添加一个方法,其方法名与之前在SQL映射文件中修改的id名一致,其余均与现有接口中的方法相同。具体代码如下:
package com.ccff.mybatis.dao;
import com.ccff.mybatis.model.Book;
public interface IBookDao {
//根据图书的id查询相应的图书信息
public Book findBookById(int id);
public Book findBookById2(int id);
}
接着,修改测试方法,创建名为“TestFindBookById2”的测试方法,用来测试从缓存读取数据的依据是SQL的id。具体代码如下:
@Test
public void TestFindBookById2(){
Book book = null;
//第一次查询
book = bookDao.findBookById(1);
System.out.println("第一次查询:"+book);
//第二次查询
book = bookDao.findBookById2(1);
System.out.println("第二次查询:"+book);
}
最后,运行该测试方法后,查看输出到控制台的日志信息,发现第二次查询结果与第一次完全相同,但第二次查询并没有从缓存中读取数据,而是直接从DB中进行的查询。这是因为从缓存读取数据的依据是查询SQL的映射id,而非查询结果。
3.4 增删改对一级缓存的影响
对于增删改操作,无论最后是否进行了提交,即执行了sqlSession.commit() ,均会清空一级查询缓存,使查询再次从DB中查询数据。
首先,修改Dao接口文件,在接口中添加根据图书的id修改相应的图书信息的方法,具体代码如下:
package com.ccff.mybatis.dao;
import com.ccff.mybatis.model.Book;
public interface IBookDao {
//验证一级缓存的存在性:根据图书的id查询相应的图书信息
public Book findBookById(int id);
//验证从缓存读取数据的依据是SQL的id:根据图书的id查询相应的图书信息
public Book findBookById2(int id);
//验证增删改操作会清空一级缓存:根据图书的id修改相应的图书信息
public void updateBookById(Book book);
}
然后,修改SQL映射文件,添加对应的update标签,具体配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ccff.mybatis.dao.IBookDao">
<!--根据图书的id查询相应的图书信息-->
<select id="findBookById" parameterType="int" resultType="Book">
select * from book where id = #{id}
</select>
<select id="findBookById2" parameterType="int" resultType="Book">
select * from book where id = #{id}
</select>
<!--验证增删改操作会清空一级缓存:根据图书的id修改相应的图书信息-->
<update id="updateBookById" parameterType="Book">
update book set number = #{number} where id = #{id}
</update>
</mapper>
接着,修改测试方法,添加名为“TestUpdateBookById”的测试方法,具体代码如下:
@Test
public void TestUpdateBookById(){
Book book = null;
//第一次查询
book = bookDao.findBookById(1);
System.out.println("第一次查询:"+book);
//执行增删改操作
Book updateBook = new Book();
updateBook.setId(3);
updateBook.setNumber(5);
bookDao.updateBookById(updateBook);
sqlSession.commit();
//第二次查询
book = bookDao.findBookById2(1);
System.out.println("第二次查询:"+book);
}
最后,执行TestUpdateBookById方法,查看控制台的输出信息,发现前后两次查询均是从数据库中查询获取的数据,说明中间执行的修改图书信息操作清空了SqlSession中的缓存。
4、二级缓存
4.1 二级缓存原理阐述
一级缓存是基于同一个SqlSession类的实例对象的。但是,有些时候在Web工程中会将执行查询操作的方法封装在某个Service方法中,当查询完一次后,Service方法结束,此时SqlSession类的实例对象就会关闭,一级缓存就会被清空。此时若再次调用Service方法查询同一个信息,新打开一个SqlSession类的实例对象,由于一级缓存区域是空的,因而无法从缓存中获取信息。
当出现上面的情况而无法使用一级缓存时,可以使用二级缓存。二级缓存存在于Mapper实例中,当多个SqlSession类的实例对象加载相同的Mapper文件,并执行其中的SQL配置时,它们就会共享一个Mapper缓存。与一级缓存类似,当SqlSession类的实例对象加载Mapper进行查询时,会先去Mapper的缓存区域寻找该值,若不存在,则去数据库查询,然后将查询出来的结果存储到缓存区域,待下次查询相同数据时从缓存区域获取。当某个SqlSession类的实例对象进行了增删改等改变数据的操作时,Mapper实例都会清空其二级缓存。
与一级缓存相比,二级缓存的范围更大。多个SqlSession类的实例对象可以共享一个Mapper的二级缓存区域。一个Mapper有一个自己的二级缓存区域(按照namespace划分),两个Mapper的namespace如果相同,那么这两个Mapper执行的SQL查询会被缓存在同一个二级缓存中。
使用二级缓存的目的,不是共享数据,因为MyBatis从缓存中读取数据的依据是SQL的id,而非查询出的对象。所以,二级缓存中的数据不是为了多个查询之间共享(所有查询中只要查询结果中存在该对象的,就直接从缓存中读取,这是对数据的共享,Hibernate中的缓存就是为了共享,但MyBatis的缓存不是),而是为了延长该查询结果的保存时间,提高系统性能。
二级缓存的原理如下图所示:
4.2 二级缓存用法
二级缓存的使用很简单,只需要完成以下几步即可。
4.2.1 实体序列化
要求查询结果所涉及的实体类要实现Serializable接口。若该实体类存在父类,或其具有域属性,则父类与域属性类也要实现序列化接口。因此在本例中,Book实体要实现序列化接口,具体代码如下:
package com.ccff.mybatis.model;
import java.io.Serializable;
public class Book implements Serializable {
private int id;
private String name;
private String author;
private String isbn;
private String publisher;
private int number;
private Double price;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getIsbn() {
return isbn;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
public String getPublisher() {
return publisher;
}
public void setPublisher(String publisher) {
this.publisher = publisher;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
@Override
public String toString() {
return "Book{" +
"id=" + id +
", name='" + name + '\'' +
", author='" + author + '\'' +
", isbn='" + isbn + '\'' +
", publisher='" + publisher + '\'' +
", number=" + number +
", price=" + price +
'}';
}
}
4.2.2 SQL映射文件中添加cache标签
在BookMapperSQL映射文件中添加cache标签,同时也可以在cache标签中添加一些相关属性设置,可以对二级缓存的运行性能进行控制。当然,若不指定设置,则均保持默认值。具体配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ccff.mybatis.dao.IBookDao">
<cache eviction="FIFO" flushInterval="10800000" readOnly="true" size="512"/>
<!--根据图书的id查询相应的图书信息-->
<select id="findBookById" parameterType="int" resultType="Book">
select * from book where id = #{id}
</select>
<select id="findBookById2" parameterType="int" resultType="Book">
select * from book where id = #{id}
</select>
<!--验证增删改操作会清空一级缓存:根据图书的id修改相应的图书信息-->
<update id="updateBookById" parameterType="Book">
update book set number = #{number} where id = #{id}
</update>
</mapper>
上面在cache标签中进行了一些属性配置,其中:
- eviction:逐出策略。当二级缓存中的对象达到最大值时,就需要通过逐出策略将缓存中的对象移出缓存。默认为LRU,这里设置为FIFO。常用的逐出策略有:
- FIFO:先进先出
- LRU:未被使用时间最长的
- flushInterval:刷新缓存的时间间隔,单位毫秒。这里的刷新缓存即清空缓存。一般不指定,即当执行增删改时刷新缓存。
- readOnly:设置缓存中数据是否只读。只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,这提供了很重要的性能优势。但读写的缓存会返回缓存对象的拷贝,这会慢一些,但是安全,因此默认是false。
- size:二级缓存中可以存放的最多对象个数,默认值为1024个。
4.3 二级缓存存在性证明
首先,修改之前编写的数据连接类。修改com.ccff.mybatis.datasource包下的DataConnection类,具体代码如下:
package com.ccff.mybatis.datasource;
import com.ccff.mybatis.dao.IBasketballPlayerDao;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class DataConnection {
//mybatis配置文件
private String resource = "SqlMapConfig.xml";
private SqlSessionFactory sqlSessionFactory;
private SqlSession sqlSession;
public SqlSession getSqlSession() throws IOException {
if (sqlSessionFactory == null){
InputStream inputStream = Resources.getResourceAsStream(resource);
//创建会话工厂,传入mybatis配置文件的信息
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
sqlSession = sqlSessionFactory.openSession();
return sqlSession;
}
public IBasketballPlayerDao getBasketballPlayerMapper() {
IBasketballPlayerDao basketballPlayerMapper = sqlSession.getMapper(IBasketballPlayerDao.class);
return basketballPlayerMapper;
}
}
然后,修改BookTest测试类,将原有的DataConnection类的实例对象dataConnection抽成成员变量。
接着,添加测试方法TestProofSecondLevelCache,该方法用于证明二级缓存的存在性,具体代码如下:
package com.ccff.mybatis.test;
import com.ccff.mybatis.dao.IBookDao;
import com.ccff.mybatis.datasource.DataConnection;
import com.ccff.mybatis.model.Book;
import org.apache.ibatis.session.SqlSession;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
public class BookTest {
private SqlSession sqlSession;
private IBookDao bookDao;
private DataConnection dataConnection = new DataConnection();
@Before
public void init() {
try {
sqlSession = dataConnection.getSqlSession();
bookDao = sqlSession.getMapper(IBookDao.class);
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void TestFindBookById(){
Book book = null;
//第一次查询
book = bookDao.findBookById(1);
System.out.println("第一次查询:"+book);
//第二次查询
book = bookDao.findBookById(1);
System.out.println("第二次查询:"+book);
}
@Test
public void TestFindBookById2(){
Book book = null;
//第一次查询
book = bookDao.findBookById(1);
System.out.println("第一次查询:"+book);
//第二次查询
book = bookDao.findBookById2(1);
System.out.println("第二次查询:"+book);
}
@Test
public void TestUpdateBookById(){
Book book = null;
//第一次查询
book = bookDao.findBookById(1);
System.out.println("第一次查询:"+book);
//执行增删改操作
Book updateBook = new Book();
updateBook.setId(3);
updateBook.setNumber(5);
bookDao.updateBookById(updateBook);
sqlSession.commit();
//第二次查询
book = bookDao.findBookById2(1);
System.out.println("第二次查询:"+book);
}
@Test
public void TestProofSecondLevelCache(){
//第一次查询
bookDao = sqlSession.getMapper(IBookDao.class);
Book book1 = bookDao.findBookById(1);
System.out.println("第一次查询:"+book1);
sqlSession.close();
try {
sqlSession = dataConnection.getSqlSession();
} catch (IOException e) {
e.printStackTrace();
}
//第二次查询
bookDao = sqlSession.getMapper(IBookDao.class);
Book book2 = bookDao.findBookById(1);
System.out.println("第二次查询:"+book2);
}
@After
public void tearDown(){
if (sqlSession != null)
sqlSession.close();
}
}
最后,运行测试方法TestProofSecondLevelCache,在控制台输出如下日志信息。
Cache Hit Ratio表示缓存命中率。开启二级缓存后,每执行一次查询,系统都会计算一次二级缓存的命中率。第一次查询也是先从缓存中查询,只不过缓存中一定是没有的。所以,会再从DB查询。由于二级缓存中不存在该数据,所以命中率为0。但第二次查询是从二级缓存中读取的,所以这一次的命中率为1/2=0.5.当然,若有第三次查询的话,则命中率会是2/3=0.67。
4.4 增删改对二级缓存的影响
4.4.1 默认对缓存的刷新
增删改操作,无论是否进行提交,均会清空一级、二级缓存,使查询再次从DB中获取。
修改测试类BookTest,添加方法TestCUDAffectSecondLevelCache,具体代码如下:
@Test
public void TestCUDAffectSecondLevelCache(){
//第一次查询
bookDao = sqlSession.getMapper(IBookDao.class);
Book book1 = bookDao.findBookById(1);
System.out.println("第一次查询:"+book1);
sqlSession.close();
try {
sqlSession = dataConnection.getSqlSession();
} catch (IOException e) {
e.printStackTrace();
}
bookDao = sqlSession.getMapper(IBookDao.class);
//执行增删改操作
Book updateBook = new Book();
updateBook.setId(2);
updateBook.setNumber(10);
bookDao.updateBookById(updateBook);
sqlSession.commit();
//第二次查询
Book book2 = bookDao.findBookById(1);
System.out.println("第二次查询:"+book2);
}
运行该测试方法,发现前后两次查询均从数据库中获取,说明中间对图书信息的修改,导致二级缓存被清空。
4.4.2 设置增删改操作不刷新二级缓存
若要使某个增删改操作不清空二级缓存,则需要在其insert、delete或update标签中添加属性flushCache=“false”,默认为true。
修改BookMapper文件,在update标签中添加属性flushCache=“false”,具体配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ccff.mybatis.dao.IBookDao">
<cache eviction="FIFO" flushInterval="10800000" readOnly="true" size="512"/>
<!--根据图书的id查询相应的图书信息-->
<select id="findBookById" parameterType="int" resultType="Book">
select * from book where id = #{id}
</select>
<select id="findBookById2" parameterType="int" resultType="Book">
select * from book where id = #{id}
</select>
<!--验证增删改操作会清空一级缓存:根据图书的id修改相应的图书信息-->
<update id="updateBookById" parameterType="Book" flushCache="false">
update book set number = #{number} where id = #{id}
</update>
</mapper>
再次运行TestCUDAffectSecondLevelCache测试方法,查询输出到控制台的日志信息,发现此时,第二次查询是从二级缓存中获取的,缓存命中率为0.5。说明此时,该update操作并没有清空二级缓存。
4.5 二级缓存的关闭
二级缓存默认为开启状态。若要将其关闭,则需要进行相关设置。根据关闭的范围大小,可以分为全局关闭与局部关闭。
4.5.1 全局关闭
所谓全局关闭是指,整个应用的二级缓存全部关闭,所有查询均不使用二级缓存。全局开关设置在全局配置文件的全局设置settings标签中,该属性为cacheEnabled,设置为false,则关闭;设置为true,则开启,默认值为true。即二级缓存默认是开启的。具体配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--注册DB连接四要素的属性文件-->
<properties resource="jdbc.properties" />
<!--全局参数设置-->
<settings>
<!-- 配置LOG信息 -->
<setting name="logImpl" value="LOG4J" />
<!-- 延迟加载总开关 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 侵入式延迟加载开关 -->
<setting name="aggressiveLazyLoading" value="true"/>
<!-- 关闭二级查询缓存 -->
<setting name="cacheEnabled" value="false" />
</settings>
<!--配置别名-->
<typeAliases>
<!--<typeAlias type="com.ccff.mybatis.model.User" alias="User"/>-->
<package name="com.ccff.mybatis.model"/>
</typeAliases>
<!-- 和spring整合后 environments配置将废除-->
<environments default="development">
<environment id="development">
<!-- 使用jdbc事务管理-->
<transactionManager type="JDBC" />
<!-- 数据库连接池-->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!--配置SQL映射文件的位置-->
<mappers>
<mapper resource="sqlmap/UserMapper.xml"/>
<mapper resource="sqlmap/StudentMapper.xml"/>
<mapper resource="sqlmap/BasketballPlayerMapper.xml"/>
<mapper resource="sqlmap/FinacialMapper.xml"/>
<mapper resource="sqlmap/NewsLabelMapper.xml" />
<mapper resource="sqlmap/LazyLoadingMapper.xml" />
<mapper resource="sqlmap/BookMapper.xml" />
</mappers>
</configuration>
4.5.2 局部关闭
所谓局部关闭是指,整个应用的二级缓存是开启的,但只是针对某个select查询,不使用二级缓存。此时可以单独只关闭该select标签的二级缓存。
在该要关闭二级缓存的select标签中,将其属性useCache设置为false,即可关闭该查询的二级缓存。该属性默认为true,即每一个select查询的二级缓存默认是开启的。关闭局部二级缓存配置示例如下:
<select id="findBookById2" parameterType="int" resultType="Book" useCache="false">
select * from book where id = #{id}
</select>
4.6 二级缓存的使用原则
二级缓存的使用有以下三个原则:
第一,只能在一个命名空间下使用二级缓存。 由于二级缓存中的数据是基于namespace的,即不同的namespace中的数据互不干扰。在多个namespace中若均存在对同一个表的操作,那么这多个namespace中的数据可能就会出现不一致的现象。
第二,在单表上使用二级缓存。 如果一个表与其它表有关联关系,那么久非常有可能存在多个namespace对同一数据的操作。而不同namespace中的数据互不干扰,所以有可能出现这多个namespace中的数据不一致现象。
第三,查询多于修改时使用二级缓存。 在查询操作远远多于增删改操作的情况下可以使用二级缓存。因为任何增删改操作都将刷新二级缓存,对二级缓存的频繁刷新将降低系统性能。
5、ehcache二级缓存
MyBatis的特长是SQL操作,缓存数据管理不是其特长,为了提高缓存的性能,MyBatis允许使用第三方缓存产品。ehCache就是其中的一种。
这里需要说明的是:使用ehCache二级缓存,实体类无需事先序列化接口。
5.1 导入jar包
这里需要两个jar包:一个为ehcache的核心jar包,一个是MyBatis与ehcache整合的插件jar包。它们可以从 https://github.com/mybatis/ehcache-cache/releases 下载。
解压该文件,获取到它们。其中lib下的是ehcache的核心jar包。
将MyBatis与ehcache整合的jar包mybatis-ehcache-1.0.3.jar和lib下的ehcache核心jar包ehcache-core-2.6.8.jar添加到项目的WEB-INF/lib下,并Add as Library。
5.2 添加ehcache.xml
解压ehcache核心jar包ehcache-core-2.6.8.jar,将其中的配置文件ehcache-failsafe.xml直接放到config文件夹下,并将该配置文件名称更改为ehcache.xml。
5.2.1 diskStore标签
在该配置文件中有一个diskStore标签,该标签指定一个文件目录,当内存空间不够,需要将二级缓存中数据写到硬盘上时,会写到这个指定目录中。其值一般为java.io.tmpdir,表示当前系统的默认文件临时目录。
<diskStore path="java.io.tmpdir"/>
当前系统的默认文件临时目录,可以通过System.property()方法查看。
String path = System.property("java.io.tmpdir");
System.out.println(path);
5.2.2 defaultCache标签
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxElementsOnDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
<persistence strategy="localTempSwap"/>
</defaultCache>
设定缓存的默认属性数据:
maxElementsInMemory: 指定该内存缓存区可以存放缓存对象的最多个数。
eternal: 设定缓存对象是否不会过期。若设为true,表示对象永远不会过期,此时会忽略timeToIdleSeconds与timeToLiveSeconds属性。默认值为false。
timeToIdleSeconds: 设定允许对象处于空闲状态的最长时间,以秒为单位。当对象自从最近一次被访问后,若处于空闲状态的时间超过了timeToIdleSeconds设定的值,这个对象就会过期。当对象过期,ehcache就会将它从缓存中清除。设置值为0,则对象可以无限期地处于空闲状态。
timeToLiveSeconds: 设定对象允许存在于缓存中的最长时间,以秒为单位。当对象自从被存放到缓存后,若处于缓存中的时间超过了timeToLiveSeconds设定的值,这个对象就会过期。当对象过期,ehcache就会将它从缓存中清除。设置值为0,则对象可以徐嫌弃地存在于缓存中。注意,只有timeToLiveSeconds>=timeToIdleSeconds才有意义。
maxElementsOnDisk: 指定硬盘缓存区可以存放缓存对象的最多个数。
diskExpiryThreadIntervalSeconds: 指定硬盘中缓存对象的失效时间间隔。
memoryStoreEvictionPolicy: 如果内存缓存区超过限制,选择移向硬盘缓存区中的对象时使用的策略。支持三种策略:
FIFO:先进先出
LRU:最近最少使用
LFU:最少使用
5.3 启用ehcache缓存机制
修改BookMapper映射文件,为cache标签添加type属性,具体修改如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ccff.mybatis.dao.IBookDao">
<!--<cache eviction="FIFO" flushInterval="10800000" readOnly="true" size="512"/>-->
<cache type="org.mybatis.caches.ehcache.EhcacheCache" />
<!--根据图书的id查询相应的图书信息-->
<select id="findBookById" parameterType="int" resultType="Book">
select * from book where id = #{id}
</select>
<select id="findBookById2" parameterType="int" resultType="Book" useCache="false">
select * from book where id = #{id}
</select>
<!--验证增删改操作会清空一级缓存:根据图书的id修改相应的图书信息-->
<update id="updateBookById" parameterType="Book" flushCache="false">
update book set number = #{number} where id = #{id}
</update>
</mapper>
org.mybatis.caches.ehcache.EhcacheCache该类可以在mybatis-ehcache的jar包中可以找到。
由于采用ehcache缓存,实体类不再需要序列化,因此修改实体类,去掉实现序列化。
然后,在测试类中添加名为“TestUsingEhcache”的测试方法用于测试ehcache缓存是否开启,具体代码如下:
@Test
public void TestUsingEhcache(){
//第一次查询
bookDao = sqlSession.getMapper(IBookDao.class);
Book book1 = bookDao.findBookById(1);
System.out.println("第一次查询:"+book1);
sqlSession.close();
try {
sqlSession = dataConnection.getSqlSession();
} catch (IOException e) {
e.printStackTrace();
}
//第二次查询
bookDao = sqlSession.getMapper(IBookDao.class);
Book book2 = bookDao.findBookById(1);
System.out.println("第二次查询:"+book2);
}
最后,运行该测试方法,查询在控制台的输出日志信息,发现ehcache缓存开启。
5.4 ehcache在不同SQL映射文件中的个性化设置
在ehcache.xml中设置的属性值,会对该项目中所有使用ehcache缓存机制的缓存区域起作用。一个项目中可以有多个SQL映射文件,不同的SQL映射文件有不同的缓存区域。对于不同缓存区域也可进行专门针对当前区域的个性化设置,可通过指定不同SQL映射文件的cache属性值来设置。
在SQL映射文件中的cache属性值的优先级要高于ehcache.xml中的属性值。例如修改BookMapper.xml中的cache属性值,对该SQL映射文件实现个性化设置,具体配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ccff.mybatis.dao.IBookDao">
<!--<cache eviction="FIFO" flushInterval="10800000" readOnly="true" size="512"/>-->
<cache type="org.mybatis.caches.ehcache.EhcacheCache">
<property name="maxElementsInMemory" value="5000" />
<property name="timeToIdleSeconds" value="240" />
</cache>
<!--根据图书的id查询相应的图书信息-->
<select id="findBookById" parameterType="int" resultType="Book">
select * from book where id = #{id}
</select>
<select id="findBookById2" parameterType="int" resultType="Book" useCache="false">
select * from book where id = #{id}
</select>
<!--验证增删改操作会清空一级缓存:根据图书的id修改相应的图书信息-->
<update id="updateBookById" parameterType="Book" flushCache="false">
update book set number = #{number} where id = #{id}
</update>
</mapper>