深入了解Mybatis架构设计

架构设计

我们可以把Mybatis的功能架构分为三层:

API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。

Mybatis和数据库的交互有两种方式:

使用传统的Mybatis提供API 使用Mapper代理的方式 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。他主要的目的是根据调用的请求完成一次数据库操作。 基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来最为基础组件。为上层的数据处理层提供最基础的支撑。

Mybatis主要构件

构件 描述 SqlSession 作为Mybatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删查改功能 Executor Mybatis执行器,是Mybatis调度的核心,负责SQL语句的生成和查询缓存的维护 StatementHandler 封装了JDBC Statement操作,负责对JDBC statement的操作,如设置参数、将Statement结果集转换为List集合 ParameterHandler 负责对用户传递的参数转换为JDBC Statement所需要的参数 ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换为List类型的集合 TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换 MappedStatement MappedStatement维护了一条<select、 update 、 delete 、insert >节点的封装 SqlSource 负责根据用户传递的parameterObject,动态的生成SQL语句,将信息封装到BoundSql对象中 BoundSql 表示动态生成的SQL语句以及相应的参数信息

总体流程:

加载配置并初始化

配置来源于两个地方,一个是配置文件(conf.xml,mapper*.xml),一个是java代码中的注解,将配置文件内容封装到Configuration,将sql的配置信息加载成为一个mappedstatement对象,存储在内存中。 2. 接收调用请求

触发条件:调用Mybatis提供的API

传入参数:为SQL的ID和传入的参数

将请求传递给下层的请求处理层进行处理

处理操作请求 根据SQL的ID查找对应的MappedStatement对象 根据传入参数对象解析,得到最终要执行的SQL和执行传入参数 获取数据库连接,将最终SQL语句和参数给到数据库执行,并得到执行结果 根据MappedStatement对象中的结果映射配置对得到的执行结果进行转换处理,并得到最终的处理结果 释放连接资源 返回处理结果

Mybatis缓存

Mybatis有一级缓存和二级缓存。Mybatis收到查询请求后首先会查询二级缓存,若二级缓存未命中,再去查询一级缓存,一级缓存没有,再查询数据库。

一级缓存

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++;
//从localCache缓存里查数据,没有就去查数据库
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;
}

这个localCache是BaseExecutor里面的一个属性

public abstract class BaseExecutor implements Executor {

protected PerpetualCache localCache;

PerpetualCache类

public class PerpetualCache implements Cache {

private final String id;

private Map<Object, Object> cache = new HashMap<Object, Object>();

public PerpetualCache(String id) {
this.id = id;
}

@Override
public String getId() {
return id;
}

@Override
public int getSize() {
return cache.size();
}

@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}

@Override
public Object getObject(Object key) {
return cache.get(key);
}

二级缓存

启用二级缓存步骤:

开启cacheEnabled(默认打开)
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
需要在二级缓存的Mapper配置文件中加入
<cache></cache>
注意,二级缓存要想生效,必须要调用sqlSession.commit或close方法
public <E> List<E> 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, parameterObject, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
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);
}

注意Cache cache = ms.getCache();,这个cache是从MappedStatement中获取到的,由于MappedStatement存在全局配置中,可以多个CachingExecutor获取到,这样就会出现线程安全问题。除此之外,若不加以控制,多个事务共用一个缓存实例,会导致脏读的存在。

那么mybatis是怎么解决脏读的呢?借用了上面的tcm这个变量,也就是TransactionalCacheManager类来解决的。

TransactionalCacheManager类维护了Cache,TransactionalCache的关系,真正的数据还是交由TransactionalCache处理的。

结构如图:

public class TransactionalCacheManager {

private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}

public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}

public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}

public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}

public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}

private TransactionalCache getTransactionalCache(Cache cache) {
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}

}

接下来看一下TransactionalCache的代码

public class TransactionalCache implements Cache {

private static final Log log = LogFactory.getLog(TransactionalCache.class);

// 真正的缓存对象
private final Cache delegate;
private boolean clearOnCommit;
//在事务被提交前,所有从数据库中查询的结果将缓存在此集合中
private final Map<Object, Object> entriesToAddOnCommit;
//在事务被提交前,当缓存未命中时,CacheKey 将会被存储在此集合中
private final Set<Object> entriesMissedInCache;

public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<Object, Object>();
this.entriesMissedInCache = new HashSet<Object>();
}

@Override
public String getId() {
return delegate.getId();
}

@Override
public int getSize() {
return delegate.getSize();
}

@Override
public Object getObject(Object key) {
// issue #116
//获取缓存的时候从delegate里获取的
Object object = delegate.getObject(key);
if (object == null) {
//缓存未命中,将key存入entriesMissedInCache.
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}

@Override
public ReadWriteLock getReadWriteLock() {
return null;
}

@Override
public void putObject(Object key, Object object) {
//put的时候只是将数据库的数据放入到了entriesToAddOnCommit
entriesToAddOnCommit.put(key, object);
}

@Override
public Object removeObject(Object key) {
return null;
}

@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}

public void commit() {
if (clearOnCommit) {
delegate.clear();
}
//刷新未缓存的结果到delegate中去
flushPendingEntries();
reset();
}

public void rollback() {
unlockMissedEntries();
reset();
}

private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}

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);
}
}
}

private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
try {
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
+ "Consider upgrading your cache adapter to the latest version.  Cause: " + e);
}
}
}

}

我们存储二级缓存的时候是放入到TransactionalCache.entriesToAddOnCommit这个map中,但是每次查询的时候是从delegate查询的,所以这个二级缓存查询数据库后,缓存是没有立刻生效的。只有当执行了sqlSession的commit或close方法后,它会调用到tcm的commit,在调用到transactionlCache的commit,刷新缓存到delegate了。

总结:

二级缓存的设计上,大量运用了装饰器模式,如SynchronizedCache、LoggingCache。 二级缓存实现了Sqlsession之间的缓存数据共享,属于namespace级别 二级缓存的实现由CachingExecutor和一个事务型预缓存TransactionlCache完成。