### 1. MyBatis缓存机制的核心构件
1.1 Cache接口
接口简单明了,Cache的基本操作;put/get/remove/clear。
public interface Cache {
String getId();//分组ID
void putObject(Object key, Object value);//put
Object getObject(Object key);//get
Object removeObject(Object key);//remove
void clear();//clear
int getSize();//可选 core不再使用
ReadWriteLock getReadWriteLock();//3.2.6后已废弃
}
1.2 Cache的基本实现:PerpetualCache
通过HashMap实现缓存管理。
public class PerpetualCache implements Cache {
private String id;//ID进行分组
private Map<Object, Object> cache = new HashMap<Object, Object>();//数据缓存
//...实现接口,就是对HashMap的操作
}
1.3 CacheKey的构成
核心功能,可以动态更新Key的hash值。因为底层缓存数据是基于HashMap实现的,在比对value|key是否存在时,会调用hashCode方法和equals。
CacheKey覆写了hashcode和equals。
比较顺序:hashCode–>checksum–>count–>updateList,只要有一个不等则说明不是相同的Key。
public class CacheKey implements Cloneable, Serializable {
//...
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
private int multiplier;
private int hashcode;
private long checksum;
private int count;
private List<Object> updateList;
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
this.updateList = new ArrayList<Object>();
}
//...
//hashcode的计算方法
private void doUpdate(Object object) {
int baseHashCode = object == null ? 1 : object.hashCode();
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;//hashcode的算法
updateList.add(object);
}
//...
//比较2个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;
}
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;
}
//...
}
1.4 MyBatis中CacheKey的创建
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
//...
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
//...
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
//...
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
Key的构成:cacheKey=ID + offset + limit + sql + parameterValues + environmentId
2. MyBatis的一级缓存
一级缓存,即本地缓存,永久缓存,Mybatis自行控制缓存的读写删,在执行器(BaseExecutor)中使用,执行器是在SqlSessionFactory.openSession()后创建的SqlSession中执行SQL语句时创建的,所以一级缓存的生命周期等同于SqlSession,也就是说是Session级的缓存。
2.1 一级缓存的创建、清空和销毁
MyBatis在commit和rollback时都会清空一级缓存,在SqlSession关闭时也会主动清空缓存,可以被GC掉。
protected BaseExecutor(Configuration configuration, Transaction transaction) {
//...
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
//...
}
@Override
public void close(boolean forceRollback) {
try {
//...
} finally {
//...
localCache = null;
localOutputParameterCache = null;
//...
}
}
@Override
public void commit(boolean required) throws SQLException {
//...
clearLocalCache();
//...
if (required) {
transaction.commit();
}
}
@Override
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
clearLocalCache();
//...
} finally {
if (required) {
transaction.rollback();
}
}
}
}
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
2.2 一级缓存的刷新
缓存必然是针对Select查询操作提高查询效率设计的,在Mapper XML的配置文件中可以设置 flushCache=”ture”(默认为false)来刷新一级缓存(也包括二级缓存),任何时候只要语句被调用,都会清空一级缓存和二级缓存。
2.3 一级缓存的配置
mybatis-config.xml
<settings>
//...
<setting name="localCacheScope" value="SESSION | STATEMENT"/>
//...
</settings>
localCacheScope默认为SESSION,缓存一个会话中执行的所有的查询结果,如果设置为STATEMENT,则在同一个会话中的不同的调用将不会共享数据,在调用完毕后会清理一级缓存。
//...
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
//...
2.4 嵌套查询的优化
MyBatis利用一级缓存机制加速重复嵌套查询。
private static class DeferredLoad {
//...
public boolean canLoad() {
return localCache.getObject(key) != null && localCache.getObject(key) != EXECUTION_PLACEHOLDER;
}
public void load() {
//...
List<Object> list = (List<Object>) localCache.getObject(key);
//...
}
}
//...
3. MyBatis的二级缓存
实现 org.mybatis.cache.Cache 接口都可以作为MyBatis的二级缓存;二级缓存的Cache对象由Configuration进行管理,而
每次构建SqlSessionFactory对象时都会创建新的Configuration对象,因此,二级缓存的生命周期与SqlSessionFactory是相同的。基于Mapper XML 配置,在创建每个MapperedStatement对象时,都会根据其所属的namespace名称空间,给其分配Cache缓存实例。
二级缓存由CachingExecutor负责管理维护,开启了二级缓存后,Executor将使用CacheExecutor,基于装饰器模式对配置的Executor进行包装,扩展了缓存功能。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
//...
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
//...
}
3.1 二级缓存的配置
mybatis-config.xml配置,false则不启用二级缓存,ture则自动使用CachingExecutor。
<settings>
//...
<setting name="cacheEnabled" value="true|false" />
//...
</settings>
Mapper XML配置
xxx.mapper.xml
<cache
type="PERPETUAL"//
eviction="LRU"//算法
flushInterval="60000"//刷新间隔,间隔60秒清空缓存,被动触发非定时器轮询
size="512"//大小
readOnly="false"//true:返回cache结果的克隆对象
blocking="false"
/>
根据缓存器配置创建Cache实例,以namespace为CacheId,一个namespace对应一个Cache实例。
可以通过实现 org.mybatis.cache.Cache 接口创建自定义的缓存器,比如基于Redis或者Memcached实现分布式缓存。
3.2 MyBatis内置的二级缓存算法
Mybatis的所有Cache算法都是基于装饰器模式对PerpetualCache扩展增加功能。
FIFO:先入先出,基于LinkedList实现;
LRU:最近最少使用,基于LinkedHashMap实现,在put的时候,自动移除最少使用缓存对象;
SOFT:对Cache的value进行SoftReference包装;当缓存对象是Soft reference可达时,gc会向系统申请更多内存,而不是直接回收它,当内存不足的时候才回收它;
WEAK:对Cache的value进行WeakReference包装;WeakReference不会强制对象保存在内存中。它拥有比较短暂的生命周期,允许你使用垃圾回收器的能力去权衡一个对象的可达性。在垃圾回收器扫描它所管辖的内存区域过程中,一旦gc发现对象是weakReference可达,就会把它放到ReferenceQueue中,等下次gc时回收它。