基础支持层位于MyBatis整体架构的最底层,支撑着MyBatis的核心处理层,是整个框架的基石。基础支持层中封装了多个较为通用的、独立的模块。不仅仅为MyBatis提供基础支撑,也可以在合适的场景中直接复用。
上篇文章我们给大家聊了下binding模块,本篇文章我们重点来聊下缓存(Cache)模块。
缓存模块
MyBatis作为一个强大的持久层框架,缓存是其必不可少的功能之一,Mybatis中的缓存分为一级缓存和二级缓存。但本质上是一样的,都是使用Cache接口实现的。缓存位于 org.apache.ibatis.cache包下。
通过结构我们能够发现Cache其实使用到了装饰器模式来实现缓存的处理。首先大家需要先回顾下装饰器模式的相关内容哦。我们先来看看Cache中的基础类的API
// 煎饼加鸡蛋加香肠
“装饰者模式(Decorator Pattern)是指在不改变原有对象的基础之上,将功能附加到对象上,提供了比继承更有弹性的替代方案(扩展原有对象的功能)。”
1 Cache接口
Cache接口是缓存模块中最核心的接口,它定义了所有缓存的基本行为,Cache接口的定义如下:
public interface Cache {/*** 缓存对象的 ID* @return The identifier of this cache*/String getId();/*** 向缓存中添加数据,一般情况下 key是CacheKey value是查询结果* @param key Can be any object but usually it is a {@link CacheKey}* @param value The result of a select.*/void putObject(Object key, Object value);/*** 根据指定的key,在缓存中查找对应的结果对象* @param key The key* @return The object stored in the cache.*/Object getObject(Object key);/*** As of 3.3.0 this method is only called during a rollback* for any previous value that was missing in the cache.* This lets any blocking cache to release the lock that* may have previously put on the key.* A blocking cache puts a lock when a value is null* and releases it when the value is back again.* This way other threads will wait for the value to be* available instead of hitting the database.* 删除key对应的缓存数据** @param key The key* @return Not used*/Object removeObject(Object key);/*** Clears this cache instance.* 清空缓存*/void clear();/*** Optional. This method is not called by the core.* 缓存的个数。* @return The number of elements stored in the cache (not its capacity).*/int getSize();/*** Optional. As of 3.2.6 this method is no longer called by the core.* <p>* Any locking needed by the cache must be provided internally by the cache provider.* 获取读写锁* @return A ReadWriteLock*/default ReadWriteLock getReadWriteLock() {return null;}}
Cache接口的实现类很多,但是大部分都是装饰器,只有PerpetualCache提供了Cache接口的基本实现。
2 PerpetualCache
PerpetualCache在缓存模块中扮演了ConcreteComponent的角色,其实现比较简单,底层使用HashMap记录缓存项,具体的实现如下:
/*** 在装饰器模式用 用来被装饰的对象* 缓存中的 基本缓存处理的实现* 其实就是一个 HashMap 的基本操作* @author Clinton Begin*/public class PerpetualCache implements Cache {private final String id; // Cache 对象的唯一标识// 用于记录缓存的Map对象private final Map<Object, Object> cache = new HashMap<>();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);}@Overridepublic Object removeObject(Object key) {return cache.remove(key);}@Overridepublic void clear() {cache.clear();}@Overridepublic boolean equals(Object o) {if (getId() == null) {throw new CacheException("Cache instances require an ID.");}if (this == o) {return true;}if (!(o instanceof Cache)) {return false;}Cache otherCache = (Cache) o;// 只关心IDreturn getId().equals(otherCache.getId());}@Overridepublic int hashCode() {if (getId() == null) {throw new CacheException("Cache instances require an ID.");}// 只关心IDreturn getId().hashCode();}}
然后我们可以来看看cache.decorators包下提供的装饰器。他们都实现了Cache接口。这些装饰器都在PerpetualCache的基础上提供了一些额外的功能,通过多个组合实现一些特殊的需求。
3 BlockingCache
通过名称我们能看出来是一个阻塞同步的缓存,它保证只有一个线程到缓存中查找指定的key对应的数据。
public class BlockingCache implements Cache {private long timeout; // 阻塞超时时长private final Cache delegate; // 被装饰的底层 Cache 对象// 每个key 都有对象的 ReentrantLock 对象private final ConcurrentHashMap<Object, ReentrantLock> locks;public BlockingCache(Cache delegate) {// 被装饰的 Cache 对象this.delegate = delegate;this.locks = new ConcurrentHashMap<>();}@Overridepublic String getId() {return delegate.getId();}@Overridepublic int getSize() {return delegate.getSize();}@Overridepublic void putObject(Object key, Object value) {try {// 执行 被装饰的 Cache 中的方法delegate.putObject(key, value);} finally {// 释放锁releaseLock(key);}}@Overridepublic Object getObject(Object key) {acquireLock(key); // 获取锁Object value = delegate.getObject(key); // 获取缓存数据if (value != null) {// 有数据就释放掉锁,否则继续持有锁releaseLock(key);}return value;}@Overridepublic Object removeObject(Object key) {// despite of its name, this method is called only to release locksreleaseLock(key);return null;}@Overridepublic void clear() {delegate.clear();}private ReentrantLock getLockForKey(Object key) {return puteIfAbsent(key, k -> new ReentrantLock());}private void acquireLock(Object key) {Lock lock = getLockForKey(key);if (timeout > 0) {try {boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);if (!acquired) {throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());}} catch (InterruptedException e) {throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);}} else {lock.lock();}}private void releaseLock(Object key) {ReentrantLock lock = locks.get(key);if (lock.isHeldByCurrentThread()) {lock.unlock();}}public long getTimeout() {return timeout;}public void setTimeout(long timeout) {this.timeout = timeout;}}
通过源码我们能够发现,BlockingCache本质上就是在我们操作缓存数据的前后通过 ReentrantLock对象来实现了加锁和解锁操作。其他的具体实现类,大家可以自行查阅
4 缓存的应用
4.1 缓存对应的初始化
在Configuration初始化的时候会为我们的各种Cache实现注册对应的别名
在解析settings标签的时候,设置的默认值有如下
cacheEnabled默认为true,localCacheScope默认为 SESSION
在解析映射文件的时候会解析我们相关的cache标签
然后解析映射文件的cache标签后会在Configuration对象中添加对应的数据在
private void cacheElement(XNode context) {// 只有 cache 标签不为空才解析if (context != null) {String type = context.getStringAttribute("type", "PERPETUAL");Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);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.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);}}
继续
然后我们可以发现 如果存储 cache 标签,那么对应的 Cache对象会被保存在 currentCache 属性中。
进而在 Cache 对象 保存在了 MapperStatement 对象的 cache 属性中。
然后我们再看看openSession的时候又做了哪些操作,在创建对应的执行器的时候会有缓存的操作
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;Executor executor;if (ExecutorType.BATCH == executorType) {executor = new BatchExecutor(this, transaction);} else if (ExecutorType.REUSE == executorType) {executor = new ReuseExecutor(this, transaction);} else {// 默认 SimpleExecutorexecutor = new SimpleExecutor(this, transaction);}// 二级缓存开关,settings 中的 cacheEnabled 默认是 trueif (cacheEnabled) {executor = new CachingExecutor(executor);}// 植入插件的逻辑,至此,四大对象已经全部拦截完毕executor = (Executor) interceptorChain.pluginAll(executor);return executor;}
也就是如果 cacheEnabled 为 true 就会通过 CachingExecutor 来装饰executor 对象,然后就是在执行SQL操作的时候会涉及到缓存的具体使用。这个就分为一级缓存和二级缓存,这个我们来分别介绍
4.2 一级缓存
一级缓存也叫本地缓存(Local Cache),MyBatis的一级缓存是在会话(SqlSession)层面进行缓存的。MyBatis的一级缓存是默认开启的,不需要任何的配置(如果要关闭,localCacheScope设置为STATEMENT)。在BaseExecutor对象的query方法中有关闭一级缓存的逻辑
然后我们需要考虑下在一级缓存中的 PerpetualCache 对象在哪创建的,因为一级缓存是Session级别的缓存,肯定需要在Session范围内创建,其实PerpetualCache的实例化是在BaseExecutor的构造方法中创建的
protected BaseExecutor(Configuration configuration, Transaction transaction) {this.transaction = transaction;this.deferredLoads = new ConcurrentLinkedQueue<>();this.localCache = new PerpetualCache("LocalCache");this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");this.closed = false;this.configuration = configuration;this.wrapper = this;}
一级缓存的具体实现也是在BaseExecutor的query方法中来实现的
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {// 异常体系之 ErrorContextErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());if (closed) {throw new ExecutorException("Executor was closed.");}if (queryStack == 0 && ms.isFlushCacheRequired()) {// flushCache="true"时,即使是查询,也清空一级缓存clearLocalCache();}List<E> list;try {// 防止递归查询重复处理缓存queryStack++;// 查询一级缓存// ResultHandler 和 ResultSetHandler的区别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;}
一级缓存的验证:
同一个Session中的多个相同操作
@Testpublic void test1() throws Exception{// 1.获取配置文件InputStream in = Resources.getResourceAsStream("mybatis-config.xml");// 2.加载解析配置文件并获取SqlSessionFactory对象SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);// 3.根据SqlSessionFactory对象获取SqlSession对象SqlSession sqlSession = factory.openSession();// 4.通过SqlSession中提供的 API方法来操作数据库List<User> list = sqlSession.selectList("com.gupaoedu.mapper.UserMapper.selectUserList");System.out.println(list.size());// 一级缓存测试System.out.println("---------");list = sqlSession.selectList("com.gupaoedu.mapper.UserMapper.selectUserList");System.out.println(list.size());// 5.关闭会话sqlSession.close();}
输出日志
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@477b4cdf]==> Preparing: select * from t_user ==> Parameters: <== Columns: id, user_name, real_name, password, age, d_id<== Row: 1, zhangsan, 张三, 123456, 18, null<== Row: 2, lisi, 李四, 11111, 19, null<== Row: 3, wangwu, 王五, 111, 22, 1001<== Row: 4, wangwu, 王五, 111, 22, 1001<== Row: 5, wangwu, 王五, 111, 22, 1001<== Row: 6, wangwu, 王五, 111, 22, 1001<== Row: 7, wangwu, 王五, 111, 22, 1001<== Row: 8, aaa, bbbb, null, null, null<== Row: 9, aaa, bbbb, null, null, null<== Row: 10, aaa, bbbb, null, null, null<== Row: 11, aaa, bbbb, null, null, null<== Row: 12, aaa, bbbb, null, null, null<== Row: 666, hibernate, 持久层框架, null, null, null<==Total: 1313---------13
可以看到第二次查询没有经过数据库操作
不同Session的相同操作
@Testpublic void test2() throws Exception{// 1.获取配置文件InputStream in = Resources.getResourceAsStream("mybatis-config.xml");// 2.加载解析配置文件并获取SqlSessionFactory对象SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);// 3.根据SqlSessionFactory对象获取SqlSession对象SqlSession sqlSession = factory.openSession();// 4.通过SqlSession中提供的 API方法来操作数据库List<User> list = sqlSession.selectList("com.gupaoedu.mapper.UserMapper.selectUserList");System.out.println(list.size());sqlSession.close();sqlSession = factory.openSession();// 一级缓存测试System.out.println("---------");list = sqlSession.selectList("com.gupaoedu.mapper.UserMapper.selectUserList");System.out.println(list.size());// 5.关闭会话sqlSession.close();}
输出结果
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@477b4cdf]==> Preparing: select * from t_user ==> Parameters: <== Columns: id, user_name, real_name, password, age, d_id<== Row: 1, zhangsan, 张三, 123456, 18, null<== Row: 2, lisi, 李四, 11111, 19, null<== Row: 3, wangwu, 王五, 111, 22, 1001<== Row: 4, wangwu, 王五, 111, 22, 1001<== Row: 5, wangwu, 王五, 111, 22, 1001<== Row: 6, wangwu, 王五, 111, 22, 1001<== Row: 7, wangwu, 王五, 111, 22, 1001<== Row: 8, aaa, bbbb, null, null, null<== Row: 9, aaa, bbbb, null, null, null<== Row: 10, aaa, bbbb, null, null, null<== Row: 11, aaa, bbbb, null, null, null<== Row: 12, aaa, bbbb, null, null, null<== Row: 666, hibernate, 持久层框架, null, null, null<==Total: 1313Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@477b4cdf]Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@477b4cdf]Returned connection 1199262943 to pool.---------Opening JDBC ConnectionChecked out connection 1199262943 from pool.Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@477b4cdf]==> Preparing: select * from t_user ==> Parameters: <== Columns: id, user_name, real_name, password, age, d_id<== Row: 1, zhangsan, 张三, 123456, 18, null<== Row: 2, lisi, 李四, 11111, 19, null<== Row: 3, wangwu, 王五, 111, 22, 1001<== Row: 4, wangwu, 王五, 111, 22, 1001<== Row: 5, wangwu, 王五, 111, 22, 1001<== Row: 6, wangwu, 王五, 111, 22, 1001<== Row: 7, wangwu, 王五, 111, 22, 1001<== Row: 8, aaa, bbbb, null, null, null<== Row: 9, aaa, bbbb, null, null, null<== Row: 10, aaa, bbbb, null, null, null<== Row: 11, aaa, bbbb, null, null, null<== Row: 12, aaa, bbbb, null, null, null<== Row: 666, hibernate, 持久层框架, null, null, null<==Total: 1313Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@477b4cdf]Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@477b4cdf]Returned connection 1199262943 to pool.
通过输出我们能够发现,不同的Session中的相同操作,一级缓存是没有起作用的。
4.3 二级缓存
二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace级别的,可以被多个SqlSession共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。
二级缓存的设置,首先是settings中的cacheEnabled要设置为true,当然默认的就是为true,这个步骤决定了在创建Executor对象的时候是否通过CachingExecutor来装饰。
那么设置了cacheEnabled标签为true是否就意味着 二级缓存是否一定可用呢?当然不是,我们还需要在 对应的映射文件中添加 cache 标签才行。
<!-- 声明这个namespace使用二级缓存 --><cache type="org.apache.ibatis.cache.impl.PerpetualCache"size="1024" <!—最多缓存对象个数,默认1024-->eviction="LRU" <!—回收策略-->flushInterval="120000" <!—自动刷新时间 ms,未配置时只有调用时刷新-->readOnly="false"/> <!—默认是false(安全),改为true可读写时,对象必须支持序列化 -->
cache属性详解:
再来看下cache标签在源码中的体现,创建cacheKey
@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {// 获取SQLBoundSql boundSql = ms.getBoundSql(parameterObject);// 创建CacheKey:什么样的SQL是同一条SQL? >>CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
createCacheKey自行进去查看
而这看到的和我们前面在缓存初始化时看到的 cache 标签解析操作是对应上的。所以我们要开启二级缓存两个条件都要满足。
这样的设置表示当前的映射文件中的相关查询操作都会触发二级缓存,但如果某些个别方法我们不希望走二级缓存怎么办呢?我们可以在标签中添加一个 useCache=false 来实现的设置不使用二级缓存
还有就是当我们执行的对应的DML操作,在MyBatis中会清空对应的二级缓存和一级缓存。
private void flushCacheIfRequired(MappedStatement ms) {Cache cache = ms.getCache();// 增删改查的标签上有属性:flushCache="true" (select语句默认是false)// 一级二级缓存都会被清理if (cache != null && ms.isFlushCacheRequired()) {tcm.clear(cache);}}
在解析映射文件的时候DML操作flushCacheRequired为true
4.4 第三方缓存
在实际开发的时候我们一般也很少使用MyBatis自带的二级缓存,这时我们会使用第三方的缓存工具Ehcache获取Redis来实现,那么他们是如何来实现的呢?
/mybatis/redis-cache
添加依赖
<dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-redis</artifactId><version>1.0.0-beta2</version></dependency>
然后加上Cache标签的配置
<cache type="org.mybatis.caches.redis.RedisCache"eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
然后添加redis的属性文件
host=192.168.100.120port=6379connectionTimeout=5000soTimeout=5000database=0
测试效果
数据存储到了Redis中
然后大家也可以自行分析下第三方的Cache是如何替换掉PerpetualCache的,因为PerpetualCache是基于HashMap处理的,而RedisCache是基于Redis来存储缓存数据的。
提示
缓存模块我们就介绍到此。 然后大家可以基于我们上面所介绍的基础支持层,再系统的来梳理下核心处理层的流程
~~ 好了,缓存模块的内容就给大家介绍到这里,如果对你有帮助,欢迎点赞关注加收藏
下篇我们介绍 MyBatis中的插件机制,敬请期待 V_V