MyBatis使用篇(八)—— MyBatis缓存结构

16 篇文章 0 订阅
14 篇文章 0 订阅

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>
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值