回答
Mybatis 内置了强大了事务性查询缓存机制。默认情况下,Mybatis 定义了两级缓存,并且提供Cache
接口扩展,提高了框架的扩展性。
- 一级缓存:也叫“本地缓存”,默认情况下开启(SqlSession 级别的缓存)
- 二级缓存:也叫“全局缓存”,基于 namespace 级别的缓存,需要手动开启和配置。
实现原理
一级缓存
Mybatis 一级缓存由PerpetualCache
实现,采用 Map 结构存储数据。其作用域是 SqlSession,各个 SqlSession 之间的缓存相互隔离。每次查询后,结果会被缓存到PerpetualCache
中,后续相同查询直接取缓存数据。
核心实现类和方法
- BaseExecutor:负责执行 SQL,管理一级缓存。
public abstract class BaseExecutor implements Executor {
// ... 其他代码省略
@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();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
// ... 其他代码省略
}
- PerpetualCache:缓存实现类,采用 Map 结构存储数据。
public class PerpetualCache implements Cache {
// ... 其他代码省略
private final String id;
// Map 结果本地缓存结果
private final Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
// ... 其他代码省略
}
二级缓存
Mybatis 二级缓存是基于命名空间(namespace)的缓存,存储在CachingExecutor
。它在每次执行查询之前,先从二级缓存中查找,如果找到则返回缓存结果,否则执行查询并将结果缓存。
默认不打开二级缓存,要开启二级缓存,需通过配置打开,并且使用二级缓存的属性类需要实现 Serializable 序列化接口(可用来保存对象的状态)。配置如下:
<configuration>
<!-- 其他配置 -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
</configuration>
<mapper namespace="com.damingge.mapper.EmployeeMapper">
<!-- 启用二级缓存 -->
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
</mapper>
核心实现类和方法
- CachingExecutor:二级缓存执行器。
public class CachingExecutor implements Executor {
// ... 其他代码省略
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 生成缓存key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 从映射声明器获取 Cache
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 获取缓存结果
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 实际执行
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
// ... 其他代码省略
}
- Cache:缓存接口,Mybatis 默认提供了多种缓存实现,如 PerpetualCache、LruCache、FifoCache、BlockingCache 等。
扩展
一级缓存的 cache key 如何生成?
Mybatis 一级缓存键值通过 CacheKey 实现,生成 Cache Key 包含 SQL 语句ID、查询参数、分页参数等。
- CacheKey:CacheKey 类通过 update 方法将各个属性添加到缓存键中。
public class CacheKey implements Cloneable, Serializable {
// ... 其他代码省略
public CacheKey(Object[] objects) {
this();
updateAll(objects);
}
public int getUpdateCount() {
return updateList.size();
}
//核心方法
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
// 所有属性存放至一个List集合对象
updateList.add(object);
}
public void updateAll(Object[] objects) {
for (Object o : objects) {
update(o);
}
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof CacheKey)) {
return false;
}
final CacheKey cacheKey = (CacheKey) object;
if (hashcode != cacheKey.hashcode) {
return false;
}
if (checksum != cacheKey.checksum) {
return false;
}
if (count != cacheKey.count) {
return false;
}
// 对象比较
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (!ArrayUtil.equals(thisObject, thatObject)) {
return false;
}
}
return true;
}
// ... 其他代码省略
}
- BaseExecutor:BaseExecutor 类通过 createCacheKey 方法生成 CacheKey
public abstract class BaseExecutor implements Executor {
// ... 其他代码省略
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
CacheKey cacheKey = new CacheKey();
//SQL语句ID
cacheKey.update(ms.getId());
// 分页参数
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
// SQL
cacheKey.update(boundSql.getSql());
// 入参
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
Configuration configuration = ms.getConfiguration();
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
for (ParameterMapping parameterMapping : parameterMappings) {
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);
}
cacheKey.update(value);
}
}
return cacheKey;
}
// ... 其他代码省略
}
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。