MyBatis缓存机制详解

MyBatis缓存机制详解MyBatis 缓存机制详解 1 MyBatis 缓存 1 1 MyBatis 缓存概述 1 2 MyBatis 一二级缓存区别 2 MyBatis 一级缓存 2 1 MyBatis 一级缓存概述 2 2 MyBatis 一级缓存配置 2 3 MyBatis 一级缓存原理分析 2 4 MyBatis 一级缓存总结 3 MyBatis 二级缓存 3 1 MyBatis 二级缓存概述 3 2

MyBatis缓存机制详解

1. MyBatis缓存

1.1 MyBatis缓存概述

1.2 MyBatis一二级缓存区别

2. MyBatis一级缓存

2.1 MyBatis一级缓存概述

2.2 MyBatis一级缓存配置

2.3 MyBatis一级缓存原理分析

2.4 MyBatis一级缓存总结

3. MyBatis二级缓存

3.1 MyBatis二级缓存概述

3.2 MyBatis二级缓存配置

3.3 MyBatis二级缓存原理分析

3.4 MyBatis二级缓存总结

4. MyBatis缓存测试

5. 参考文档

1. MyBatis缓存

1.1 MyBatis缓存概述

MyBatis作为目前最常用的ORM数据库访问持久层框架,其本身支持动态SQL存储映射等高级特性也非常优秀,通过Mapper文件采用动态代理模式使SQL与业务代码相解耦,日常开发中使用也非常广泛,本文主要讨论mybatis缓存功能,mybatis缓存本身设计初衷是为了解决同一会话相同查询的效率问题,单机环境下也确实起到了提高查询效率的作用,但是随着业务场景变化以及分布式微服务的出现,其弊端也渐渐显现出来,不同会话间操作数据,关联查询数据采用mybatis缓存时会存在出现脏数据的风险。

1.2 MyBatis一二级缓存区别

1.Mybatis一级缓存是SQLSession级别的,一级缓存的作用域是SQlSession;Mabits一级缓存默认是开启的。 在同一个SqlSession中,执行相同的SQL查询时;第一次会去查询数据库,并写在缓存中,第二次会直接从缓存中取。 在同一次会话中执行两次相同查询中间执行了更新操作的时候,缓存会被清空,第二次相同查询仍然会去查询数据库。

2.Mybatis二级缓存是Mapper级别的,二级缓存的作用域是全局的,多个SQlSession共享的,二级缓存的作用域更大;Mybatis二级缓存默认是没有开启的。 第一次调用mapper下的SQL去查询用户的信息,查询到的信息会存放在该mapper对应的二级缓存区域。 第二次调用namespace下的mapper映射文件中,相同的sql去查询用户信息,会去对应的二级缓存内取结果。

2. MyBatis一级缓存

2.1 MyBatis一级缓存概述

默认情况下,只启用了本地的会话缓存,也就是一级缓存,它仅仅对一个会话中的数据进行缓存。 mybatis一级缓存指的是在应用运行过程中,一次数据库会话中,执行多次相同的查询,会优先查询缓存中的数据,减少数据库查询次数,提高查询效率。
每个SqlSession中持有了Executor,每个Executor中有一个LocalCache。当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement,在Local Cache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache,最后返回结果给用户。

2.2 MyBatis一级缓存配置

mybatis一级缓存默认是开启的,可根据需要选择级别是session或这statement。开发者只需在MyBatis的配置文件中,添加如下语句,就可以使用一级缓存。共有两个选项,session或者statement,默认是session级别,即在一个MyBatis会话中执行的所有语句,都会共享这一个缓存。一种是statement级别,可以理解为缓存只对当前执行的这一个Statement有效。

2.3 MyBatis一级缓存原理分析

1.在初始化SqlSesion时,会使用Configuration类创建一个全新的Executor,作为DefaultSqlSession构造函数的参数。

    // newExecutor 尤其可以注意这里,如果二级缓存开关开启的话,是使用CahingExecutor装饰BaseExecutor的子类
if (cacheEnabled) {

executor = new CachingExecutor(executor);
}

2.SqlSession创建完毕后,根据Statment的不同类型,会进入SqlSession的不同方法中,如果是Select语句的话,最后会执行到SqlSession的selectList,代码如下所示:

@Override
public List selectList(String statement, Object parameter, RowBounds rowBounds) {

MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}

3.SqlSession把具体的查询职责委托给了Executor。如果只开启了一级缓存的话,首先会进入BaseExecutor的query方法。代码如下所示:

@Override
public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {

BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

4.在上述代码中,会先根据传入的参数生成CacheKey,进入该方法查看CacheKey是如何生成的,代码如下所示:

CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
//后面是update了sql中带的参数
cacheKey.update(value);

在上述的代码中,将MappedStatement的Id、SQL的offset、SQL的limit、SQL本身以及SQL中的参数传入了CacheKey这个类,最终构成CacheKey。以下是这个类的内部结构:

public CacheKey() { 

this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
this.updateList = new ArrayList();
}

首先是成员变量和构造函数,有一个初始的hachcode和乘数,同时维护了一个内部的updatelist。在CacheKey的update方法中,会进行一个hashcode和checksum的计算,同时把传入的参数添加进updatelist中。如下代码所示:

public void update(Object object) { 

int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;

updateList.add(object);
}

除去hashcode、checksum和count的比较外,只要updatelist中的元素一一对应相等,那么就可以认为是CacheKey相等。只要两条SQL的下列五个值相同,即可以认为是相同的SQL。
Statement Id + Offset + Limmit + Sql + Params

5.BaseExecutor的query方法继续往下走,代码如下所示:

list = resultHandler == null ? (List) localCache.getObject(key) : null;
if (list != null) {

// 这个主要是处理存储过程用的。
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {

list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

如果查不到的话,就从数据库查,在queryFromDatabase中,会对localcache进行写入。

在query方法执行的最后,会判断一级缓存级别是否是STATEMENT级别,如果是的话,就清空缓存,这也就是STATEMENT级别的一级缓存无法共享localCache的原因。代码如下所示:

if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { 

clearLocalCache();
}

在源码分析的最后,我们确认一下,如果是insert/delete/update方法,缓存就会刷新的原因。
SqlSession的insert方法和delete方法,都会统一走update的流程,代码如下所示:

@Override
public int insert(String statement, Object parameter) {

return update(statement, parameter);
}
@Override
public int delete(String statement) {

return update(statement, null);
}

update方法也是委托给了Executor执行。BaseExecutor的执行方法如下所示:

@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {

ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {

throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}

每次执行update前都会清空localCache。

2.4 MyBatis一级缓存总结

1.MyBatis一级缓存的生命周期和SqlSession一致。
2.MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
3.MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。

3. MyBatis二级缓存

3.1 MyBatis二级缓存概述

在上文中提到的一级缓存中,其最大的共享范围就是一个SqlSession内部,如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询,具体的工作流程如下所示。

3.2 MyBatis二级缓存配置

要正确的使用二级缓存,需完成如下配置的。
1.在MyBatis的配置文件中开启二级缓存。

2.在MyBatis的映射XML中配置cache或者 cache-ref 。
cache标签用于声明这个namespace使用二级缓存,并且可以自定义配置。

   
type:cache使用的类型,默认是PerpetualCache,这在一级缓存中提到过。
eviction: 定义回收的策略,常见的有FIFO,LRU。
flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。
size: 最多缓存对象的个数。
readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。

3.cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。

3.3 MyBatis二级缓存原理分析

源码分析从CachingExecutor的query方法展开,源代码走读过程中涉及到的知识点较多,不能一一详细讲解,读者朋友可以自行查询相关资料来学习。
CachingExecutor的query方法,首先会从MappedStatement中获得在配置初始化时赋予的Cache。

Cache cache = ms.getCache();

本质上是装饰器模式的使用,具体的装饰链是:
SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache。

SynchronizedCache:同步Cache,实现比较简单,直接使用synchronized修饰方法。
LoggingCache:日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。
SerializedCache:序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。
LruCache:采用了Lru算法的Cache实现,移除最近最少使用的Key/Value。
PerpetualCache: 作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap。

3.4 MyBatis二级缓存总结

1.MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
2.MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
3.在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。

4. MyBatis缓存测试

测试案例地址:https://gitee.com/rjzhu/opencode/tree/master/mybatis-cache-demo

/** * MyBatis缓存测试类 */
public class StudentMapperTest {


private SqlSessionFactory factory;

/** * 初始化SqlSessionFactory */
@Before
public void setUp() throws Exception {

factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config.xml"));
}

/** * 查看缓存配置是否生效 * * */
@Test
public void showDefaultCacheConfiguration() {

System.out.println("本地缓存范围: " + factory.getConfiguration().getLocalCacheScope());
System.out.println("二级缓存是否被启用: " + factory.getConfiguration().isCacheEnabled());
}

/** * MyBatis缓存测试一 * 测试:同一个会话,相同查询连续查询三次 * 结果:第一次查询数据库,二三次查询从缓存读取 * 结论:同一个会话,多次相同查询,只有第一次查询数据库,其他都是缓存中获取,提高了查询效率 */
@Test
public void testLocalCache() {

SqlSession sqlSession = factory.openSession(true); // 自动提交事务
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);

//第一次查询数据库,二三次查询直接从缓存读取
System.out.println("第一次查询:" + studentMapper.getStudentById(1));
System.out.println("第二次查询:" + studentMapper.getStudentById(1));
System.out.println("第三次查询:" + studentMapper.getStudentById(1));

sqlSession.close();
}

/** * MyBatis缓存测试二 * 测试:同一个会话,先查询,再新增,再次重复第一次查询 * 结果:第一次与第二次查询都查询数据库,修改操作后执行的相同查询,查询了数据库,一级缓存失效。 * 结论:同一个会话执行更新操作后缓存失效,源码中会清空缓存 */
@Test
public void testLocalCacheClear() {

SqlSession sqlSession = factory.openSession(true); // 自动提交事务
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);

//第一次与第二次查询都查询数据库,修改操作后执行的相同查询,查询了数据库,一级缓存失效。
System.out.println("第一次查询:" + studentMapper.getStudentById(1));
System.out.println("增加了" + studentMapper.addStudent(StudentEntity.builder().name("明明").age(20).build()) + "个学生");
System.out.println("第二次查询:" + studentMapper.getStudentById(1));

sqlSession.close();
}

/** * MyBatis缓存测试三 * 测试:同时开启两个会话,会话一连续两次查询,会话二更新操作,会话一再次相同查询,会话二相同查询 * 结果:会话一第一次查询数据库,第二次查询缓存,会话二更新完成,会话一再次相同查询仍然查询缓存(读取脏数据),会话二查询数据库获取最新数据。 * 结论:缓存作用范围是一个会话当中,当其中有会话更新数据,其他会话会读取到脏数据 */
@Test
public void testLocalCacheScope() {

//开启两个SqlSession,在sqlSession1中查询数据,使一级缓存生效,在sqlSession2中更新数据库
//验证一级缓存只在数据库会话内部共享。
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读取数据: " + studentMapper.getStudentById(1));
System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
System.out.println("studentMapper2更新了" + studentMapper2.updateStudentName("小岑", 1) + "个学生的数据");
System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}

/** * MyBatis缓存测试四 * 测试:同时开启两个会话,两个会话执行相同的查询 * 结果:两次都是查询数据库 * 结论:缓存作用范围是一个会话当中,不同会话,即使是相同查询,只使用各自的缓存 */
@Test
public void testCacheWithoutCommitOrClose() {

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读取数据: " + studentMapper.getStudentById(1));
System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));

}

/** * MyBatis缓存测试四 * 测试:同时开启两个会话,两个会话执行相同的查询 * 结果:两次都是查询数据库 * 结论:缓存作用范围是一个会话当中,不同会话,即使是相同查询,只使用各自的缓存 */
@Test
public void testCacheWithCommitOrClose() {

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读取数据: " + studentMapper.getStudentById(1));
sqlSession1.close();
System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));

}

/** * MyBatis缓存测试五 * 测试:同时开启三个会话,通过接口方式,会话一查询后提交事务,会话二执行相同查询,缓存查询,会话三更新提交事务,会话二查询缓存 * 结果:只有第一次查询数据库,其余都是查询缓存 * 结论:只有提交事务以后,后续相同查询才会查询缓存,否则查询数据库 */
@Test
public void testCacheWithUpdate() {

SqlSession sqlSession1 = factory.openSession(true); // 自动提交事务
SqlSession sqlSession2 = factory.openSession(true); // 自动提交事务
SqlSession sqlSession3 = factory.openSession(true); // 自动提交事务

StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
StudentMapper studentMapper3 = sqlSession3.getMapper(StudentMapper.class);

System.out.println("studentMapper1读取数据: " + studentMapper.getStudentById(1));
sqlSession1.close();
System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));

studentMapper3.updateStudentName("方方", 1);
sqlSession3.commit();
System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}

/** * MyBatis缓存测试六 * 测试:测试关联查询,出现脏数据问题, * 结论:不同会话之间关联查询的时候,其中会话更新单独更新关联的其中一个表,另一个会话感知不到,在不同的mapper文件中,缓存查询会出现脏数据情况 */
@Test
public void testCacheWithDiffererntNamespace() {

// 设置自动提交事务
SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
SqlSession sqlSession3 = factory.openSession(true);

StudentMapper studentMapper1 = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
ClassMapper classMapper3 = sqlSession3.getMapper(ClassMapper.class);

System.out.println("studentMapper1读取数据: " + studentMapper1.getStudentByIdWithClassInfo(1));
sqlSession1.close();

System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentByIdWithClassInfo(1));

//更新数据
classMapper3.updateClassName("特色一班", 1);
sqlSession3.commit();

//读取到脏数据,studentMapper2读取数据: StudentEntity(id=1, name=方方, age=16, className=一班)
System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentByIdWithClassInfo(1));
}

}

5. 参考文档

MyBatis中文网:https://mybatis.net.cn/index.html
MyBatis英文网:https://mybatis.org/mybatis-3/index.html
MyBatis执行流程源码分析:https://blog.csdn.net/m0_37583655/article/details/122115750
聊聊MyBatis缓存机制
mybatis一级缓存和二级缓存的区别是什么

编程小号
上一篇 2025-02-21 07:30
下一篇 2025-02-23 16:21

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/hz/145406.html