1、MyBatis缓存的使用
MyBatis拥有一级缓存和二级缓存,MyBatis默认开启一级缓存(无法关闭),二级缓存默认也是开启,但需要在每个Mapper.xml既每个MappedStatement类添加Cache标签,二级缓存才会有效。在Configuration中将cacheEnabled设置为false,将关闭二级缓存。
MyBatis二级缓存的使用:
- 在MyBatis主配置文件中指定cacheEnabled属性值为true
在MyBatis-config.xml中配置
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
- 在MyBatis Mapper配置文件中,配置缓存策略、缓存刷新频率、缓存的容量等属性:
<cache eviction="FIFO"
flushInterval="6000"
size="512"
readOnly="true"/>
- 在配置Mapper时,通过useCache属性指定Mapper执行时是否使用缓存。另外,还可以通过配置flushCache属性指定Mapper执行后是否刷新缓存,针对这条语句开启二级缓存 例如:
<select id="listAllUser"
flushCache="false"
useCache="true"
resultType="com.blog4java.mybatis.example.entity.UserEntity">
select
<include refid="userAllFied"/>
from user
</select>
2、MyBatis 缓存实现类
MyBatis的缓存是基于JVM堆内存实现的,既所有缓存数据都存放在Java对象中。MyBatis通过Cache接口定义缓存对象的行为,Cache接口代码如下:
public interface Cache {
// 用于缓存缓存Key,一般Key是由Mapper的命名空间名称
String getId();
// Key为CacheKey实例,value为需要缓存的对象
void putOject(Object key Object value);
// 根据CacheKey获取缓存的对象
Object getObject(Object key);
// 根据CacheKey移除数据
Object removeObject(Object key);
// 清空缓存
void clear();
// 获取大小
int getSize();
// 读写锁,该方法在3.2.6版本后已经不在使用
ReadWriteLock getReadWriteLock();
}
MyBatis采用装饰器模式设计缓存类,Cache接口有一个基本的实现类,既PerpetualCache类,该类内部只有一个类型为String的id属性和一个类型为HashMap的cache属性。此类重写了equals()方法,当两个缓存对象的id相同时,既认为两个缓存对象相同,另外,PerpetualCache还重写了hashCod()方法,仅缓存对象的Id作为因子生成hashCode。
实现Cache接口的实现类有如下几个:
- PrepetualCache: 基本实现类,类中仅有一个类型为String的id属性和一个类型为HashMap的cache属。
- BlockingCache: 阻塞版的缓存装饰器,能够保证同一时间只有一个线程到缓存中查找指定的Key对应的数据。
- FifoCache: 先入先出缓存装饰器,FifoCache内部有一个维护具有长度限制的Key键值链表(LinkedList实例)和一个被装饰的缓存对象,Key值链表主要是维护Key的FIFO顺序,而缓存存储和获取则交给被装饰的缓存对象来完成。
- LoggingCache: 为缓存增加日志输出功能,记录缓存的请求次数和命中次数,通过日志输出缓存命中率。
- LruCache: 最近最少使用的缓存装饰器,当缓存容量满了之后,使用LRU算法淘汰最近最少使用的Key和Value。LruCache中通过重写LinkedHashMap类的removeEldestEntry()方法获取最近最少使用的Key值,将Key值保存在LruCache类的eldestKey属性中,然后在缓存中添加对象时,淘汰eldestKey对应的Value值。具体实现细节读者可参考LruCache类的源码。
- ScheduledCache: 自动刷新缓存装饰器,当操作缓存对象时,如果当前时间与上次清空缓存的时间间隔大于指定的时间间隔,则清空缓存。清空缓存的动作由getObject()、putObject()、removeObject()等方法触发。
- SerializedCache: 序列化缓存装饰器,向缓存中添加对象时,对添加的对象进行序列化处理,从缓存中取出对象时,进行反序列化处理。
- SoftCache: 软引用缓存装饰器,SoftCache内部维护了一个缓存对象的强引用队列和软引用队列,缓存以软引用的方式添加到缓存中,并将软引用添加到队列中,获取缓存对象时,如果对象已经被回收,则移除Key,如果未被回收,则将对象添加到强引用队列中,避免被回收,如果强引用队列已经满了,则移除最早入队列的对象的引用。
- SynchronizedCache: 线程安全缓存装饰器,SynchronizedCache的实现比较简单,为了保证线程安全,对操作缓存的方法使用synchronized关键字修饰。TransactionalCache:事务缓存装饰器,该缓存与其他缓存的不同之处在于,TransactionalCache增加了两个方法,即commit()和rollback()。当写入缓存时,只有调用commit()方法后,缓存对象才会真正添加到TransactionalCache对象中,如果调用了rollback()方法,写入操作将被回滚。
- WeakCache: 弱引用缓存装饰器,功能和SoftCache类似,只是使用不同的引用类型。
3、MyBatis 一级缓存实现原理
MyBatis的一级缓存是存放在BaseExecutor中,由于BaseExecutor是由SqlSession创建的,所以一级缓存是属于Session级别的。
public abstract class BaseExecutor implements Executor {
...
// 一级缓存
protected PerpetualCache localCache;
// 缓存存储过程的结果
protected PerpetualCache localOutputParameterCache;
// MyBatis的配置信息
protected Configuration configuration;
...
查询操作(query):
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获取绑定的Sql语句对象
BoundSql boundSql = ms.getBoundSql(parameter);
// 根据与这条SQL语句相关联的值(MappedStatement,参数parameter,返回值访问rowBounds,SQL语句boundSql),获取对应的CacheKey,保证CacheKey是此条SQL语句的唯一标识。
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
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());
// 如果此Executor已关闭,将抛出异常
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 如果flushCacheRequired为true,表示每次请求都刷新缓存。更新,新增,删除语句默认都是true,而查询语句默认是false
if (queryStack == 0 && ms.isFlushCacheRequired()) {
// 清空一级缓存
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 如果ResultHandler为null,将从一级缓存中获取数据。
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;
}
// 从数据库中查找数据,并将数据写入到缓存中
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 {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 将数据库中查出来的结果存储到一级缓存中
localCache.putObject(key, list);
// 如果该mappedStatement是CALLABLE类型的
if (ms.getStatementType() == StatementType.CALLABLE) {
// 将数据结果存储到存放存储过程的cache中
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
更新操作
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 在执行更新操作之前,会清空一级缓存
clearLocalCache();
return doUpdate(ms, parameter);
}
4、MyBatis 二级缓存
MyBatis二级缓存存在于CachingExecutor对象中,在该对象中具有一个tcm属性,是TransactionalCacheManager类型,里面封装了Map<Cache, TransactionalCache>对象。Cache对应与一个Namespace,既一个Mapper,其中TransactionalCache对应一个MappedStatement(既:一个Mapper中的一个方法)
二级缓存是Namespace级别的
二级缓存在Configuration中默认是开启的,但还需要在每个Mapper中添加<cache></cache>标签。同时满足两个都开启才算开启二级缓存。
缓存只针对于查询操作,修改操作会清空所有缓存。
从源码的角度分析
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 构建缓存Key,通过此CacheKey能够获取二级缓存中的数据
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 如果没有添加<cache>标签,在加载MappedStatement时,MappedStatement对象中的cache将会null,
Cache cache = ms.getCache();
if (cache != null) {
// 是否每次执行清空此namespace中的缓存
flushCacheIfRequired(ms);
// 如果是开启缓存,并且ResulHandler为null
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);
}
注意:
在分布式环境下,务必将MyBatis的localCacheScope属性设置为STATEMENT,避免其他节
点执行SQL更新语句后,本节点缓存得不到刷新而导致的数据一致性问题。
小结
- 每执行一条Sql语句对应一个MappedStatement对象,只有当执行同一条SQL语句,并且条件参数,返回结果条数范围相同时,才会读取到之前缓存的结果。
- 每条更新操作(新增,修改,删除)的SQL语句都会清空一级缓存。
- 每条查询语句对应的MappedStatement对象的flushCacheRequired属性默认都是false
- 在Spring中每执行一次sql语句就会开启一个SqlSession,操作执行完后,就会关闭SqlSession。 在一般的开发中,sql语句返回的结果我们无法在下次执行的SQL语句的时候获取到此缓存的值,因为之前的SqlSession已关闭。但存在一个情况会使用到SqlSession,就是在某个方法中添加了@Transactional注解(事务注解),在里面同时执行两次sql语句,第二次会从一级缓存中获取数据。因为添加@Transactional事务的方法里,只有所有sql语句执行成功,才会commit,等commit之后才会关闭SqlSession。
文章对你如果有帮助,请点个赞,谢谢!