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 一级缓存和二级缓存简介

上一篇:Python各式装饰器


下一篇:prop解决一个checkbox选中后再次选中失效的问题