首页 > 其他分享 >MyBatis 一级缓存原理

MyBatis 一级缓存原理

时间:2024-09-03 20:52:49浏览次数:11  
标签:缓存 一级 Object id SqlSession MyBatis main public

优质博文: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语句所需要的参数完全一致就可以了。

五、总结

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

标签:缓存,一级,Object,id,SqlSession,MyBatis,main,public
From: https://blog.csdn.net/zhengzhaoyang122/article/details/141651074

相关文章

  • 【Spring Boot】整合MyBatis
    **整合MyBatis**前言SpringBoot和MyBatis都是非常流行的Java框架。SpringBoot简化了Spring应用的开发,而MyBatis则是一个优秀的持久层框架,支持自定义SQL、存储过程以及高级映射。mybatis官方文档:http://mybatis.org/spring-boot-starter/myba......
  • SpringBoot3.x+MyBatisPlus+druid多数据源配置
    1引言本章主要介绍SpringBoot3.x多数据源配置,以及在此基础上配置分页拦截,自动填充功等功能,源码链接在文章最后。下面列出几个重要文件进行介绍。2项目结构整体项目结构如下,主要介绍配置文件和配置类。3主要代码3.1pom.xml注意SpringBoot3.x对应依赖为mybatis-plu......
  • 攻破工程级复杂缓存难题--企业实战12
    缓存技术在现代分布式系统中至关重要,不仅提升了系统性能,还减轻了后端数据库的压力。然而,缓存系统也面临着诸多挑战,如缓存穿透、缓存雪崩、缓存击穿和热点key问题。通过多种策略的综合应用,包括本地缓存、双缓存方案、多级缓存、多副本、热点key拆分和动态分散等,可以有效应对这些......
  • SpringBoot项目常用配置文件MybatisPlusConfig、RedisConfig、RedissonConfig、Swagge
    MybatisPlusConfig:@Configuration@MapperScan("com.yupi.usercenter.mapper")publicclassMybatisPlusConfig{@BeanpublicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptorinterceptor=newMybatisPlusInterc......
  • 常见持久层框架赏析,到底是什么让你选择 MyBatis?
    在绝大多数在线应用场景中,数据是存储在关系型数据库中的,当然,有特殊要求的场景中,我们也会将其他持久化存储(如ElasticSearch、HBase、MongoDB等)作为辅助存储。但不可否认的是,关系型数据库凭借几十年的发展、生态积累、众多成功的案例,依然是互联网企业的核心存储。作为一个Java开......
  • MyBatis 反射工具箱:带你领略不一样的反射设计思路
    反射是Java世界中非常强大、非常灵活的一种机制。在面向对象的Java语言中,我们只能按照public、private等关键字的规范去访问一个Java对象的属性和方法,但反射机制可以让我们在运行时拿到任何Java对象的属性或方法。有人说反射打破了类的封装性,破坏了我们的面向对象思维,我......
  • 《ARM Cortex-R 学习指南》-【第七章】-缓存
    第七章缓存基本上,处理器缓存是一个位于核心与主存之间的小而快速的内存块。它存储了主存中最近访问的项目的副本。访问缓存内存的速度明显快于访问主存。由于缓存仅保存了主存内容的一个子集,因此它必须同时存储主存中项目的地址及其相关数据。每当核心想要读取或写入特定......
  • 自定义缓存组件 代替 Spring@Cache缓存注解
    自定义缓存组件代替Spring@Cache缓存注解  在实现上述功能之前先来点基础的,redis在SpringBoot项目中常规的用法,好对缓存和redis客户端的使用有一定了解。  1.添加依赖redis客户端依赖(连接redis服务端必备 )<!--客户端依赖二选一--><dependency><groupId>redi......
  • T8332FI LED驱动芯片—原厂货源,一级代理,质量保障,库存充裕
    T8332FILED驱动芯片概述T8332FI是一款多功能的LED驱动芯片,主要用于驱动高功率LED。这款芯片支持多种转换配置,包括Boost、Buck、Buck-Boost以及SEPIC转换器,具有良好的恒定电流控制能力,恒流精度通常在±3%以内。芯片输入电压范围广泛,支持从5V到60V的输入电压,适用于汽车照明、LC......
  • MybatisPlus 主键策略之type=IdType.ASSIGN_ID等详解
    雪花算法(雪花)是微博开源的分布式ID生成算法其核心思想就是:使用一个64位的长型的数字作为全局唯一ID,主要介绍了MybatisPlus 主键策略(type=IdType.ASSIGN_ID等详解),需要的朋友可以参考下: 我们可以通过@TableId注解的 类型属性来设置主键id的增长策略,一共有几个多个主键策略,......