MyBatis 一级缓存原理
优质博文:IT-BLOG-CN
一、一级缓存配置
MyBatis
一级缓存默认是开启的。如果需要显示的开启,需要在MyBaits
配置文件中<settings>
标签中添加如下语句:
<settings>
<setting name="localCacheScope" value="SESSION"/>
</settings>
value
共有两个选项,SESSION
或者STATEMENT
,默认是SESSION
级别,即在一个MyBatis
会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT
级别,可以理解为缓存只对当前执行的这一个Statement
有效。
一级缓存基于SqlSession
举个例子:
public void getStudentById() throws Exception {
SqlSession sqlSession = factory.openSession(true); // 自动提交事务
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
System.out.println(studentMapper.getStudentById(1));
System.out.println(studentMapper.getStudentById(1));
}
执行结果:我们可以看到,只有第一次真正查询了数据库,后续的查询使用了一级缓存。
DEBUG [main] - ==> Preparing: SELECT id,name,age FROM student WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <== Columns: id, name, age
TRACE [main] - <== Row: 1, 小明, 13
DEBUG [main] - <== Total: 1
StudentEntity{id=1, name='小明', age=13}
StudentEntity{id=1, name='小明', age=13}
二、一级缓存可重复读现象
两个SqlSession
操作当前行,一级缓存的可重复读案例。具体在sqlSession1
中查询数据,使一级缓存生效,在sqlSession2
中更新数据库,验证一级缓存只在数据库会话内部共享。
@Test
public void testLocalCacheScope() throws Exception {
SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
System.out.println(studentMapper.getStudentById(1));
System.out.println("更新了" + studentMapper2.updateStudentName("小花",1) + "名学生的数据");
System.out.println(studentMapper.getStudentById(1));
System.out.println(studentMapper2.getStudentById(1));
}
sqlSession2
更新了id
为1
的学生的姓名,从小明改为了小花,但session1
之后的查询中,id
为1
的学生的名字还是小明,出现了重复读,说明一级缓存只在数据库会话内部共享。
DEBUG [main] - ==> Preparing: SELECT id,name,age FROM student WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <== Columns: id, name, age
TRACE [main] - <== Row: 1, 小明, 13
DEBUG [main] - <== Total: 1
StudentEntity{id=1, name='小明', age=13}
DEBUG [main] - ==> Preparing: INSERT INTO student(name,age) VALUES(?,?)
DEBUG [main] - ==> Parameters: 小花(String), 13(Integer)
DEBUG [main] - <== Updates: 1
更新了1名学生的数据 --SqlSession2更新了数据
StudentEntity{id=1, name='小明', age=13} --SqlSession1读到了缓存中的数据
DEBUG [main] - ==> Preparing: SELECT id,name,age FROM student WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <== Columns: id, name, age
TRACE [main] - <== Row: 1, 小花, 13
DEBUG [main] - <== Total: 1
StudentEntity{id=1, name='小花', age=13}
如果你的业务不希望让MyBatis
的一级缓存进行可重复读,就需要进行一级缓存清除。
三、一级缓存清除方法
【推荐】在映射文件xml
中添加<select flushCache="true"></select>
【了解】执行SqlSession
的close
(会释放掉一级缓存PerpetualCache
对象)或clearCache
(会清空PerpetualCache
对象中的数据)方法
【了解】执行SqlSession
的commit
(执行插入、更新、删除操作后)
这里的更新指的是当前SqlSession
进行了增删改查操作。举个例子:
@Test
public void addStudent() throws Exception {
SqlSession sqlSession = factory.openSession(true); // 自动提交事务
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
System.out.println(studentMapper.getStudentById(1));
System.out.println("增加" + studentMapper.addStudent(buildStudent()) + "名学生");
System.out.println(studentMapper.getStudentById(1));
sqlSession.close();
}
执行结果:我们可以看到,在修改操作后执行的相同查询,查询了数据库,一级缓存失效。
DEBUG [main] - ==> Preparing: SELECT id,name,age FROM student WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <== Columns: id, name, age
TRACE [main] - <== Row: 1, 小明, 13
DEBUG [main] - <== Total: 1
StudentEntity{id=1, name='小明', age=13}
DEBUG [main] - ==> Preparing: INSERT INTO student(name,age) VALUES(?,?)
DEBUG [main] - ==> Parameters: 小李(String), 14(Integer)
DEBUG [main] - <== Updates: 1
添加1名学生
DEBUG [main] - ==> Preparing: SELECT id,name,age FROM student WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <== Columns: id, name, age
TRACE [main] - <== Row: 1, 小明, 13 -- 这里数据虽然一样,但是它是查询数据库获取的。
DEBUG [main] - <== Total: 1
四、源码分析
通过上面的使用,能够清楚的发现,一级缓存主要是基于SqlSession
的,所以我们主要对SqlSession
的原理进行说明。
如下图所示,MyBatis
一次会话对应一个SqlSession
对象。SqlSession
对象中包含一个Executor
,Executor
对象中创建一个本地缓存local cache
,对于每一次查询,都会尝试根据执行的语句生成的MappedStatement
的Hash
值去本地缓存中查找,如果在缓存中,就直接从缓存中取出,然后返回给用户;否则,从数据库读取数据,将查询结果存入缓存并返回给用户。
如上所示SqlSession
将它的工作交给了Executor
执行器这个角色来完成,负责完成对数据库的各种操作。当创建了一个SqlSession
对象时,MyBatis
会为这个SqlSession
对象创建一个新的Executor
执行器,而缓存信息就被维护在这个Executor
执行器中,MyBatis
将缓存和对缓存相关的操作封装成了Cache
接口中。具体实现类的类关系图如下图所示:
根据依赖关系图,可知Session
级别的一级缓存实际上就是使用PerpetualCache
维护的,我们就看看PerpetualCache
实现原理,其内部就是通过一个简单的HashMap<k,v>
来实现的,没有其他的任何限制。如下是PerpetualCache
的实现代码:
package org.apache.ibatis.cache.impl;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;
/**
* 使用简单的HashMap来维护缓存
* @author Clinton Begin
*/
public class PerpetualCache implements Cache {
private String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
public PerpetualCache(String id) {
this.id = id;
}
public String getId() {
return id;
}
public int getSize() {
return cache.size();
}
public void putObject(Object key, Object value) {
cache.put(key, value);
}
public Object getObject(Object key) {
return cache.get(key);
}
public Object removeObject(Object key) {
return cache.remove(key);
}
public void clear() {
cache.clear();
}
public ReadWriteLock getReadWriteLock() {
return null;
}
public boolean equals(Object o) {
if (getId() == null) throw new CacheException("Cache instances require an ID.");
if (this == o) return true;
if (!(o instanceof Cache)) return false;
Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}
public int hashCode() {
if (getId() == null) throw new CacheException("Cache instances require an ID.");
return getId().hashCode();
}
}
疑问:MyBatis
的一级缓存通过HashMap
存储的那么他的key
是怎么生成的尼?
对于每次的查询请求,Executor
都会根据传递的参数信息以及动态生成的SQL
语句,将上面的条件根据一定的计算规则,创建一个对应的CacheKey
对象。
CacheKey
的构建被放置到了Executor
接口的实现类BaseExecutor
中,定义如下:
/**
* 所属类: org.apache.ibatis.executor.BaseExecutor
* 功能 : 根据传入信息构建CacheKey
*/
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) throw new ExecutorException("Executor was closed.");
CacheKey cacheKey = new CacheKey();
//1.statementId
cacheKey.update(ms.getId());
//2. rowBounds.offset
cacheKey.update(rowBounds.getOffset());
//3. rowBounds.limit
cacheKey.update(rowBounds.getLimit());
//4. SQL语句
cacheKey.update(boundSql.getSql());
//5. 将每一个要传递给JDBC的参数值也更新到CacheKey中
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
for (int i = 0; i < parameterMappings.size(); i++) { // mimic DefaultParameterHandler logic
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
//将每一个要传递给JDBC的参数值也更新到CacheKey中
cacheKey.update(value);
}
}
return cacheKey;
}
刚才已经提到,Cache
接口的实现,本质上是使用的HashMap<k,v>
,而构建CacheKey
的目的就是为了作为HashMap<k,v>
中的key
值。而HashMap
是通过key
值的hashcode
来组织和存储的,那么,构建CacheKey
的过程实际上就是构造其hashCode
的过程。下面的代码就是CacheKey
的核心hashcode
生成算法,可以看一下:
public void update(Object object) {
if (object != null && object.getClass().isArray()) {
int length = Array.getLength(object);
for (int i = 0; i < length; i++) {
Object element = Array.get(object, i);
doUpdate(element);
}
} else {
doUpdate(object);
}
}
private void doUpdate(Object object) {
//1. 得到对象的hashcode;
int baseHashCode = object == null ? 1 : object.hashCode();
//对象计数递增
count++;
checksum += baseHashCode;
//2. 对象的hashcode 扩大count倍
baseHashCode *= count;
//3. hashCode * 拓展因子(默认37)+拓展扩大后的对象hashCode值
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
MyBatis认为的完全相同的查询,不是指使用sqlSession查询时传递给算起来Session的所有参数值完完全全相同,你只要保证statementId,rowBounds,最后生成的SQL语句,以及这个SQL语句所需要的参数完全一致就可以了。
五、总结
一级缓存执行的时序图,如下图所示