MyBatis 一级缓存原理

优质博文:IT-BLOG-CN

一、一级缓存配置

MyBatis一级缓存默认是开启的。如果需要显示的开启,需要在MyBaits配置文件中<settings>标签中添加如下语句:

xml 复制代码
<settings>
	<setting name="localCacheScope" value="SESSION"/>
</settings>

value共有两个选项,SESSION或者STATEMENT,默认是SESSION级别,即在一个MyBatis会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个Statement有效。

一级缓存基于SqlSession举个例子:

java 复制代码
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));
}

执行结果:我们可以看到,只有第一次真正查询了数据库,后续的查询使用了一级缓存。

sql 复制代码
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中更新数据库,验证一级缓存只在数据库会话内部共享。

java 复制代码
@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的学生的名字还是小明,出现了重复读,说明一级缓存只在数据库会话内部共享。

sql 复制代码
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进行了增删改查操作。举个例子:

java 复制代码
@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();
}

执行结果:我们可以看到,在修改操作后执行的相同查询,查询了数据库,一级缓存失效

sql 复制代码
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的实现代码:

java 复制代码
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中,定义如下:

java 复制代码
/** 
 * 所属类:  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生成算法,可以看一下:

java 复制代码
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语句所需要的参数完全一致就可以了。

五、总结

一级缓存执行的时序图,如下图所示

相关推荐
六月闻君10 分钟前
MySQL 报错:1137 - Can‘t reopen table
数据库·mysql
SelectDB技术团队19 分钟前
兼顾高性能与低成本,浅析 Apache Doris 异步物化视图原理及典型场景
大数据·数据库·数据仓库·数据分析·doris
郑祎亦20 分钟前
Spring Boot 项目 myblog 整理
spring boot·后端·java-ee·maven·mybatis
不是二师兄的八戒20 分钟前
本地 PHP 和 Java 开发环境 Docker 化与配置开机自启
java·docker·php
爱编程的小生32 分钟前
Easyexcel(2-文件读取)
java·excel
inventecsh35 分钟前
mongodb基础操作
数据库·mongodb
白云如幻39 分钟前
SQL99版链接查询语法
数据库·sql·mysql
带多刺的玫瑰1 小时前
Leecode刷题C语言之统计不是特殊数字的数字数量
java·c语言·算法
陪学1 小时前
百度遭初创企业指控抄袭,维权还是碰瓷?
人工智能·百度·面试·职场和发展·产品运营
爱吃烤鸡翅的酸菜鱼1 小时前
MySQL初学之旅(4)表的设计
数据库·sql·mysql·database