使用缓存可以是应用更快的获取数据,避免频繁的数据库交互,尤其是在查询越多、缓存命中率越高的情况下,使用缓存的作用就越明显。MyBatis作为持久层框架,提供了强大的查询缓存特性,可非常方便的配置和使用。MyBatis系统中默认定义了两级缓存:一级缓存和二级缓存。
1、默认情况下,一级缓存(SqlSession级别的缓存,也称为本地缓存)是开启的,并且不能控制。 2、二级缓存需要手动开启和配置,他是和命名空间绑定的,即二级缓存需要在全局配置文件中开启,并且在sql映射文件中指定二级缓存使用的缓存中间件(MyBatis有自己的实现,但是毫不客气的说和专业的缓存中间件一比就是个弟弟!!!) 3、为了提高扩展性。MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存(或者使用第三方缓存)
1、一级缓存
先通过一个简单的演示看看MyBatis一级缓存是如何起作用的。
package test;
import com.xust.iot.beans.User;
import com.xust.iot.mapper.StudentMapper;
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 org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.List;
public class AppTest {
private static SqlSessionFactory sqlSessionFactory;
private static Logger log = Logger.getLogger(AppTest.class);
@Before
public void init() {
String resource = "mybatis-config.xml";
try {
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 测试MyBatis一级缓存的特性
*/
@Test
public void test01() {
SqlSession session = sqlSessionFactory.openSession(true);
try {
StudentMapper sm = session.getMapper(StudentMapper.class);
List<User> user1 = sm.getUserByName("小明");
if (null != user1 && user1.size() > 0) {
for (User u : user1) {
System.out.println(u.toString());
}
}
System.out.println("第二次查询\"小明\"............");
List<User> user2 = sm.getUserByName("小明");
if (null != user1 && user1.size() > 0) {
for (User u : user1) {
System.out.println(u.toString());
}
}
System.out.println("user1==user?==>" + (user1 == user2));
} catch (Exception e) {
log.error(e + "---" + new Date());
} finally {
session.close();
}
}
}
运行结果截图如下:

可以看到,两次查询值MyBatis只给数据库发送了一次SQL语句,但是两次查询的结果都是一样的,而且再往下发现两个List<User>对象竟然是同一个对象,之所以这样就是MyBatis的一级缓存在起作用。 在同一个SqlSession中查询是MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入PerpetualCache 的 HashMap本地缓存对象中。如果同一个SqlSession中执行的方法和参数完全一致,闹通过算法就会生成相同的键值,当Map缓存对象中已经存在该键值时,就会返回缓存中的对象,而不会再给数据库发sql语句了。 但是要注意,下面几种情况发生会使一级缓存会失效:
- 1. 不同的SqlSession用不同的一级缓存,他们之间的数据不能共享
- 2. 就算是同一个SqlSession对象,但是如果执行的方法和参数不同也不行
- 3. 默认情况下,在SqlSession期间执行任何一次增删改操作,一级缓存会被清空
- 4. 手动清空一级缓存,一级缓存也会失效
package test;
import com.xust.iot.beans.User;
import com.xust.iot.mapper.StudentMapper;
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 org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.List;
public class AppTest {
private static SqlSessionFactory sqlSessionFactory;
private static Logger log = Logger.getLogger(AppTest.class);
@Before
public void init() {
String resource = "mybatis-config.xml";
try {
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* MyBatis的一级缓存存在于SqlSession生命周期中,在同一个SqlSession中查询是MyBatis会把
* 执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map中。
*/
@Test
public void test02() {
//第一个会话
SqlSession session = sqlSessionFactory.openSession(true);
StudentMapper sm = session.getMapper(StudentMapper.class);
List<User> user1 = sm.getUserByName("小明");
if (null != user1 && user1.size() > 0) {
for (User u : user1) {
System.out.println(u.toString());
}
}
//第二个会话
System.out.println("开启新的会话......");
SqlSession session2 = sqlSessionFactory.openSession(true);
StudentMapper sm2 = session2.getMapper(StudentMapper.class);
List<User> user2 = sm2.getUserByName("小明");
if (null != user2 && user2.size() > 0) {
for (User u : user2) {
System.out.println(u.toString());
}
}
session.close();
session2.close();
}
}
测试结果如下:

MyBatis的<insert>、<delete>、<update>和<select>标签都有一个属性:flushCache
,在默认情况下,对于增删改操作这个标签默认值是true,也就是每次操作后要清空缓存,而对于<select>操作这个属性默认值是false,也即不刷新缓冲。也就是下面这段代码要说明的:
package test;
import com.xust.iot.beans.User;
import com.xust.iot.mapper.StudentMapper;
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 org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.List;
public class AppTest {
private static SqlSessionFactory sqlSessionFactory;
private static Logger log = Logger.getLogger(AppTest.class);
@Before
public void init() {
String resource = "mybatis-config.xml";
try {
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void test03() {
SqlSession session = sqlSessionFactory.openSession(true);
StudentMapper sm = session.getMapper(StudentMapper.class);
List<User> user1 = sm.getUserByName("小明");
if (null != user1 && user1.size() > 0) {
for (User u : user1) {
System.out.println(u.toString());
}
}
//执行任意一次增删改操作,当前SqlSession的一级缓存立即被清空
System.out.println("删除一个用户......");
sm.deleteUserById(14);
System.out.println("删除完成.......");
//由于缓存被清了,因此还得给数据库发sql语句查询
StudentMapper sm2 = session.getMapper(StudentMapper.class);
List<User> user2 = sm2.getUserByName("小明");
if (null != user2 && user2.size() > 0) {
for (User u : user2) {
System.out.println(u.toString());
}
}
session.close();
}
}
测试结果如下:

下面的这种情况就更直观了,但本质上和上面那种情况是一样的——都是一级缓存被清空了。
package test;
import com.xust.iot.beans.User;
import com.xust.iot.mapper.StudentMapper;
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 org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.List;
public class AppTest {
private static SqlSessionFactory sqlSessionFactory;
private static Logger log = Logger.getLogger(AppTest.class);
@Before
public void init() {
String resource = "mybatis-config.xml";
try {
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void test03() {
SqlSession session = sqlSessionFactory.openSession(true);
StudentMapper sm = session.getMapper(StudentMapper.class);
List<User> user1 = sm.getUserByName("小明");
if (null != user1 && user1.size() > 0) {
for (User u : user1) {
System.out.println(u.toString());
}
}
//清除缓存
System.out.println("手动清除缓存......");
session.clearCache();
StudentMapper sm2 = session.getMapper(StudentMapper.class);
List<User> user2 = sm2.getUserByName("小明");
if (null != user2 && user2.size() > 0) {
for (User u : user2) {
System.out.println(u.toString());
}
}
session.close();
}
}
测试结果如下:

2、二级缓存
- 二级缓存默认也是采用 PerpetualCache,HashMap存储;
- 二级缓存的存储作用域为 Mapper(确切说是namespace),即一个Mapper执行了insert、update或delete操作,不影响另外一个Mapper(不同namespace);
- 二级缓存可自定义存储实现,如 Ehcache、redis;
- 二级缓存开启后,需要对应的java Bean实现,并且这个java Bean要实现Serializable接口进行序列化
2.1 配置二级缓存
二级缓存有两种配置方法,一种是基于Mapper.xml文件来配置;另一种就是基于Mapper.java接口来配置。下面分别来看看如何配置使用MyBatis的二级缓存: 首先,无论是通过Mapper.xml文件来配置,还是通过Mapper.java接口来配置,都需要在mybatis-config.xml文件中通过settings设置显式地开启二级缓存:
<!--一些有关于mybatis运行行为的设置-->
<settings>
<!--开启二级缓存-->
<setting name="cacheEnabled" value="true"/>
<!--其他的设置-->
</settings>
2.2 在Mapper.xml中配置二级缓存
在xml文件中配置的方法很简单,在保证二级缓存的全局配置开启的情况下,在UserMapper.xml中只需要添加<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.xust.iot.mapper.StudentMapper">
<!--在UserMapper中开启二级缓存-->
<cache></cache>
<select id="getUserByName" parameterType="string" resultType="user">
select * from user
<where>
name=#{username}
</where>
</select>
<delete id="deleteUserById" parameterType="integer" >
delete from user
<where>
id=#{userId}
</where>
</delete>
</mapper>
<cacha>标签的属性: * eviction:缓存回收策略:flushInterval:缓存多长时间清空一次,默认不清空,单位毫秒 ms LRU——最少使用的,移除最长时间不适用的对象; FIFO——先进先出 WEAK——弱引用,更积极的移除基于垃圾回收器状态和弱引用规则的对象 SOFT——软引用,更积极的移除基于垃圾回收器状态和弱引用规则的对象 * flushInterval:缓存多久清空一次,默认不清空,时间毫秒 ms * readOnly:缓存是否只读 * size:缓存觉存放元素个数,默认1024 * type:自定义缓存的全类名
2.3 在Mapper接口中配置二级缓存
在接口中配置主要是借助@CacheNamespace
这个注解,但是要注意:配置文件和接口注释是不能够配合使用的。只能通过全注解的方式或者全部通过xml配置文件的方式使用。也就是说你用了配置文件就不要用这种方式,用了接口配置的方式就别用xml配置文件的方式。如下:
package com.xust.iot.mapper;
import com.xust.iot.beans.User;
import org.apache.ibatis.annotations.*;
import java.util.List;
@CacheNamespace
public interface StudentMapper {
/**
* 查询所有姓名为name的用户
* @param name
* @return
*/
@Select("select * from user where name=#{username}")
@Options(useCache = true)
public List<User> getUserByName(@Param("username") String name);
/**
* 删除id=userId的用户
* @param id
*/
@Delete("delete from user where id=#{userId}")
public void deleteUserById(@Param("userId") Integer id);
}
并在mybatis-config.xml中重新配置Mapper映射文件:
<mappers>
<!-- <mapper resource="mapper/StudentMapper.xml"></mapper>-->
<mapper class="com.xust.iot.mapper.StudentMapper"/>
</mappers>
4、使用自带的MyBatis二级缓存
配置Mybatis二级缓存的方法有两种,只要配置好,二级缓存就可以工作了。但是在使用前需要注意的是,由于MyBatis使用SerializedCache序列化缓存来实现可读写缓存类,并通过序列化和反序列化来保证通过缓存获取数据时,得到的是一个新的实例。因此,如果配置了只读缓存,MyBatis就会使用Map来存储缓存值。而这个缓存类要求所有被序列化的对象必须实现Serializable接口 。因此我们的java Bean需要实现Serializable接口。
package com.xust.iot.beans;
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = -8963153814502574628L;
//其他属性
}
做好所有准备后,编写一个测试类来看看二级缓存的效果:
package test;
import com.xust.iot.beans.User;
import com.xust.iot.mapper.StudentMapper;
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 org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.List;
public class AppTest {
private static SqlSessionFactory sqlSessionFactory;
private static Logger log = Logger.getLogger(AppTest.class);
@Before
public void init() {
String resource = "mybatis-config.xml";
try {
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void testL2Cache() {
//第一个会话
SqlSession session = sqlSessionFactory.openSession(true);
StudentMapper sm = session.getMapper(StudentMapper.class);
List<User> user1 = sm.getUserByName("小明");
if (null != user1 && user1.size() > 0) {
for (User u : user1) {
System.out.println(u.toString());
}
}
session.close();
//第二个会话
System.out.println("开启新的会话......");
SqlSession session2 = sqlSessionFactory.openSession(true);
StudentMapper sm2 = session2.getMapper(StudentMapper.class);
List<User> user2 = sm2.getUserByName("小明");
if (null != user2 && user2.size() > 0) {
for (User u : user2) {
System.out.println(u.toString());
}
}
session2.close();
}
}
测试结果:

5、MyBatis缓存的执行逻辑
1.当一个SqlSession第一次执行一次select后,查到数据后会首先把查询到的结果保存到一级缓存中 2.当该SqlSession被关闭或者提交后,保存在一级缓存中的数据会转移到二级缓存中(前提是正确开启并配置了二级缓存) 3.当另一个SqlSession第一次执行同样select时,首先会在二级缓存中找,如果没找到,就去自己的一级缓存中找,找到了就返回,如果没找到就去数据库查,MyBatis就是通过这样的机制从而减少了数据库压力提高了性能。
MyBatis的执行流程总结起来就是:二级缓存-->一级缓存-->数据库

注意事项: 1. 如果SqlSession执行了DML操作(insert、update、delete),并commit了,那么mybatis就会清空当前mapper缓存中的所有缓存数据,这样可以保证缓存中的存的数据永远和数据库中一致,避免出现脏读 2. mybatis的缓存是基于[namespace:sql语句:参数]
来进行缓存的,意思就是:SqlSession的HashMap存储缓存数据时,是使用[namespace:sql:参数]
作为key,查询返回的语句作为value保存的。