AI智能
改变未来

深入了解Mybatis架构设计


架构设计

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

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

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

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

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语句以及相应的参数信息

总体流程:

  1. 加载配置并初始化

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

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

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

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

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

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 #601deferredLoads.clear();if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {// issue #482clearLocalCache();}}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;}@Overridepublic String getId() {return id;}@Overridepublic int getSize() {return cache.size();}@Overridepublic void putObject(Object key, Object value) {cache.put(key, value);}@Overridepublic Object getObject(Object key) {return cache.get(key);}

二级缓存

启用二级缓存步骤:

  1. 开启cacheEnabled(默认打开)
<settings><setting name="cacheEnabled" value="true"/></settings>
  1. 需要在二级缓存的Mapper配置文件中加入
<cache></cache>
  1. 注意,二级缓存要想生效,必须要调用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>();}@Overridepublic String getId() {return delegate.getId();}@Overridepublic int getSize() {return delegate.getSize();}@Overridepublic Object getObject(Object key) {// issue #116//获取缓存的时候从delegate里获取的Object object = delegate.getObject(key);if (object == null) {//缓存未命中,将key存入entriesMissedInCache.entriesMissedInCache.add(key);}// issue #146if (clearOnCommit) {return null;} else {return object;}}@Overridepublic ReadWriteLock getReadWriteLock() {return null;}@Overridepublic void putObject(Object key, Object object) {//put的时候只是将数据库的数据放入到了entriesToAddOnCommitentriesToAddOnCommit.put(key, object);}@Overridepublic Object removeObject(Object key) {return null;}@Overridepublic 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完成。
赞(0) 打赏
未经允许不得转载:爱站程序员基地 » 深入了解Mybatis架构设计