0065 MyBatis一级缓存与二级缓存

数据库中数据虽多,但访问频率却不同,有的数据1s内就会有多次访问,而有些数据几天都没人查询,这时候就可以将访问频率高的数据放到缓存中,就不用去数据库里取了,提高了效率还节约了数据库资源

MyBatis有两种缓存,分为一级和二级缓存。
一级缓存作用域为SqlSession,同一个SqlSession才能访问到,采用HashMap存储数据,当该SqlSession进行了DML操作或者调用close()方法,其中的一级缓存就会清空。一级缓存默认开启。

二级缓存的作用域是mapper映射配置文件,多个SqlSession共用二级缓存,二级缓存仍然使用HashMap存储数据,如果多个SqlSession执行同一个命名空间的同一条sql,且传入的参数也相同,就是说最终执行相同的sql的话,就会从二级缓存中拿数据。二级缓存需要配置,默认不开启

一级缓存

sql映射器配置xml文件:

<mapper namespace="net.sonng.mbt.mapper.BookMapper">
    <select id="findAll" resultType="net.sonng.mbt.entity.Book">
        SELECT * FROM book
    </select>
    
    <select id="findBookById" parameterType="int" resultType="net.sonng.mbt.entity.Book">
        SELECT * FROM book WHERE id=#{id}
    </select>
    
    <update id="updateBook" parameterType="net.sonng.mbt.entity.Book" >
        UPDATE book 
        <set>
            <if test="name!=null" >`name`=#{name},</if>
            <if test="press!=null" >press=#{press},</if>
            <if test="author!=null" >author=#{author},</if>
            <if test="isbn!=null" >isbn=#{isbn},</if>
            <if test="douban!=null" >douban=#{douban}</if>
        </set>
        WHERE id=${id}
    </update>
    <select id="findBookInId" parameterType="list" resultType="net.sonng.mbt.entity.Book">
        SELECT * FROM book WHERE id In 
        <foreach item="id" index="i" collection="list" open="(" separator="," close=")">
            #{id}
        </foreach>
    </select>
</mapper>

映射器接口略
测试类:

package net.sonng.mbt.test;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import net.sonng.mbt.entity.Book;
import net.sonng.mbt.mapper.BookMapper;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class BookTest {
    public static void main(String[] args) throws IOException{
        InputStream inputStream=Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession session=sqlSessionFactory.openSession();
        BookMapper bookMapper=session.getMapper(BookMapper.class);
        
        Book book0=bookMapper.findBookById(5);      //第一次查询
        System.out.println(book0);
        
        Book book1=bookMapper.findBookById(5);      //同样的语句,第二次查询
        System.out.println(book1);
        book1.setDouban(9.9f);
        bookMapper.updateBook(book1);               //DML语句,更改了数据
        session.commit();                           //注意这里,有没有commit效果一样,都会更新数据库,清除一级缓存
        
        Book book2=bookMapper.findBookById(5);      //同样的语句,第三次查询
        System.out.println(book2);
        session.close();
    }
}

输出如下:

DEBUG [main] - ==> Preparing: SELECT * FROM book WHERE id=? //第一次查询
DEBUG [main] - > Parameters: 5(Integer)
DEBUG [main] - <
Total: 1
Book [id=5, name=深入理解Java 7 核心技术与最佳实践, press=机械工业出版社, author=成富著, isbn=9787111380399, douban=9.9]
Book [id=5, name=深入理解Java 7 核心技术与最佳实践, press=机械工业出版社, author=成富著, isbn=9787111380399, douban=9.9] //第二次查询,没有执行sql语句,直接从SqlSession一级缓存中拿到数据
DEBUG [main] - ==> Preparing: UPDATE book SET name=?, press=?, author=?, isbn=?, douban=? WHERE id=5 //更新了数据
DEBUG [main] - > Parameters: 深入理解Java 7 核心技术与最佳实践(String), 机械工业出版社(String), 成富著(String), 9787111380399(String), 9.9(Float) //update语句清空了一级缓存
DEBUG [main] - <
Updates: 1
DEBUG [main] - ==> Preparing: SELECT * FROM book WHERE id=? //第三次查询,一级缓存已被情况,执行sql语句入库查询
DEBUG [main] - > Parameters: 5(Integer)
DEBUG [main] - <
Total: 1
Book [id=5, name=深入理解Java 7 核心技术与最佳实践, press=机械工业出版社, author=成富著, isbn=9787111380399, douban=9.9]

测试结论之一:不管前面的查询有没有提交事务,查询结果都会写到一级缓存中;如果session执行了DML,一级缓存会被清空
特别注意的是:要是同一条sql语句才会从缓存中拿数据,比如看下面的测试代码:

package net.sonng.mbt.test;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import net.sonng.mbt.entity.Book;
import net.sonng.mbt.mapper.BookMapper;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class BookTest {
    public static void main(String[] args) throws IOException{
        InputStream inputStream=Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession session=sqlSessionFactory.openSession();
        BookMapper bookMapper=session.getMapper(BookMapper.class);
        List<Integer> ids=new ArrayList<Integer>();
        ids.add(1);
        ids.add(2);
        ids.add(4);
        ids.add(5);
        List<Book> books0=bookMapper.findBookInId(ids);  //第一次查询
        for(Book b:books0){
            System.out.println(b);
        }
        
        List<Book> books1=bookMapper.findBookInId(ids);  //同一条语句,第二次查询
        for(Book b:books1){
            System.out.println(b);
        }
        
        Book book0=bookMapper.findBookById(5);           //虽然缓存中有这条记录,但还是执行了一次sql查询
        System.out.println(book0);
        
        session.close();
    }
}

输出如下:

DEBUG [main] - ==> Preparing: SELECT * FROM book WHERE id In ( ? , ? , ? , ? ) //第一次查询
DEBUG [main] - > Parameters: 1(Integer), 2(Integer), 4(Integer), 5(Integer)
DEBUG [main] - <
Total: 4
Book [id=1, name=深入理解Java虚拟机 JVM高级特性与最佳实践, press=机械工业出版社, author=周志明著, isbn=9787111421900, douban=8.8]
Book [id=2, name=疯狂Java讲义 第3版, press=电子工业出版社, author=李刚著, isbn=9787121236693, douban=7.8]
Book [id=4, name=Java编程思想 第4版, press=机械工业出版社, author=(美)Bruce Eckel著, isbn=9787111213826, douban=9.1]
Book [id=5, name=深入理解Java 7 核心技术与最佳实践, press=机械工业出版社, author=成富著, isbn=9787111380399, douban=9.9]
Book [id=1, name=深入理解Java虚拟机 JVM高级特性与最佳实践, press=机械工业出版社, author=周志明著, isbn=9787111421900, douban=8.8] //同一条语句,第二次查询,从缓存中拿数据,没有执行新的sql查询
Book [id=2, name=疯狂Java讲义 第3版, press=电子工业出版社, author=李刚著, isbn=9787121236693, douban=7.8]
Book [id=4, name=Java编程思想 第4版, press=机械工业出版社, author=(美)Bruce Eckel著, isbn=9787111213826, douban=9.1]
Book [id=5, name=深入理解Java 7 核心技术与最佳实践, press=机械工业出版社, author=成富著, isbn=9787111380399, douban=9.9]
DEBUG [main] - ==> Preparing: SELECT * FROM book WHERE id=? //其实要查询的数据在缓存已经存在,但是还是执行了一次查询
DEBUG [main] - > Parameters: 5(Integer)
DEBUG [main] - <
Total: 1
Book [id=5, name=深入理解Java 7 核心技术与最佳实践, press=机械工业出版社, author=成富著, isbn=9787111380399, douban=9.9]

测试结论之二:要最终执行相同的sql语句,才会到一级缓存中拿数据

MyBatis使用HashMap存储缓存信息,key根据查询出来的对象生成,value是查询出来的对象

使用一级缓存要特别注意:缓存的作用范围是SqlSession,假如SqlSessionA中已经有了一级缓存,而SqlSessionB修改了数据库的数据,此时SqlSessionA再执行相同查询,会从SqlSessionA的一级缓存中拿数据,导致跟数据库中的不同。关于这一点,是否有别的配置可以解决,还没见到,或许要通过编程解决,看下面的测试代码:

package net.sonng.mbt.test;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import net.sonng.mbt.entity.Book;
import net.sonng.mbt.mapper.BookMapper;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class BookTest {
    public static void main(String[] args) throws IOException{
        InputStream inputStream=Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession session1=sqlSessionFactory.openSession();

        BookMapper bookMapper1=session1.getMapper(BookMapper.class);
        Book book1=bookMapper1.findBookById(5);                      //session1的第一次查询,入库查询
        System.out.println(book1);
        Book book2=bookMapper1.findBookById(5);                      //session1的第二次查询,从一级缓存中拿数据
        System.out.println(book2);
        
        SqlSession session2=sqlSessionFactory.openSession();     
        BookMapper bookMapper2=session2.getMapper(BookMapper.class);
        Book book3=bookMapper2.findBookById(5);                      //session2的第一次查询,入库查询
        book3.setDouban(10.0f);
        bookMapper2.updateBook(book3);
        session2.commit();                                           //session2修改了数据库中的数据,并且提交了事务
        Book book4=bookMapper2.findBookById(5);                      //session2的第二次查询,入库查询
        System.out.println(book4);
        session2.commit();
        
        Book book5=bookMapper1.findBookById(5);                       //session1的第三次查询,从一级缓存中拿到过时的数据
        System.out.println(book5);
    }
}

输出如下:

DEBUG [main] - ==> Preparing: SELECT * FROM book WHERE id=? //session1第一次查询,执行sql,入库查询
DEBUG [main] - > Parameters: 5(Integer)
DEBUG [main] - <
Total: 1
Book [id=5, name=深入理解Java 7 核心技术与最佳实践, press=机械工业出版社, author=成富著, isbn=9787111380399, douban=6.9]
Book [id=5, name=深入理解Java 7 核心技术与最佳实践, press=机械工业出版社, author=成富著, isbn=9787111380399, douban=6.9] //session1第二次查询,从一级缓存中拿数据
DEBUG [main] - ==> Preparing: SELECT * FROM book WHERE id=? //session2第一次查询,执行sql,入库查询
DEBUG [main] - > Parameters: 5(Integer)
DEBUG [main] - <
Total: 1
DEBUG [main] - ==> Preparing: UPDATE book SET name=?, press=?, author=?, isbn=?, douban=? WHERE id=5 //session2修改了数据库的数据,且提交了事务
DEBUG [main] - > Parameters: 深入理解Java 7 核心技术与最佳实践(String), 机械工业出版社(String), 成富著(String), 9787111380399(String), 10.0(Float)
DEBUG [main] - <
Updates: 1
DEBUG [main] - ==> Preparing: SELECT * FROM book WHERE id=? //session2第二次查询,执行sql,入库查询
DEBUG [main] - > Parameters: 5(Integer)
DEBUG [main] - <
Total: 1
Book [id=5, name=深入理解Java 7 核心技术与最佳实践, press=机械工业出版社, author=成富著, isbn=9787111380399, douban=10.0] //由此可以看出,数据库中的数据已经被修改
Book [id=5, name=深入理解Java 7 核心技术与最佳实践, press=机械工业出版社, author=成富著, isbn=9787111380399, douban=6.9] //session1第三次查询,从一级缓存中拿到了过时的数据

测试结论之三:sessionA修改了数据库的数据,不会影响到sessionB的一级缓存数据

二级缓存

二级缓存是跨SqlSession的,其作用范围是同一个mapper映射文件,就是同一个命名空间。
MyBatis默认开启一级缓存,但没有开启二级缓存,因此还需要配置

在mybatis-config.xml的settings元素中开启二级缓存

    <settings>
        <setting name="logImpl" value="LOG4J" />
        <setting name="cacheEnabled" value="true" />
    </settings>

因为二级缓存是基于mapper映射文件的,因此映射文件中还要进行一些配置:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//ibatis.apache.org//DTDMapper 3.0//EN" "http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<mapper namespace="net.sonng.mbt.mapper.BookMapper">
    <cache eviction="LRU" flushInterval="60000" size="512" readOnly="true" />  <!-- 二级缓存配置 -->
    <select id="findAll" resultType="net.sonng.mbt.entity.Book">
        SELECT * FROM book
    </select>
    
    <select id="findBookById" parameterType="int" resultType="net.sonng.mbt.entity.Book">
        SELECT * FROM book WHERE id=#{id}
    </select>
    <select id="findBooks" parameterType="net.sonng.mbt.entity.Book" resultType="net.sonng.mbt.entity.Book">
        SELECT * FROM book WHERE 
            <choose>
                <when test="isbn!=null" >
                    isbn=#{isbn}
                </when>
                <when test="name!=null">
                    `name`=#{name}
                </when>
                <otherwise>
                    douban&gt;7
                </otherwise>
            </choose>
    </select>
    
    <update id="updateBook" parameterType="net.sonng.mbt.entity.Book" >
        UPDATE book 
        <set>
            <if test="name!=null" >`name`=#{name},</if>
            <if test="press!=null" >press=#{press},</if>
            <if test="author!=null" >author=#{author},</if>
            <if test="isbn!=null" >isbn=#{isbn},</if>
            <if test="douban!=null" >douban=#{douban}</if>
        </set>
        WHERE id=${id}
    </update>
    <select id="findBookInId" parameterType="list" resultType="net.sonng.mbt.entity.Book">
        SELECT * FROM book WHERE id In 
        <foreach item="id" index="i" collection="list" open="(" separator="," close=")">
            #{id}
        </foreach>
    </select>
    
    <select id="findBookLikeName" parameterType="string" resultType="net.sonng.mbt.entity.Book">
        <bind name="pattern" value="'%'+_parameter+'%'" />
        SELECT * FROM book WHERE `name` LIKE #{pattern}
    </select>
</mapper>

cache元素的几个属性:
----flushInterval:刷新间隔,单位毫秒。默认不设置,即不刷新,仅在调用语句时刷新。
----size:缓存的数目,默认1024
----readOnly:是否只读,默认false。true表示多次查询时返回的是这个对象的引用;false表示返回的是对象的拷贝
----eviction:收回策略,默认LRU。
--------LRU:最近最少使用策略,移除长时间不被使用的对象
--------FIFO:先进先出策略,按对象进入缓存的顺序移除
--------SOFT:软引用策略,移除基于垃圾回收器状态和软引用规则的对象
--------WEAK:弱引用策略

实体类还要实现Serializable接口

二级缓存测试之一:

package net.sonng.mbt.test;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import net.sonng.mbt.entity.Book;
import net.sonng.mbt.mapper.BookMapper;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class BookTest {
    public static void main(String[] args) throws IOException{
        InputStream inputStream=Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
        
        SqlSession session1=sqlSessionFactory.openSession();
        BookMapper bookMapper1=session1.getMapper(BookMapper.class);
        Book book1=bookMapper1.findBookById(5);  //第一次查询
        System.out.println(book1);
        session1.commit();  //session1提交事务,特别注意:如果这里不commit,那么没有二级缓存
        
        SqlSession session2=sqlSessionFactory.openSession();
        BookMapper bookMapper2=session2.getMapper(BookMapper.class);
        Book book2=bookMapper2.findBookById(5);  //第二次查询
        System.out.println(book2);
        session2.close();
        session1.close();
    }
}

输出如下:

DEBUG [main] - Cache Hit Ratio [net.sonng.mbt.mapper.BookMapper]: 0.0 //session1第一次查询,二级缓存中没有数据
DEBUG [main] - ==> Preparing: SELECT * FROM book WHERE id=?
DEBUG [main] - > Parameters: 5(Integer)
DEBUG [main] - <
Total: 1
Book [id=5, name=深入理解Java 7 核心技术与最佳实践, press=机械工业出版社, author=成富著, isbn=9787111380399, douban=10.0]
DEBUG [main] - Cache Hit Ratio [net.sonng.mbt.mapper.BookMapper]: 0.5 //session2第一次查询,从二级缓存中命中数据
Book [id=5, name=深入理解Java 7 核心技术与最佳实践, press=机械工业出版社, author=成富著, isbn=9787111380399, douban=10.0]

测试结论之四:sessionA执行一次查询,如果提交了事务,查询的结果会写到二级缓存中,sessionB执行相同查询,会查询到二级缓存中的数据。注意要提交事务才会写到二级缓存中。

二级缓存测试之二:

package net.sonng.mbt.test;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import net.sonng.mbt.entity.Book;
import net.sonng.mbt.mapper.BookMapper;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class BookTest {
    public static void main(String[] args) throws IOException{
        InputStream inputStream=Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
        
        SqlSession session1=sqlSessionFactory.openSession();
        BookMapper bookMapper1=session1.getMapper(BookMapper.class);
        Book book1=bookMapper1.findBookById(5);  //session1第一次查询
        System.out.println(book1);
        session1.commit();
        book1.setDouban(10.0f);
        bookMapper1.updateBook(book1);            //session1修改了数据
        session1.commit();               //标注一。
        
        SqlSession session2=sqlSessionFactory.openSession();
        BookMapper bookMapper2=session2.getMapper(BookMapper.class);
        Book book2=bookMapper2.findBookById(5);   //session2第一次查询
        System.out.println(book2);
        session2.close();
    }
}

输出如下:

DEBUG [main] - Cache Hit Ratio [net.sonng.mbt.mapper.BookMapper]: 0.0
DEBUG [main] - ==> Preparing: SELECT * FROM book WHERE id=?
DEBUG [main] - > Parameters: 5(Integer)
DEBUG [main] - <
Total: 1
Book [id=5, name=深入理解Java 7 核心技术与最佳实践, press=机械工业出版社, author=成富著, isbn=9787111380399, douban=6.9]
DEBUG [main] - ==> Preparing: UPDATE book SET name=?, press=?, author=?, isbn=?, douban=? WHERE id=5
DEBUG [main] - > Parameters: 深入理解Java 7 核心技术与最佳实践(String), 机械工业出版社(String), 成富著(String), 9787111380399(String), 10.0(Float)
DEBUG [main] - <
Updates: 1
DEBUG [main] - Cache Hit Ratio [net.sonng.mbt.mapper.BookMapper]: 0.0 //session2的第一次查询,没有命中,需要入库查询,说明session1的update语句清除了二级缓存
DEBUG [main] - ==> Preparing: SELECT * FROM book WHERE id=?
DEBUG [main] - > Parameters: 5(Integer)
DEBUG [main] - <
Total: 1
Book [id=5, name=深入理解Java 7 核心技术与最佳实践, press=机械工业出版社, author=成富著, isbn=9787111380399, douban=10.0]

如果把测试代码二的标注一这行代码注释掉的话,也就是不提交事务,输出如下:

DEBUG [main] - Cache Hit Ratio [net.sonng.mbt.mapper.BookMapper]: 0.0
DEBUG [main] - ==> Preparing: SELECT * FROM book WHERE id=?
DEBUG [main] - > Parameters: 5(Integer)
DEBUG [main] - <
Total: 1
Book [id=5, name=深入理解Java 7 核心技术与最佳实践, press=机械工业出版社, author=成富著, isbn=9787111380399, douban=6.9]
DEBUG [main] - ==> Preparing: UPDATE book SET name=?, press=?, author=?, isbn=?, douban=? WHERE id=5
DEBUG [main] - > Parameters: 深入理解Java 7 核心技术与最佳实践(String), 机械工业出版社(String), 成富著(String), 9787111380399(String), 10.0(Float)
DEBUG [main] - <
Updates: 1
DEBUG [main] - Cache Hit Ratio [net.sonng.mbt.mapper.BookMapper]: 0.5 //session2的第一次查询,命中了数据,二级缓存中还有数据
Book [id=5, name=深入理解Java 7 核心技术与最佳实践, press=机械工业出版社, author=成富著, isbn=9787111380399, douban=10.0]

再看数据库,数据并没有被修改,这里出现了脏读。没有提交事务,这里的update语句只修改了缓存中的数据,数据库中的数据并没有修改,另一个sqlSession脏读到了缓存中的数据。
测试结论之五:sessionA如果执行了DML操作且提交了事务,会清空二级缓存;如果只执行DML操作但不提交事务,那么会修改二级缓存中的数据,而不会实际修改数据库的数据,这时候会导致sessionA或其他session的脏读。
实际上只要该mapper命名空间下的sql执行了DML并提交事务,该命名空间的二级缓存都会清空。

小结

一级缓存默认开启,二级缓存默认不开启,需要到MyBatis-config.xml中开启
一级缓存的作用范围是SqlSession,二级缓存是跨SqlSession的,是mapper映射文件级别的
一级缓存不管是否提交事务,前面的查询结果都会写到一级缓存中;但二级缓存有区别,前面的查询提交了事务,才会把结果写到二级缓存中
一级缓存会在session执行了DML时清空
二级缓存会在命名空间下的DML语句被执行并提交了事务时清空
顺序:二级缓存、一级缓存、入库查询
其他:
CSDN 《深入理解mybatis原理》 MyBatis的二级缓存的设计原理
CSDN mybatis 一级缓存和二级缓存简介

posted @ 2017-04-09 16:17  sonng  阅读(488)  评论(0编辑  收藏  举报