提示:自从上次发现mybatis缓存可被修改后,就一直想针对myBatis缓存单独做一期分析,包含其原理和运行方式,现在终于得空来详细写一篇了
熟悉MyBatis的应该知道,MyBatis内置了两级缓存,会在查询数据库时,将查询结果缓存到内存中,以便下次查询时可以直接从缓存中获取数据,从而提高数据查询效率
MyBatis缓存一般分为一级缓存和二级缓存。
是指MyBatis自身的缓存机制,是SqlSession级别的缓存。当同一个SqlSession执行相同的SQL语句时,MyBatis会将查询结果缓存到内存中。一级缓存的作用域是SqlSession,当当前的SqlSession关闭时,一级缓存也将被清空。
是指MyBatis全局的缓存机制,在多个SqlSession之间共享缓存数据。二级缓存的作用域是Mapper级别的,每个Mapper对应一个缓存。在同一应用程序中的多个SqlSession都可以共享同一个缓存,这是一种横向共享的缓存机制。但是需要注意的是,该缓存只有在Mapper映射文件中声明了缓存的情况下才能启用。
在说SqlSession级别的缓存前,我们需要介绍下sqlSession本身的一些内容。我们都知道,在早期项目中,与数据库的连接通常以JDBC 的 connection(即连接)为核心,通过jdbc的API获取数据库连接,并执行代码,其形式大体如下:
import java.sql.*;
public class MySqlConnectionExample {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/mydatabase";
String user = "root";
String password = "mypassword";
String sql = "SELECT * FROM mytable";
try {
// 1. 加载 MySQL JDBC 驱动程序
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 建立数据库连接(仅举例,实际项目一般都有连接池)
Connection conn = DriverManager.getConnection(url, user, password);
// 3. 创建 Statement 对象
Statement stmt = conn.createStatement();
// 4. 执行 SQL 查询语句,并返回结果集
ResultSet rs = stmt.executeQuery(sql);
// 5. 遍历结果集,输出查询结果
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
System.out.println("id: " + id + ", name: " + name);
}
// 6. 关闭结果集、Statement 对象和数据库连接
rs.close();
stmt.close();
conn.close();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
看起来,通过获取连接来执行sql似乎就够了,为什么myBatis还要加入一个sqlSqlsession,sqlSqlsession又是拿来做什么的呢?其实,使用 SqlSession 相比于 Connection 直接执行 SQL 的主要优点如下:
在应用程序启动时执行的进程。下述处理(1)至(4)对应于这种类型
为每个请求执行的过程。下述处理(5)至(11)对应于这种类型
sqlSession缓存并非直接挂在sqlSession对象下,而是存储在执行器 BaseExecutor.class 中的 localCache(缓存查询结果) 和 localOutputParameterCache(缓存存储过程调用结果)实现,类名为 PerpetualCache.class
而PerpetualCache类则内含一个普通的HashMap,这个HashMap即真正的缓存位置
这样的缓存键值对,值我们是能想到的,就是SQL查询后返回的结果列表,那么键是什么呢?
CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
CacheKey 由以下几个因素影响
而它的存储流程,其实很简单
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 省略部分代码
if (queryStack == 0 && ms.isFlushCacheRequired()) {
// 如果开启了flushCache,则清除缓存
clearLocalCache();
}
List<E> list;
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);
}
// 省略部分代码
return list;
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);
List list;
try {
list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
this.localCache.removeObject(key);
}
this.localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
this.localOutputParameterCache.putObject(key, parameter);
}
return list;
}
BaseExecutor不仅定义了一级缓存,同时也声明了这种缓存的生命周期:
但是,从源码我们不难发现一个问题,从缓存中取到的内容,是作为查询结果直接返回的。这意味着,如果我们对结果进行修改,将直接改变缓存值。并且,因为一级缓存没有提供关闭的参数,所以此时我们有两种方式解决
二级缓存不同于一级缓存,它默认是关闭的,需要手动开启,我们知道一级缓存是由BaseExecutor负责存储的,那么负责二级缓存的实际上是它的兄弟类CachingExecutor
它的缓存存储位置在TransactionalCacheManager 类中
public class CachingExecutor implements Executor {
// 普通的执行器,通常就是我们上面的BaseExecutor的一个子类
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
}
public class TransactionalCacheManager {
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
}
那么,我们如何才能启用这个执行器呢?首先,我们必须得在全局设置缓存可用,在配置文件上加上
mybatis.configuration.cache-enabled=true
或
mybatis-plus.configuration.cache-enabled=true
然后在我们都mapper层加上设置即可。
<mapper namespace="xx.xxx.xxx.mapper.xxxMapper">
<cache eviction="FIFO" flushInterval="60000" readOnly="false" size="1024">cache>
mapper>
当然,如果sql不是以xml形式写的,而是在mapper接口里以注解形式写的,那只要在mapper层加上注解 @CacheNamespace 也是同样的效果
二级缓存开启后,同一个 namespace 下的所有操作语句,都影响着同一个 Cache,即二级缓存被多个 SqlSession 共享,是一个全局的变量。
即使我们为其设定了自动刷新的时间,二级缓存的脏读概率仍然很大。尤其是在多表查询的情况下,尽管可以给不同的mapper设定同一个缓存,但复杂场景下这种繁琐的配置几乎不可用。
<cache-ref namespace="com.example.mapper.UserInfoMapper" />
所以二级缓存实际上很少有人会开启。