Mybatis四大组件之一:Executor执行器
一:Executor流程图以及类图
1.Executor类图
顶层节点Executor,BaseExecutor中使用了一级缓存PerpetualCache,一共有四个子类,类图中还没标全,分别是SimpleExecutor简单执行器、ReuseExecutor可重用执行器(重用同一个Statement对象)、BatchExecutor批处理执行器、ClosedExecutor关闭的执行器(ResultLoaderMap的内部类,用于标记序列化反序列化后执行器处于关闭状态)。CachingExecutor为二级缓存执行器,包含了二级缓存Cache
2.Executor的访问流程
创建SqlSession调用query方法或者update方法访问二级缓存,再访问BaseExecutor,通过配置文件中指定的执行器类别,或者构造的执行器来执行具体的doQuery方法或者doUpdate方法。Executor通过装饰者模式使用delegate对象向下传递。
二:BaseExecutor和一级缓存
1.BaseExecutor
基本功能:改查、通过提供接口做规范query、update,子类通过doQuery、doUpdate来实现具体功能。
特点:生命周期较短,存储在内存中使用(hashmap),不需要考虑存储大小。
四个子类:
1.简单执行器SimpleExecutor:每次都要构建一个新的prepareStatement对象,对sql语句进行编译
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/xx", "root", "123456");
tran = new JdbcTransaction(conn);
SimpleExecutor executor = new SimpleExecutor(configuration, tran);
MappedStatement ms = configuration.getMappedStatement("com.crw.mybatis.mapper.StudentMapper.getById");
List<Object> list = executor.doQuery(ms, 1, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, ms.getBoundSql(10));
2.重用执行器ReuseExecutor:利用原有的prepareStatement,只执行一次编译,设置了两次参数
3.批处理执行器BatchExecutor:只针对增删改操作的批操作,最后需调用flush提交到数据库
4.已关闭的执行器ClosedExecutor:在序列化反序列化后生成,代表执行器已关闭
2.一级缓存PerpetualCache
命中场景:
1.命名空间、方法、参数 都要一样
2.sqlsession也要一样(一级缓存是会话级缓存)
3.rowBounds分页必须相同
失效场景:
1.sqlSession.clearCache();或者mapper的方法或xml配置了flushCache="true"
2.执行了插入更新操作,会清空sqlSession下所有的缓存
3.缓存作用域改成Statement,执行完statement关闭时就清空缓存
3.源码分析
BaseExecutor的Query源码分析:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//获取动态Sql
BoundSql boundSql = ms.getBoundSql(parameter);
//生成一级缓存key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
@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.");
}
//清空缓存标签要跟父查询一起才能生效,不然嵌套查询就会出现循环依赖的问题
//queryStack用于标记这是嵌套查询的第几层
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--;
}
//嵌套查询中的栈,第一层是0,后面1-2-3递推
//填充缓存或者获取到了真正的值后,且会到了父查询栈时,才执行延迟加载操作
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
//延迟队列中的元素,调用load方法
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
缓存key的格式
-325093600:3010135970:com.crw.mybatis.mapper.StudentMapper.getById:0:2147483647:select id,age,name from student where id = ?:1:development
查不到缓存则执行queryFromDatabase方法
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
//先往缓存中填充占位符,EXECUTION_PLACEHOLDER,与嵌套查询相关
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//执行具体的执行器如SimpleExecutor.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;
}
触发清空一级缓存的四个操作:
1.update
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);
}
2.query中配置flushcache true且queryStack为0,也就是在执行子查询之前(子查询中不清空)
if (queryStack == 0 && ms.isFlushCacheRequired()) {
//清空缓存
clearLocalCache();
}
3.session作用域statement
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
4.手动提交或回滚
@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}
@Override
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
clearLocalCache();
flushStatements(true);
} finally {
if (required) {
transaction.rollback();
}
}
}
}
三:CachingExecutor和二级缓存
1.二级缓存简介
特点:
1.应用级缓存:生命周期贯穿整个应用,跨线程使用(会话不是线程安全的,所以一级缓存是每个会话生命周期内完成,就没有线程安全问题),场景要求命中率更高,改动更小
2.存储:需要考虑存储方式(内存、硬盘)、淘汰策略
3.可以与第三方缓存集成:目的做分布式缓存
实现方式:
Cache责任链模式、装饰者模式
每个会话都会自己的事务缓存管理器,每访问一个缓存空间,就会生成一个暂存区,当执行提交操作后,将暂存区的数据同步到真实二级缓存之中
对象结构如下:
缓存命中情况:
1.会话提交后(flushCache等操作后,都要提交才可以生效)
2.sql语句、参数相同
3.相同的statementId:方法名
4.RowBounds相同
命中率统计:
2.二级缓存源码分析
query方法:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();//1.缓存打开。2.声明缓存空间 不然就为null
if (cache != null) {
//是否要清空缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//事务缓存管理器中获取对象
//Map<Cache, TransactionalCache> transactionalCaches.getObject(key) 通过cache获取到暂存区缓存TransactionalCache,再从中通过key获取到对应的值
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//交给下级查询 下级可能是SimpleExecutor
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//当查询到数据后,数据先放入暂存区中,commit再放入二级缓存
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
transactionalCaches.getObject方法:
public Object getObject(Object key) {
// issue #116
//一旦查到存在暂存区,那么直接调用下一个链条查找缓存,查到的是真实二级缓存中的数据,因为暂存区的数据都是变更后的数据,直接查出意味着脏读
Object object = delegate.getObject(key);
if (object == null) {
//如果查到二级缓存没有数据,则在列表中加入key,来防止缓存击穿
entriesMissedInCache.add(key);
}
// issue #146
//就算二级缓存有数据 clearOnCommit参数为true也返回null
if (clearOnCommit) {
return null;
} else {
return object;
}
}
commit操作:
//遍历暂存区数据刷入缓存区中
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
缓存责任链:
1.线程同步、安全Synchronizedcache
//方法加上了同步标签
public synchronized Object getObject(Object key) {
return delegate.getObject(key);
}
2.记录命中率loggingCache
加上了命中log日志
public Object getObject(Object key) {
requests++;
final Object value = delegate.getObject(key);
if (value != null) {
hits++;
}
if (log.isDebugEnabled()) {
log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
}
return value;
}
3.淘汰LRUCache:默认溢出淘汰策略
public void setSize(final int size) {
//默认淘汰最老的节点
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
4.过期清理ScheduledCache
private boolean clearWhenStale() {
//判断超时
if (System.currentTimeMillis() - lastClear > clearInterval) {
clear();
return true;
}
return false;
}
5.防穿透BlockingCache(阻塞缓存)
private void acquireLock(Object key) {
CountDownLatch newLatch = new CountDownLatch(1);
while (true) {
//判断当前资源是否有其他线程访问,如果没有就是null
//ReentrantLock对资源上锁
CountDownLatch latch = locks.putIfAbsent(key, newLatch);
if (latch == null) {
break;
}
try {
if (timeout > 0) {
boolean acquired = latch.await(timeout, TimeUnit.MILLISECONDS);
if (!acquired) {
//指定时间内不能获取到锁则抛出异常
throw new CacheException(
"Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());
}
} else {
latch.await();
}
} catch (InterruptedException e) {
throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
}
}
}
6.最终存在内存中的PerpetualCache
private final Map<Object, Object> cache = new HashMap<>();
使用map存储
结尾感谢鲁班大叔,从他那里学到了很多Mybatis相关知识。