上一篇[MyBatis框架原理2:SqlSession运行过程][1]介绍了MyBatis的工作流程,其中涉及到了MyBatis缓存的使用,首先回顾一下工作流程图:

如果开启了二级缓存,数据查询执行过程就是首先从二级缓存中查询,如果未命中则从一级缓存中查询,如果也未命中则从数据库中查询。MyBatis的一级和二级缓存都是基于Cache接口的实现,下面先来看看Cache接口和其各种实现类。

Cache接口及常用装饰器

public interface Cache {
String getId();
//缓存中添加数据,key为生成的CacheKey,value为查询结果
void putObject(Object key, Object value);
//查询
Object getObject(Object key);
//删除
Object removeObject(Object key);
//清空缓存
void clear();
//获取缓存数量
int getSize();
//获取读写锁
ReadWriteLock getReadWriteLock();
}

Cache接口位于MyBatis的cache包下,定义了缓存的基本方法,其实现类采用了装饰器模式,通过实现类的组装,可以实现操控缓存的功能。cache包结构如下:

  • PerpetualCache是Cache接口的实现类,通过内部的HashMap来对缓存进行基本的操作,通常配合装饰器类一起使用。
  • BlockingCache装饰器:保证只有一个线程到数据库中查询指定key的数据,如果该线程在BlockingCache中未查找到数据,就获取key对应的锁,阻塞其他查询这个key的线程,通过其内部ConcurrentHashMap来实现,源码如下:
public class BlockingCache implements Cache {

  //阻塞时长
private long timeout;
private final Cache delegate;
//key和ReentrantLock对象一一对应
private final ConcurrentHashMap<Object, ReentrantLock> locks; @Override
public Object getObject(Object key) {
//获取key的锁
acquireLock(key);
//根据key查询
Object value = delegate.getObject(key);
//如果命中缓存,释放锁,未命中则继续持有锁
if (value != null) {
releaseLock(key);
}
return value;
}
@Override
//从数据库获取结果后,将结果放入BlockingCache,然后释放锁
public void putObject(Object key, Object value) {
try {
delegate.putObject(key, value);
} finally {
releaseLock(key);
}
}
...
  • FifoCache装饰器: 先入先出规则删除最早的缓存,通过其内部的Deque实现。
  • LruCache装饰器: 删除最近使用最少的缓存, 通过内部的LinkedHashMap实现。
  • SynchronizedCache装饰器:同步Cache。
  • LoggingCache装饰器: 提供日志功能,记录和输出缓存命中率。
  • SerializedCache装饰器:序列化功能。

CacheKey

CacheKey对象是用来确认缓存项的唯一标识,由其内部ArrayList添加的所有对象来确认两个CacheKey是否相同,通常ArrayList内将添加MappedStatement的id,SQL语句,用户传递给SQL语句的参数以及查询结果集范围RowBounds等,CacheKey源码如下:

public class CacheKey implements Cloneable, Serializable {
...
private final 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>();
} //向updateLis中添加对象
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); count++;
checksum += baseHashCode;
baseHashCode *= count; hashcode = multiplier * hashcode + baseHashCode; updateList.add(object);
} @Override
//重写equals方法判断CacheKey是否相同
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中每一项
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;
}
}

一级缓存

一级缓存是session级别缓存,只存在当前会话中,在没有任何配置下,MyBatis默认开启一级缓存,当一个SqlSession第一次执行SQL语句和参数查询时,将生成的CacheKey和查询结果放入缓存中,下一次通过相同的SQL语句和参数查询时,就会从缓存中获取,当进行更新或者插入操作时,一级缓存会进行清空。在上一篇中说到,MayBatis进行一级缓存查询和写入是由BaseExecutor执行的,源码如下:

  • 初始化缓存:

    一级缓存是Cache接口的PerpetualCache实现类对象
public abstract class BaseExecutor implements Executor {

  ...
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration; protected int queryStack;
private boolean closed; protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
//一级缓存初始化
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
...
  • 生成CacheKey

    BaseExecutor生成CacheKey,CacheKey的updateList中放入了MappedStatement,传入SQL的参数,结果集范围rowBounds和boundSql:
  @Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
  • 将查询结果和CacheKey放入缓存:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
//缓存中放入CacheKey和占位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//在数据库中查询操作
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
//缓存中放入CacheKey和结果集
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
//返回结果
return list;
}
  • 再次执行相同查询条件时从缓存获取结果:
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;
}
  • 更新操作时清空缓存:
 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);
}

通过以下代码验证下,分别开两个session进行相同的查询,第一个session查询两次:

public void testSelect() {
SqlSession sqlSession = sqlSessionFactory.openSession();
User user = sqlSession.selectOne("findUserById", 1);
System.out.println(user);
User user2 = sqlSession.selectOne("findUserById", 1);
System.out.println(user2);
sqlSession.close();
System.out.println("sqlSession closed!===================================");
//新建会话
SqlSession sqlSession2 = sqlSessionFactory.openSession();
User user3 = sqlSession2.selectOne("findUserById", 1);
System.out.println(user3);
sqlSession2.close();
}

把日志设置为DEBUG级别得到运行日志:

DEBUG [main] - ==>  Preparing: SELECT * FROM user WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d]
DEBUG [main] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d]
DEBUG [main] - Returned connection 369241501 to pool.
sqlSession closed!===================================
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Checked out connection 369241501 from pool.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d]
DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d]
DEBUG [main] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d]
DEBUG [main] - Returned connection 369241501 to pool.

第一次会话中,虽然查询了两次id为1的用户,但是只执行了一次SQL,关闭会话后开启一次新的会话,再次查询id为1的用户,SQL再次执行,说明了一级缓存只存在SqlSession中,不同SqlSession不能共享。

二级缓存

二级缓存是Mapper级别缓存,也就是同一Mapper下不同的session共享二级缓存区域。

只需要在XML映射文件中增加cache标签或cache-ref标签标签就可以开启二级缓存,cache-ref标签配置的是共享其指定Mapper的二级缓存区域。具体配置信息如下:

  • blocking : 是否使用阻塞缓存
  • readOnly : 是否只读
  • eviction: 缓存策略,可指定Cache接口下装饰器类FifoCache、LruCache、SoftCache和WeakCache
  • flushInterval : 自动刷新缓存时间
  • size : 设置缓存个数
  • type : 设置缓存类型,用于自定义缓存类,默认为PerpetualCache

二级缓存是在MyBatis的解析配置文件时初始化,在XMLMapperBuilder中将缓存配置解析:

private void cacheElement(XNode context) throws Exception {
if (context != null) {
//指定默认类型为PerpetualCache
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
//默认缓存策略为LruCache
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
//委托builderAssistant构建二级缓存
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}

构建过程:

public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
Cache cache = new CacheBuilder(currentNamespace)
//设置缓存类型,默认为PerpetualCache
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
//设置缓存策略,默认使用LruCache装饰器
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
//设置刷新时间
.clearInterval(flushInterval)
//设置大小
.size(size)
//设置是否只读
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}

最终得到默认的二级缓存对象结构为:

CachingExecutor将初始化的Cache对象用TransactionalCache包装后放入TransactionalCacheManager的Map中,下面代码中的tcm就是TransactionalCacheManager对象,CachingExecutor执行二级缓存操作过程:

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//从Configuration的MappedStatement中获取二级缓存
Cache cache = ms.getCache();
if (cache != null) {
//判断是否需要刷新缓存,SELECT不刷新,INSERT|UPDATE|DELETE刷新缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//从二级缓存中获取数据
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//委托BaseExecutor查询
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//查询结果放入二级缓存
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

通过之前一级缓存的例子验证二级缓存,只需要在UserMapper映射文件中加入cache标签,并且让相关POJO类实现java.io.Serializable接口,运行得到日志:

DEBUG [main] - ==>  Preparing: SELECT * FROM user WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
DEBUG [main] - Cache Hit Ratio [com.kkb.mybatis.mapper.UserMapper]: 0.0
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@5c072e3f]
DEBUG [main] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@5c072e3f]
DEBUG [main] - Returned connection 1543974463 to pool.
sqlSession closed!===================================
DEBUG [main] - Cache Hit Ratio [com.kkb.mybatis.mapper.UserMapper]: 0.3333333333333333
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]

不同session查询同一条记录时,总共只执行了一次SQL语句,并且日志打印出了缓存的命中率,这时候不同session已经共享了二级缓存区域。

[1]: https://www.cnblogs.com/abcboy/p/9656302.html

最新文章

  1. 【转载】如何用IntelliJ IDEA 14 创建Web项目
  2. eclipse自动排版JSP问题
  3. FTP多任务下载实现类
  4. ASP.NET Web API涉及到的上下文
  5. IntelliJ IDEA 编译方式介绍
  6. Oracle 的过程与函数
  7. 算法系列9《MD5》
  8. BZOJ1500 维修数列
  9. VulToEs
  10. SQL-LINQ-Lambda语法对照
  11. asp.net利用ajax和jquery-ui实现进度条
  12. adb调试实用命令
  13. echarts实现上海地域PM值(map、timeline)
  14. oracle累计求和
  15. ExtJS的4.1新特性简要介绍
  16. nodejs-ORM 操作数据库中间件waterline的使用
  17. SharedPreferences 存List集合,模拟数据库,随时存取
  18. Linuxc - gdb调试程序
  19. Java 由浅入深GUI编程实战练习(二)
  20. GCC后端移植杂记

热门文章

  1. 浏览器自动将参数中的url转码问题
  2. The Preliminary Contest for ICPC Asia Nanchang 2019 E. Magic Master
  3. Windows系统下如何卸载干净mysql
  4. Python2和Python3中列表推导式的不同
  5. css3之3D 旋转立方体与哆啦A梦
  6. js中的“==”与“===”的区别
  7. 信息提示框:MessageBox
  8. Angular 文档中的修改链接是从哪里改的
  9. 在vue中如何安装使用Vant
  10. Jmeter连接Redis服务缓存