在上文《探秘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> <settings> <setting name="cacheEnabled" value="true"/> </settings> <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> <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 就相同。