十Mybatis 缓存系统解析
Posted archerLuo罗
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了十Mybatis 缓存系统解析相关的知识,希望对你有一定的参考价值。
本文从以下几个方面介绍:
- 1、如何开启 Mybatis 的缓存
- 2、缓存的核心接口以及底层实现
- 3、一级缓存的实现过程
- 4、二级缓存的实现过程
- 5、缓存的装饰器
前言
为了提高查询速度,减少数据库压力;Mybatis 提供了缓存功能,它分为一级缓存和二级缓存。
Mybatis 缓存系统的实现使用了 模板方法模式 和 装饰器模式
1、如何开启 Mybatis 的缓存
1.1、一级缓存的作用
Mybatis 的一级缓存是会话级别的缓存,Mybatis 每创建一个 SqlSession 对象,就表示打开一次数据库会话,在一次会话中,应用程序很可能在短时间内反复执行相同的查询语句,如果不对数据进行缓存,则每查询一次就要执行一次数据库查询,这就造成数据库资源的浪费。又因为通过 SqlSession 执行的操作,实际上由 Executor 来完成数据库操作的,所以在 Executor 中会建立一个简单的缓存,即一级缓存;将每次的查询结果缓存起来,再次执行查询的时候,会先查询一级缓存,如果命中,则直接返回,否则再去查询数据库并放入缓存中。
1.2、一级缓存的开启以及生命周期
一级缓存的生命周期与 SqlSession 的生命周期相同,当调用 Executor.close 方法的时候,缓存变得不可用。一级缓存是默认开启的,一般情况下不需要特殊的配置,如果需要特殊配置,则可以通过插件的形式来实现
1.3、二级缓存的开启以及生命周期
Mybatis 提供的二级缓存是应用级别的缓存,它的生命周期和应用程序的生命周期相同,且与二级缓存相关的配置有以下 3 个
- 1)、 mybatis-config.xml 配置文件中的 cacheEnabled 配置,它是二级缓存的总开关,只有该配置为 true ,后面的缓存配置才会生效。默认为 true,即二级缓存默认是开启的。
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
- 2)、Mapper.xml 配置文件中配置的 和 标签,如果 Mapper.xml 配置文件中配置了这两个标签中的任何一个,则表示开启了二级缓存的功能,如果配置了 标签,则在解析配置文件的时候,会为该配置文件指定的 namespace 创建相应的 Cache 对象作为其二级缓存(默认为 PerpetualCache 对象),如果配置了 节点,则通过 ref 属性的namespace值引用别的Cache对象作为其二级缓存。通过 和 标签来管理其在namespace中二级缓存功能的开启和关闭
- 3)、 节点中的 useCache 属性也可以开启二级缓存,该属性表示查询的结果是否要存入到二级缓存中,该属性默认为 true,也就是说 标签默认会把查询结果放入到二级缓存中
2、缓存的核心接口
2.1、Cache
Mybatis 使用 Cache 来表示缓存,它是一个接口,定义了缓存需要的一些方法
public interface Cache {
//获取缓存的id,即 namespace
String getId();
// 添加缓存
void putObject(Object key, Object value);
//根据key来获取缓存对应的值
Object getObject(Object key);
// 删除key对应的缓存
Object removeObject(Object key);
// 清空缓存
void clear();
// 获取缓存中数据的大小
int getSize();
//取得读写锁, 从3.2.6开始没用了
ReadWriteLock getReadWriteLock();
}
2.2、PerpetualCache
Mybatis 为 Cache 接口提供的真正的实现类是 PerpetualCache,其他的只是应用装饰器模式,提供能额外功能,
如线程安全缓存 SynchronizedCache,它的底层存储还是 PerpetualCache。
PerpetualCache 的实现就是一个简单的 HashMap
public class PerpetualCache implements Cache {
// id,一般对应mapper.xml 的namespace 的值
private String id;
// 用来存放数据,即缓存底层就是使用 map 来实现的
private Map<Object, Object> cache = new HashMap<Object, Object>();
public PerpetualCache(String id) {
this.id = id;
}
//......其他的getter方法.....
// 添加缓存
@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);
}
// 清空缓存
@Override
public void clear() {
cache.clear();
}
}
2.3、CacheKey
Mybatis 的缓存使用了 key-value 的形式存入到 HashMap 中,而 key 的话,Mybatis 使用了 CacheKey 来表示 key,
它的生成规则为:mappedStementId + offset + limit + SQL + queryParams + environment生成一个哈希码.
public class CacheKey implements Cloneable, Serializable {
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
// 参与计算hashcode,默认值为37
private int multiplier;
// CacheKey 对象的 hashcode ,默认值 17
private int hashcode;
// 检验和
private long checksum;
// updateList 集合的个数
private int count;
// 由该集合中的所有对象来共同决定两个 CacheKey 是否相等
private List<Object> updateList;
public int getUpdateCount() {
return updateList.size();
}
// 调用该方法,向 updateList 集合添加对应的对象
public void update(Object object) {
if (object != null && object.getClass().isArray()) {
// 如果是数组,则循环处理每一项
int length = Array.getLength(object);
for (int i = 0; i < length; i++) {
Object element = Array.get(object, i);
doUpdate(element);
}
} else {
doUpdate(object);
}
}
// 计算 count checksum hashcode 和把对象添加到 updateList 集合中
private void doUpdate(Object object) {
int baseHashCode = object == null ? 1 : object.hashCode();
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
// 判断两个 CacheKey 是否相等
@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;
}
// 如果前几项都不满足,则循环遍历 updateList 集合,判断每一项是否相等,如果有一项不相等则这两个CacheKey不相等
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (thisObject == null) {
if (thatObject != null) {
return false;
}
} else {
if (!thisObject.equals(thatObject)) {
return false;
}
}
}
return true;
}
@Override
public int hashCode() {
return hashcode;
}
}
CacheKey 的创建
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
//cacheKey 对象
CacheKey cacheKey = new CacheKey();
// 向 updateList 存入id
cacheKey.update(ms.getId());
// 存入offset
cacheKey.update(rowBounds.getOffset());
// 存入limit
cacheKey.update(rowBounds.getLimit());
// 存入sql
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
String propertyName = parameterMapping.getProperty();
MetaObject metaObject = configuration.newMetaObject(parameterObject);
Object value = metaObject.getValue(propertyName);
// 存入每一个参数
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// 存入 environmentId
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
3、一级缓存的实现
Mybatis 的一级缓存是在 BaseExecutor 中实现的
3.1、Executor
Executor 接口定义了操作数据库的基本方法,我们用 SqlSession 来执行 sql时,其实是操作了 Executor 的相关方法
public interface Executor {
ResultHandler NO_RESULT_HANDLER = null;
// insert | update | delete 的操作方法
int update(MappedStatement ms, Object parameter) throws SQLException;
// 查询,带分页,带缓存
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
// 查询,带分页
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
// 查询存储过程
<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
//刷新批处理语句
List<BatchResult> flushStatements() throws SQLException;
// 事务提交
void commit(boolean required) throws SQLException;
// 事务回滚
void rollback(boolean required) throws SQLException;
// 创建缓存的key
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
// 是否缓存
boolean isCached(MappedStatement ms, CacheKey key);
// 清空缓存
void clearLocalCache();
// 延迟加载
void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
// 获取事务
Transaction getTransaction();
}
3.2、BaseExecutor
BaseExecutor 是一个抽象类,实现了 Executor 接口的所有方法,使用了模板模式和策略模式,抽象了 (doUpdate, doQuery, doQueryCursor, doFlushStatement)4个核心方法用来执行sql,它们由不同的子类实现
Mybatis 的一级缓存就是在该类中实现的。
具体代码实现如下
@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 {
// 缓存中获取不到,则调用queryFromDatabase()方法从数据库中查询
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;
}
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 {
// 调用doQuery()方法查询
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;
}
4、二级缓存的实现
Mybatis 的二级缓存是用 CachingExecutor 来实现的,它是 Executor 的一个装饰器类。为 Executor 对象添加了缓存的功能。
在介绍 CachingExecutor 之前,先来看看 CachingExecutor 依赖的两个类,TransactionalCacheManager 和 TransactionalCache。
4.1、TransactionalCache
TransactionalCache 实现了 Cache 接口,主要用于保存在某个 SqlSession 的某个事务中需要向某个二级缓存中添加的数据,代码如下:
public class TransactionalCache implements Cache {
// 底层封装的二级缓存对应的Cache对象
private Cache delegate;
// 为true时,表示当前的 TransactionalCache 不可查询,且提交事务时会清空缓存
private boolean clearOnCommit;
// 存放需要添加到二级缓存中的数据
private Map<Object, Object> entriesToAddOnCommit;
// 存放未命中缓存的 CacheKey 对象
private Set<Object> entriesMissedInCache;
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<Object, Object>();
this.entriesMissedInCache = new HashSet<Object>();
}
// 添加缓存数据的时候,先暂时放到 entriesToAddOnCommit 集合中,在事务提交的时候,再把数据放入到二级缓存中,避免脏数据
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
// 提交事务,
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
// 把 entriesToAddOnCommit 集合中的数据放入到二级缓存中
flushPendingEntries();
reset();
}
// 把 entriesToAddOnCommit 集合中的数据放入到二级缓存中
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
// 放入到二级缓存中
delegate.putObject(entry.getKey(), entry.getValue()Mybatis源码解析MyBatis的二级缓存源码解析