MyBatis的二级缓存是Application级别的缓存,它可以提高对数据库查询的效率,以提高应用的性能。本文将全面分析MyBatis的二级缓存的设计原理。
本文目录结构如下:
如上图所示,当开启一个会话时,SqlSession对象首先会到CachingExecutor中进行判断二级缓存中是否可以命中数据(当Mybatis没有使用二级缓存时,也会首先到CachingExecutor中,只不过if 判断不通过,不会执行二级缓存流程),若二级缓存命中数据,则直接返回结果。否则继续到BaseExecutor中执行流程,在BaseExecutor中首先会判断 一级缓存LocalCache是否可以命中数据,若命中则直接返回结果,否则到数据库中获取数据,然后将数据库中获取的数据缓存到一级缓存LocalCache中。若开启了二级缓存,还会将数据库中回去的数据缓存到二级缓存 TransactionalCacheManager中。
CachingExecutor是 Executor的装饰者,使用到设计模式中的装饰者模式。用于增强Executor,使用其具备缓存功能。
如上图所示:MyBatis中的Cache以SPI实现,给需要集成其它Cache或者自定义Cache提供了接口。
Mybatis二级缓存也不例外,但是二级缓存的嵌套(装饰)比较深。
// CachingExecutor query(二级缓存入口)
@Override
public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
//判断是否开启缓存
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//从二级缓存 TransactionalCacheManeger中查找缓存
List list = (List) 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);
}
如上图 Mybatis二级缓存入口代码为例:
缓存的获取通过Mybatis的一个工具TransactionalCacheManager
来取出。实际的缓存K/V存放数据结构非常复杂。Key(参见CacheKey
)与一级缓存一致, V层次较深, 也大量使用到了包装器模式, 包装层次为:
如上如所示:二级缓存走向较为复杂,但是上图中所有的类(除HashMap外)都实现了Cache接口,每个XXXCache都有各自的职责,通过装饰走向的方式增加二级缓存Cache.
Mybatis默认开启会话级别一级缓存,但是二级缓存是关闭的。如果想要使用Mybatis的二级缓存,需要经过以下几个配置:
1. XML配置文件中开启二级缓存 全局配置变量参数 cacheEnabled=true
2. 在Mapper XML 中添加 或 标签开启Mapper Cache
3. 在select 语句上添加 useCache = true
MyBatis对二级缓存的支持粒度很细,它会指定某一条查询语句是否使用二级缓存。
虽然在Mapper中配置了
Mybatis将二级缓存的粒度控制的非常小,使得缓存的使用更加通用,足以满足局部查询数据缓存。当我们尝试在项目做数据缓存时,没有必要一上来直接上Redis (即使Redis性能很好(相对情况下),但是仅仅为了局部数据缓存就在项目中引入Redis也是不太合理的。),可以首先考虑使用Mybatis的二级缓存。
使用Mybatis二级缓存注意事项:
1. 只能在【只有单表操作】的表上使用缓存 (多表可能会出现脏数据)
不只是要保证这个表在整个系统中只有单表操作,而且和该表有关的全部操作必须全部在一个namespace下。
2. 在可以保证查询远远大于insert,update,delete操作的情况下使用缓存
当然,使用Mybatis二级缓存也可以考虑使用第三方提供的缓存组件,比如: ehcache等.
也可以自己设计一个Cache,比如借用项目中已有的Redis进行缓存数据,自定义Cache 只需要实现Cache接口即可,然后配置自定义Cache即可使用,下面是笔者自己使用Redis实现的Cache,仅供参考:
package com.ssm.demo;
import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
*
* 使用Redis实现Mybatis二级缓存
*
* @author: chengxiaonan
**/
public class MybatisRedisCache implements Cache {
//private static final Logger logger = LoggerFactory.getLogger(MybatisRedisCache.class);
// 读写锁
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
private RedisTemplate redisTemplate = SpringContextHolder.getBean("redisTemplate");
private String id;
public MybatisRedisCache(final String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
//logger.info("Redis Cache id " + id);
this.id = id;
}
@Override
public String getId() {
return this.id;
}
@Override
public void putObject(Object key, Object value) {
if (value != null) {
// 向Redis中添加数据,有效时间是2天
redisTemplate.opsForValue().set(key.toString(), value, 2, TimeUnit.DAYS);
}
}
@Override
public Object getObject(Object key) {
try {
if (key != null) {
Object obj = redisTemplate.opsForValue().get(key.toString());
return obj;
}
} catch (Exception e) {
//logger.error("redis ");
}
return null;
}
@Override
public Object removeObject(Object key) {
try {
if (key != null) {
redisTemplate.delete(key.toString());
}
} catch (Exception e) {
}
return null;
}
@Override
public void clear() {
//logger.debug("清空缓存");
try {
Set keys = redisTemplate.keys("*:" + this.id + "*");
if (!CollectionUtils.isEmpty(keys)) {
redisTemplate.delete(keys);
}
} catch (Exception e) {
}
}
@Override
public int getSize() {
Long size = (Long) redisTemplate.execute(new RedisCallback() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
return connection.dbSize();
}
});
return size.intValue();
}
@Override
public ReadWriteLock getReadWriteLock() {
return this.readWriteLock;
}
}