MyBatis 的缓存分为一级缓存和二级缓存。
先说一下一级缓存的使用:
MyBatis一级缓存
MyBatis 的一级缓存是基于 SqlSession
的,在同一个SqlSession
对象的生命周期中,MyBatis 会把执行的方法和参数通过算法生成缓存的key,将key和查询结果放到一个 Map 对象中去,执行参数相同的同一条查询操作得到的是同一个实例对象。
MyBatis 默认开启一级缓存。
示例代码:
package com.kay;
import com.kay.dao.UserMapper;
import com.kay.entity.User;
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.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.io.Reader;
/**
* Created by kay on 2018/3/2.
*
* 测试一级缓存在 同一个SqlSession内保存
*
*/
public class UserMapperTest {
private static SqlSessionFactory sqlSessionFactory;
@BeforeClass
public static void init(){
try {
Reader reader = Resources.getResourceAsReader("SqlMapConfig.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void testSelect(){
SqlSession sqlSession = sqlSessionFactory.openSession();
User user1=null;
try {
user1 = (User) sqlSession.selectOne("getById",1);
System.out.println("user1:"+user1);
// user1.setName("xxxx"); //TODO 其实只是更新了缓存中的数据?
// sqlSession.commit(); //TODO 加上这句就不会走缓存了
User user2 = (User)sqlSession.selectOne("getById", 1);
System.out.println("user2:"+user2);
}finally {
sqlSession.close();
}
sqlSession = sqlSessionFactory.openSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user3 = userMapper.getById(1);
System.out.println("user3:"+user3);
//Assert.assertNotEquals(user1,user3); //不同sqlSession不是同一个对象
// userMapper.updateUser(new User(2, "newUser")); //TODO 更新之后就不走缓存了
User user4 = userMapper.getById(1);
// Assert.assertEquals(user3,user4); //是同一个对象
}finally {
sqlSession.close();
}
}
}
从日志可以看到:在同一个sqlSession 对象中,都是只执行了一次查询,对象为同一对象。
通过clear 或者 commit 操作之后就会更新缓存。(update/delete/insert修改数据)
来通过源码来看一下一级缓存的查找过程:
1.首先去MapperProxy代理里面执行invoke:
可以看到 通过 cachedMapperMethod()
方法获取一个接口方法,然后执行该查询。
2.进入execute调用里面,实则是调用了 SqlSession的selectOne方法:
selectOne最后也是调用的selectList 方法,传入 语句和参数:
调用 DefaultSqlSession 的selectList方法,拿到sql语句的id取配置里面获取这个执行对象,DefaultSqlSession对象里面持有了一个 CachingExecutor 对象
CachingExecutor 对象根据执行方法和参数算出缓存的key值:
然后调用自身的query方法去查缓存:
为什么这个Cache为空呢?因为这个缓存是二级缓存,我们并没有打开,于是就委托给一个代理去执行查询,看到这里就知道,缓存是先从二级里面去找的。来看看这个代理对象时什么:
通过调试看到代理是Executor接口的一个实现类:SimpleExecutor ,于是我们找到这个类里面去看它的query 方法做了什么:
但是我们发现它并没有query方法,于是我们想到一般的接口都会有一个顶层的抽象类,封装一些通用的处理方法,减少重复工作,果不其然,我们继续找到了BaseExecutor,它里面就有query方法:看它拿到缓存key值之后做了什么:
关键就是这句代码了:
list = resultHandler == null?(List)this.localCache.getObject(key)
看名字我们就知道了,this.localCache.getObject(key)
去本地缓存找对应key的value!
看看这个localCache 是个啥:
进入该类:噢,原来里面有个HashMap:
执行完查询后list就有值了:
到这里,我们就知道PerpetualCache
就是来放一级缓存的Map包装!
MyBatis 二级缓存
首先打开二级缓存配置:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
二级缓存是基于mapper命名空间的(或者说sqlSesionFactory),在需要的namespace里面设置缓存:
<!-- 启用缓存-->
<cache/>
话不多说,还是同一段代码:
package com.kay;
import com.kay.dao.UserMapper;
import com.kay.entity.User;
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.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.io.Reader;
/**
* Created by kay on 2018/3/2.
*
* 测试二级缓存在 同一个 namespace /SqlSessionFactory 内保存
*
*/
public class UserMapperCache2 {
private static SqlSessionFactory sqlSessionFactory;
@BeforeClass
public static void init(){
try {
Reader reader = Resources.getResourceAsReader("SqlMapConfig.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void testSelect(){
SqlSession sqlSession = sqlSessionFactory.openSession();
User user1=null;
try {
user1 = (User) sqlSession.selectOne("getById",1);
System.out.println("user1:"+user1);
User user2 = (User)sqlSession.selectOne("getById", 1);
System.out.println("user2:"+user2);
//到这里还是使用的一级缓存,所有 user1和user2 是同一个对象
Assert.assertEquals(user1,user2); //是同一个对象
}finally {
sqlSession.close();
}
//开启新的sqlSession,TODO 此处开始变为二级缓存,
sqlSession = sqlSessionFactory.openSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user3 = userMapper.getById(1);
System.out.println("user3:"+user3);
Assert.assertNotEquals(user1,user3); //不同sqlSession不是同一个对象
User user4 = userMapper.getById(1);
System.out.println("user4:"+user4);
Assert.assertEquals(user3,user4); //是同一个对象
}finally {
sqlSession.close();
}
}
}
user1和user2在同一个sqlSession内,其实用的还是一级缓存,此时二级缓存中还没有东西,所以二次查询缓存命中都是为0.
关闭sqlSession之后,缓存被同步到了二级缓存之中,新开启sqlSession后一级缓存失效,进入二级缓存,此时命中,也就是三分之一,0.333333.下次查询又是命中二级缓存,命中率为四分之二,0.5.
为什么后面二次都不是同一个对象了呢??
首先看一下我们的缓存怎么配置的:<cache/>
看下缓存还可以怎么设置,有哪些属性:
<!--1.收回策略:LRU/FIFO/SOFT/WEAK
2.刷新间隔 毫秒单位
3.引用数目,默认1024个(无论集合或对象)
4.只读属性,默认false 开启后会返回对象的拷贝,慢/更安全-->
<cache eviction="FIFO"
flushInterval="600000"
size="1024"
readOnly="true"/>
注意 readOnly 这个属性,默认为可读写,此时会采用序列化的方式将对象缓存,使用到的类是:SerializedCache
,当我们设置不同的读写属性时会发生什么呢:
- 可读写(readOnly = false) 默认使用 序列化对象,获取时反序列化会得到新的对象实例
- 只读 (readOnly = true ) 使用Map 来存储对象,所以返回同一个实例
这就是为什么返回的不是一个实例。
关于二级缓存的深入解析,参考《MyBatis 从入门到精通》作者的一篇博文《深入了解MyBatis二级缓存》,我就不班门弄斧了,这篇文章分析的很透彻。
连接:http://blog.csdn.net/isea533/article/details/44566257
祝大家元宵节快乐,晚安
2018/3/3 kay