您的位置:首页 > 财经 > 产业 > MyBatis 一级缓存原理

MyBatis 一级缓存原理

2025/2/28 3:45:05 来源:https://blog.csdn.net/zhengzhaoyang122/article/details/141651074  浏览:    关键词: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更新了id1的学生的姓名,从小明改为了小花,但session1之后的查询中,id1的学生的名字还是小明,出现了重复读,说明一级缓存只在数据库会话内部共享。

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>
【了解】执行SqlSessionclose(会释放掉一级缓存PerpetualCache对象)或clearCache(会清空PerpetualCache对象中的数据)方法
【了解】执行SqlSessioncommit(执行插入、更新、删除操作后)

这里的更新指的是当前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对象中包含一个ExecutorExecutor对象中创建一个本地缓存local cache,对于每一次查询,都会尝试根据执行的语句生成的MappedStatementHash值去本地缓存中查找,如果在缓存中,就直接从缓存中取出,然后返回给用户;否则,从数据库读取数据,将查询结果存入缓存并返回给用户。
在这里插入图片描述
如上所示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语句所需要的参数完全一致就可以了。

五、总结

一级缓存执行的时序图,如下图所示
在这里插入图片描述

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com