MyBatis一级缓存解密:深入探究缓存机制与应用场景
MyBatis的一级缓存是提高数据库访问性能的重要组成部分,也是默认开启的缓存机制。本文将深入研究MyBatis一级缓存,详细解析其工作原理、优势和应用场景。我们将探讨一级缓存的生命周期、作用范围、失效策略等关键特性,以及如何利用一级缓存提高系统的性能和可维护性。
在上文《探秘MyBatis缓存原理:Cache接口与实现类源码分析》中,我们已经介绍了 MyBatis 的 Cache 接口以及对应的实现类。其中的 PerpetualCache 是 MyBatis 缓存的最基础的实现类,底层通过 HashMap 存储数据,其他的实现类都属于装饰器,基于 PerpetualCache 的各个方面进行增强,各个实现类的理论和实现我们学习过后,本文我们就来探究一下,MyBatis 真正的缓存机制是怎么样的!
MyBatis 缓存机制
MyBatis 为提高其数据库查询性能,提供了两层缓存机制(只针对查询做缓存),包括一级缓存和二级缓存。
- ⼀级缓存:将查询到的数据存储到 SqlSession 中,所以只对本 SqlSession 有效。范围比较小,只对于一次 SQL 会话。
- ⼆级缓存:将查询到的数据存储到 SqlSessionFactory 中。范围比较大,针对于整个数据库级别。
MyBatis 也可以集成其它第三方缓存:比如基于 Java 开发的 EhCache、基于 C 语言开发的 Memcache等。
一级缓存
一级缓存也叫本地缓存(LocalCache)。一级缓存默认开启,当 MyBatis 在一次 SqlSession 数据库查询之后,会将查询结果以键值对形式存储到内存中,当前 SqlSession 后续以相同 SQL 查询时,会直接去查询内存缓存,避免数据库查询,提高查询性能。
- 作用范围:一级缓存的作用范围是在同一个 SqlSession 中。当你在一个 SqlSession 中执行了一次查询操作后,查询的结果会被缓存在内存中,下次执行相同的查询时,MyBatis 会首先检查缓存中是否存在该查询的结果,如果存在,则直接返回缓存中的结果,而不会再次查询数据库。
- 生命周期:一级缓存的生命周期与 SqlSession 相关联。当 SqlSession 关闭时,一级缓存也会被清空,这意味着一级缓存只在 SqlSession 的生命周期内有效。
- 缓存键:MyBatis 默认使用 SQL 语句、输入参数和 RowBounds 作为缓存的键值。这意味着如果两次查询的 SQL 语句相同、输入参数相同且分页参数 RowBounds 相同,则会命中缓存。
- 缓存清除:MyBatis 提供了多种方式来清除一级缓存,包括调用 SqlSession 的
clearCache()
方法手动清除缓存、执行 update、insert 或 delete 操作时自动清除缓存等。 - 缓存失效:当执行 update、insert 或 delete 操作时,MyBatis 会自动清除一级缓存,以避免缓存中的数据与数据库中的数据不一致。
- 线程安全性:由于一级缓存是与 SqlSession 相关联的,因此它是线程安全的。每个 SqlSession 都有自己的一级缓存,不同线程的操作不会相互影响。
验证一级缓存
MyBatis 配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<typeAlias type="world.xuewei.mybatis.entity.Account" alias="Account"/>
</typeAliases>
<environments default="default">
<environment id="default">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://**.**.**/learn?useSSL=false&characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mappers/AccountMapper.xml"/>
</mappers>
</configuration>
测试类
package world.xuewei;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import world.xuewei.mybatis.dao.AccountDao;
import java.io.IOException;
import java.io.InputStream;
/**
* @author 薛伟
* @since 2023/9/14 20:51
*/
public class CacheTest {
private SqlSession sqlSession;
@Before
public void before() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
sqlSession = sessionFactory.openSession();
}
@After
public void after() {
sqlSession.commit();
}
/**
* 验证一级缓存默认开启,但是一级缓存只对同一个 SqlSession 有效
*/
@Test
public void testGetAll() {
AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
accountDao.getAll().forEach(System.out::println);
System.out.println("======================================");
accountDao.getAll().forEach(System.out::println);
}
}
通过观察日志的打印情况,可以看出,在第二次执行相同的查询方法时,就已经是从缓存中直接拿到的数据了。
清理一级缓存
手动清理
AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
accountDao.getAll().forEach(System.out::println);
sqlSession.clearCache();
accountDao.getAll().forEach(System.out::println);
执行 update、insert 或 delete 操作
不管你是操作哪张表的,都会清空一级缓存。
AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
accountDao.getAll().forEach(System.out::println);
accountDao.delete(1);
accountDao.getAll().forEach(System.out::println);
关闭一级缓存
一级缓存默认开启,但是如果想关闭一级缓存,可以使用localCacheScopde=statement
来关闭。即添加在 MyBatis 的配置文件中的 <settings>
标签下:
<settings>
<setting name="localCacheScope" value="STATEMENT"/>
</settings>
通过观察 Configuration 配置类的源码可以看到,localCacheScope 的默认值为 Session。
public class Configuration {
// ...
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
//...
}
关闭后再次执行上面的测试代码可以看到,每次执行的时候都是去查询了数据库。
为什么可以通过指定localCacheScopde=statement
来关闭一级缓存呢?这里先剧透一下,接下来我们会讲到 MyBatis 的一级缓存主要是通过 BaseExecutor 类来实现的,核心的方法如下:
实现原理
我们带着答案去找原因,为什么一级缓存时绑定在 SqlSession 的?通过我们原有的认知,我们要联想到这个图:
SqlSession 将 SQL 语句的处理和执行交给 Executor 去处理。联想前面学习的核心对象:《深度解析MyBatis核心:探寻其核心对象的精妙设计》,缓存的支持就是又 Executor 来搞的。而 Executor 是接口,查看他的几个实现类后不难发现,我们在 BaseExecutor 中发现了基于 HashMap 实现缓存的老熟人 PerpetualCache。
public abstract class BaseExecutor implements Executor {
// 本地缓存
protected PerpetualCache localCache;
// 这个也是缓存,但是服务于存储过程
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
// 省略其他属性...
protected int queryStack;
private boolean closed;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
// 为两个缓存初始化 ID
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
// 创建缓存 Key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 如果缓存有数据,就从缓存拿
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 缓存没拿到就查询数据库,并将结果放入缓存
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
deferredLoads.clear();
// 注意这里,如果 localCacheScope 设置为 STATEMENT,那么每次查询完都会把缓存再清理...
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
}
return list;
}
@Override
public void close(boolean forceRollback) {
try {
try {
rollback(forceRollback);
} finally {
if (transaction != null) {
transaction.close();
}
}
} catch (SQLException e) {
log.warn("Unexpected exception on closing transaction. Cause: " + e);
} finally {
// 关闭 Executor 的时候清空缓存
transaction = null;
deferredLoads = null;
localCache = null;
localOutputParameterCache = null;
closed = true;
}
}
@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);
}
@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
// 提交操作会清空缓存
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}
@Override
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
// 回滚操作会清空缓存
clearLocalCache();
flushStatements(true);
} finally {
if (required) {
transaction.rollback();
}
}
}
}
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 从数据库查询
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 查询完后放在缓存里面
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
// 省略其他方法...
}
一级缓存的 Key 的结构,通过 createCacheKey 方法来创建缓存,源码如下:
通过源码我们不难发现,Key 的生成策略:MappedStatement.id + Offset + Limit + Sql + Param[*].value + Environment.id
,这些值都相同,生成的 Key 就相同。
- 点赞
- 收藏
- 关注作者
评论(0)