目录
一、简介
二、缓存类介绍
2.1 PerpetualCache
2.2 LruCache
2.3 BlockingCache
三、CacheKey
3.1 总结:二级缓存命中条件
四、一级缓存
五、二级缓存
5.1 二级缓存如何组装这些装饰器?(解析SQL映射文件中的标签,初始化二级缓存)
5.1.1 SynchronizedCache:线程同步缓存区
5.1.2 LoggingCache:统计命中率以及打印日志
5.1.3 ScheduledCache:过期清理缓存区
5.1.4 防溢出缓存区
5.1.4.1 LruCache:(最近最少使用)防溢出缓存区
5.1.4.2 FifoCache(先进先出)防溢出缓存区
5.1.5 SerializedCache
5.1.6 PerpetualCache
5.2 查询二级缓存
5.2.1 MyBatis如何解决“脏读”问题?
5.2.1.1 TransactionalCacheManager和TransactionalCache
5.2.1.2 源码分析
5.2.1.3 测试代码
5.2.1.3 小结
5.2.2 整合第三方缓存
1、整合Redis
2、整合ehcache
六、总结
6.1 二级缓存执行流程
一、简介
在 Web 应用中,缓存是必不可少的组件。通常我们都会用 Redis 或 memcached 等缓存中间件,拦截大量奔向数据库的请求,减轻数据库压力。作为一个重要的组件,MyBatis 自然也在内部提供了相应的支持。通过在框架层面增加缓存功能,可减轻数据库的压力,同时又可以提升查询速度,可谓一举两得。MyBatis 缓存结构由一级缓存和二级缓存构成,这两级缓存均是使用 Cache 接口的实现类。因此,在接下里的章节中,我将首先会向大家介绍 Cache 几种实现类的源码,然后再分析一级和二级缓存的实现。下面先来分析 Cache 及其实现类。
二、缓存类介绍
在 MyBatis 中,Cache 是缓存接口,定义了一些基本的缓存操作,所有缓存类都应该实现该接口。MyBatis 内部提供了丰富的缓存实现类,比如具有基本缓存功能的 PerpetualCache,具有 LRU 策略的缓存 LruCache,以及可保证线程安全的缓存 SynchronizedCache 和具备阻塞功能的缓存 BlockingCache 等。除此之外,还有很多缓存实现类,这里就不一一列举了。需要特别说明的是,MyBatis 在实现缓存模块的过程中,使用了装饰模式。在以上几种缓存实现类中,PerpetualCache 相当于装饰模式中的 ConcreteComponent(具体的对象,可以给这个对象添加一下职责)。LruCache、SynchronizedCache 和 BlockingCache 等相当于装饰模式中的 ConcreteDecorator(具体的装饰对象,起到动态添加职责的功能)。它们的关系如下:
以上对 Cache 接口的实现类进行了简单的介绍,接下来,我们一起深入到源码中,看看这些缓存类的实现。
2.1 PerpetualCache
PerpetualCache 是一个具有基本功能的缓存类,内部使用了 HashMap 实现缓存功能。它的源码如下:
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;}public String getId() {return id;}public int getSize() {return cache.size();}public void putObject(Object key, Object value) {// 存储键值对到 HashMapcache.put(key, value);}public Object getObject(Object key) {// 查找缓存项return cache.get(key);}public Object removeObject(Object key) {// 移除缓存项return cache.remove(key);}public void clear() {cache.clear();}// 省略部分代码
}
上面是 PerpetualCache 的全部代码,很简单。接下来,我们通过装饰类对该类进行装饰,使其功能变的丰富起来。
2.2 LruCache
LruCache,顾名思义,是一种具有 LRU 策略的缓存实现类。除此之外,MyBatis 还提供了具有 FIFO 策略的缓存 FifoCache。不过并未提供 LFU 缓存,如果大家有兴趣,可以自行拓展。接下来,我们来看一下 LruCache 的实现。
public class LruCache implements Cache {private final Cache delegate;private Map<Object, Object> keyMap;private Object eldestKey;public LruCache(Cache delegate) {this.delegate = delegate;setSize(1024);}public int getSize() {return delegate.getSize();}public void setSize(final int size) {/** 初始化 keyMap,注意,keyMap 的类型继承自 LinkedHashMap,* 并覆盖了 removeEldestEntry 方法*/keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {private static final long serialVersionUID = 4267176411845948333L;// 覆盖 LinkedHashMap 的 removeEldestEntry 方法protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {boolean tooBig = size() > size;if (tooBig) {// 获取将要被移除缓存项的键值eldestKey = eldest.getKey();}return tooBig;}};}public void putObject(Object key, Object value) {// 存储缓存项delegate.putObject(key, value);cycleKeyList(key);}public Object getObject(Object key) {// 刷新 key 在 keyMap 中的位置keyMap.get(key);// 从被装饰类中获取相应缓存项return delegate.getObject(key);}public Object removeObject(Object key) {// 从被装饰类中移除相应的缓存项return delegate.removeObject(key);}public void clear() {delegate.clear();keyMap.clear();}private void cycleKeyList(Object key) {// 存储 key 到 keyMap 中keyMap.put(key, key);if (eldestKey != null) {// 从被装饰类中移除相应的缓存项delegate.removeObject(eldestKey);eldestKey = null;}}// 省略部分代码
}
如上,LruCache 的 keyMap 属性是实现 LRU 策略的关键,该属性类型继承自 LinkedHashMap,并覆盖了 removeEldestEntry 方法。LinkedHashMap 可保持键值对的插入顺序,当插入一个新的键值对时,LinkedHashMap 内部的 tail 节点会指向最新插入的节点。head 节点则指向第一个被插入的键值对,也就是最久未被访问的那个键值对。默认情况下,LinkedHashMap 仅维护键值对的插入顺序。若要基于 LinkedHashMap 实现 LRU 缓存,还需通过构造方法将 LinkedHashMap 的 accessOrder 属性设为 true,此时 LinkedHashMap 会维护键值对的访问顺序。比如,上面代码中 getObject 方法中执行了这样一句代码 keyMap.get(key),目的是刷新 key 对应的键值对在 LinkedHashMap 的位置。LinkedHashMap 会将 key 对应的键值对移动到链表的尾部,尾部节点表示最久刚被访问过或者插入的节点。除了需将 accessOrder 设为 true,还需覆盖 removeEldestEntry 方法。LinkedHashMap 在插入新的键值对时会调用该方法,以决定是否在插入新的键值对后,移除老的键值对。在上面的代码中,当被装饰类的容量超出了 keyMap 的所规定的容量(由构造方法传入)后,keyMap 会移除最长时间未被访问的键,并保存到 eldestKey 中,然后由 cycleKeyList 方法将 eldestKey 传给被装饰类的 removeObject 方法,移除相应的缓存项目。
上面讲了 LinkedHashMap 是如何实现 LRU 特性的,这个是理解 LruCache 的源码的关键所在,所以大家务必搞懂。
2.3 BlockingCache
BlockingCache 实现了阻塞特性,该特性是基于 Java 重入锁实现的。同一时刻下,BlockingCache 仅允许一个线程访问指定 key 的缓存项,其他线程将会被阻塞住。下面我们来看一下 BlockingCache 的源码。
public class BlockingCache implements Cache {private long timeout;private final Cache delegate;private final ConcurrentHashMap<Object, ReentrantLock> locks;public BlockingCache(Cache delegate) {this.delegate = delegate;this.locks = new ConcurrentHashMap<Object, ReentrantLock>();}public void putObject(Object key, Object value) {try {// 存储缓存项delegate.putObject(key, value);} finally {// 释放锁releaseLock(key);}}public Object getObject(Object key) {// 请求锁acquireLock(key);Object value = delegate.getObject(key);// 若缓存命中,则释放锁。需要注意的是,未命中则不释放锁if (value != null) {// 释放锁releaseLock(key);}return value;}public Object removeObject(Object key) {// 释放锁releaseLock(key);return null;}private ReentrantLock getLockForKey(Object key) {ReentrantLock lock = new ReentrantLock();// 存储 <key, Lock> 键值对到 locks 中ReentrantLock previous = locks.putIfAbsent(key, lock);return previous == null ? lock : previous;}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("...");}} catch (InterruptedException e) {throw new CacheException("...");}} else {// 加锁lock.lock();}}private void releaseLock(Object key) {// 获取与当前 key 对应的锁ReentrantLock lock = locks.get(key);if (lock.isHeldByCurrentThread()) {// 释放锁lock.unlock();}}// 省略部分代码
}
如上,查询缓存时,getObject 方法会先获取与 key 对应的锁,并加锁。若缓存命中,getObject 方法会释放锁,否则将一直锁定。getObject 方法若返回 null,表示缓存未命中。此时 MyBatis 会进行数据库查询,并调用 putObject 方法存储查询结果。同时,putObject 方法会将指定 key 对应的锁进行解锁,这样被阻塞的线程即可恢复运行。
上面的描述有点啰嗦,倒是 BlockingCache 类的注释说到比较简单明了。这里引用一下:
It sets a lock over a cache key when the element is not found in cache. This way, other threads will wait until this element is filled instead of hitting the database. |
这段话的意思是,当指定 key 对应元素不存在于缓存中时,BlockingCache 会根据 lock 进行加锁。此时,其他线程将会进入等待状态,直到与 key 对应的元素被填充到缓存中。而不是让所有线程都去访问数据库。
在上面代码中,removeObject 方法的逻辑很奇怪,仅调用了 releaseLock 方法释放锁,却没有调用被装饰类的 removeObject 方法移除指定缓存项。这样做是为什么呢?大家可以先思考,答案将在分析二级缓存的相关逻辑时分析。
三、CacheKey
在 MyBatis 中,引入缓存的目的是为提高查询效率,降低数据库压力。既然 MyBatis 引入了缓存,那么大家思考过缓存中的 key 和 value 的值分别是什么吗?大家可能很容易能回答出 value 的内容,不就是 SQL 的查询结果吗。那 key 是什么呢?是字符串,还是其他什么对象?如果是字符串的话,那么大家首先能想到的是用 SQL 语句作为 key。但这是不对的,比如:
SELECT * FROM author where id > ?
id > 1 和 id > 10 查出来的结果可能是不同的,所以我们不能简单的使用 SQL 语句作为 key。从这里可以看出来,运行时参数将会影响查询结果,因此我们的 key 应该涵盖运行时参数。除此之外呢,如果进行分页查询也会导致查询结果不同,因此 key 也应该涵盖分页参数。综上,我们不能使用简单的 SQL 语句作为 key。应该考虑使用一种复合对象,能涵盖可影响查询结果的因子。在 MyBatis 中,这种复合对象就是 CacheKey。下面来看一下它的定义:
public class CacheKey implements Cloneable, Serializable {private static final int DEFAULT_MULTIPLYER = 37;private static final int DEFAULT_HASHCODE = 17;// 乘子,默认为37private final int multiplier;// CacheKey 的 hashCode,综合了各种影响因子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>();}// 省略其他方法
}
如上,除了 multiplier 是恒定不变的 ,其他变量将在更新操作中被修改。下面看一下更新操作的代码:
/** 每当执行更新操作时,表示有新的影响因子参与计算,这个方法就用于更新key的计算结果 */
public void update(Object object) {// 如果不是空,则计算object对象的HashCodeint baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);// 自增 countcount++;// 计算校验和checksum += baseHashCode;// 更新 baseHashCodebaseHashCode *= count;// 计算 hashCodehashcode = multiplier * hashcode + baseHashCode;// 保存影响因子updateList.add(object);
}
当不断有新的影响因子参与计算时,hashcode 和 checksum 将会变得愈发复杂和随机。这样可降低冲突率,使 CacheKey 可在缓存中更均匀的分布。CacheKey 最终要作为键存入 HashMap,因此它需要覆盖 equals 和 hashCode 方法。下面我们来看一下这两个方法的实现:
public boolean equals(Object object) {// 检测是否为同一个对象if (this == object) {return true;}// 检测 object 是否为 CacheKeyif (!(object instanceof CacheKey)) {return false;}final CacheKey cacheKey = (CacheKey) object;// 检测 hashCode 是否相等if (hashcode != cacheKey.hashcode) {return false;}// 检测校验和是否相同if (checksum != cacheKey.checksum) {return false;}// 检测 coutn 是否相同if (count != cacheKey.count) {return false;}// 如果上面的检测都通过了,下面分别对每个影响因子进行比较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;
}public int hashCode() {// 返回 hashcode 变量return hashcode;
}
equals 方法的检测逻辑比较严格,对 CacheKey 中多个成员变量进行了检测,以保证两者相等。hashCode 方法比较简单,返回 hashcode 变量即可。
3.1 总结:二级缓存命中条件
- 会话提交后
- sql语句、参数相同
- 相同的statementID
- RowBounds相同
注意:设置为自动提交事务不会命中二级缓存。
关于 CacheKey 就先分析到这,CacheKey 在一二级缓存中会被用到,接下来还会看到它的身影。
四、一级缓存
一级缓存是SqlSession级别的缓存,每个HTTP请求都会创建一个新的SqlSession即DefaultSqlSession,一级缓存默认开启,可通过localCacheScope=STATEMENT关闭一级缓存,参数不一致会导致缓存失效,在查询过程中发生了数据的修改也会导致缓存失效。
在进行数据库查询之前,MyBatis 首先会检查缓存中是否有相应的记录,若有的话直接返回即可。一级缓存是数据库的最后一道防护,若一级缓存未命中,查询请求将落到数据库上。一级缓存是在 BaseExecutor 被初始化的,下面我们来看一下相关的初始化逻辑:
public abstract class BaseExecutor implements Executor {// 基本缓存实现类protected PerpetualCache localCache;// 省略其他字段protected BaseExecutor(Configuration configuration, Transaction transaction) {// 创建基本缓存实例this.localCache = new PerpetualCache("LocalCache");// 省略其他字段初始化方法}
}
如上,一级缓存的类型为 PerpetualCache,没有被其他缓存类装饰过。一级缓存所存储的查询结果会在 MyBatis 执行更新操作(INSERT/UPDATE/DELETE),以及提交和回滚事务时被清空。下面我们来看一下查询一级缓存的逻辑。
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameter);// 创建 CacheKeyCacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);// 执行查询逻辑return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {// 省略部分代码List<E> list;try {queryStack++;// 查询一级缓存,获取缓存中存储的查询结果。如果命中了缓存,就会将list直接返回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--;}// 省略部分代码return list;
}
如上,在访问一级缓存之前,MyBatis 首先会调用 createCacheKey 方法创建 CacheKey。下面我们来看一下 createCacheKey 方法的逻辑:
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {if (closed) {throw new ExecutorException("Executor was closed.");}// 创建 CacheKey 对象CacheKey cacheKey = new CacheKey();// 将 MappedStatement 的 id 作为影响因子进行计算cacheKey.update(ms.getId());// RowBounds 用于分页查询,下面将它的两个字段作为影响因子进行计算cacheKey.update(rowBounds.getOffset());cacheKey.update(rowBounds.getLimit());// 获取 sql 语句,并进行计算cacheKey.update(boundSql.getSql());// 获取运行时参数集合List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();// 遍历运行时参数for (ParameterMapping parameterMapping : parameterMappings) {if (parameterMapping.getMode() != ParameterMode.OUT) {// 运行时参数Object value; // 当前大段代码用于获取 SQL 中的占位符 #{xxx} 对应的运行时参数,// 前文有类似分析,这里忽略了String propertyName = parameterMapping.getProperty();if (boundSql.hasAdditionalParameter(propertyName)) {value = boundSql.getAdditionalParameter(propertyName);} else if (parameterObject == null) {value = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {value = parameterObject;} else {MetaObject metaObject = configuration.newMetaObject(parameterObject);value = metaObject.getValue(propertyName);}// 让运行时参数参与计算cacheKey.update(value);}}if (configuration.getEnvironment() != null) {// 获取 Environment id 遍历,并让其参与计算cacheKey.update(configuration.getEnvironment().getId());}return cacheKey;
}
如上,在计算 CacheKey 的过程中,有很多影响因子参与了计算。比如 MappedStatement 的 id 字段,SQL 语句,分页参数,运行时变量,Environment 的 id 字段等。通过让这些影响因子参与计算,可以很好的区分不同查询请求。所以,我们可以简单的把 CacheKey 看做是一个查询请求的 id。有了 CacheKey,我们就可以使用它读写缓存了。在上面代码中,若一级缓存未命中,BaseExecutor 会调用 queryFromDatabase 查询数据库,并将查询结果写入缓存中。下面看一下 queryFromDatabase 的逻辑:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {List<E> list;// 向缓存中存储一个占位符localCache.putObject(key, EXECUTION_PLACEHOLDER);try {// 查询数据库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;
}
到此,关于一级缓存相关的逻辑就差不多分析完了。一级缓存的逻辑比较简单,大家可以简单过一遍。接下来分析二级缓存。
五、二级缓存
二级缓存也称作是应用级缓存,与一级缓存不同的是它的作用范围是整个应用,而且可以跨线程使用,它是一个全局作用域缓存,所以二级缓存有更高的命中率,适合缓存一些修改比较少的数据。
二级缓存的生命周期是整个应用,所以必须限制二级缓存的容量,在这里MyBatis使用的是溢出淘汰机制。而一级缓存是会话级的生命周期非常短暂是没有必要实现这些功能的。相比较之下,二级缓存机制更加完善。
MyBatis提供二级缓存的接口及实现,缓存实现时实体类必需实现Serializable接口,二级缓存在sqlSession关闭或提交之后才会生效。二级缓存不是默认开启的,使用二级缓存需要通过在全局配置文件中配置cacheEnabled属性为true,cacheEnabled默认为true,且需要在Mapper.xml映射文件中使用cache标签标注,实体类必须要实现Serializable接口。
1、mybatis-config.xml
全局配置文件开启二级缓存(默认就是true,但是还需要配合在SQL映射文件中配置<cache>标签才能完全开启二级缓存):
<setting name="cacheEnabled" value="true"/>
或者使用@CacheNamespace与@Select或@SelectProvider注解配合使用
@CacheNamespace
public interface UserMapper {@Select(value = "select * from t_user")List<User> selectAllUser();
}
2、XxxMapper.xml
SQL映射文件还需要开启<cache>标签
<cache/>
如果我们想修改缓存的一些属性,可以像下面这样配置。
<cacheeviction="FIFO"flushInterval="60000"size="512"readOnly="true"/>
3、实体类实现Serializable接口
5.1 二级缓存如何组装这些装饰器?(解析SQL映射文件中的<cache>标签,初始化二级缓存)
二级缓存在结构设计上采用装饰器+责任链模式。从最内层到最外层依次是PerpetualCache、LruCache、SerializedCache、LoggingCache、SynchronizedCache。
这里就再简单复习一下:
private void cacheElement(XNode context) {if (context != null) {// 解析cache节点的type属性String type = context.getStringAttribute("type", "PERPETUAL");// 根据type的String获取class类型Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);// 获取缓存过期策略:默认是LRUString eviction = context.getStringAttribute("eviction", "LRU"); Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);// flushInterval刷新间隔,可被设置为任意正整数,设置的值应该是一个以毫秒为单位的合理时间量。默认不设置即没有刷新间隔,缓存仅在调用语句时刷新Long flushInterval = context.getLongAttribute("flushInterval");// size引用数目,可被设置为任意正整数,要注意缓存对象的大小和运行环境中可用的内存资源。默认值是1024Integer size = context.getIntAttribute("size");// readOnly只读,可被设置为true或false。只读的缓存会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。// 而可读写的缓存会通过序列化返回缓存对象的拷贝,速度上会慢一些,但更安全默认值falseboolean readWrite = !context.getBooleanAttribute("readOnly", false);boolean blocking = context.getBooleanAttribute("blocking", false);Properties props = context.getChildrenAsProperties();// 把缓存节点加入到Configuration中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).implementation(valueOrDefault(typeClass, PerpetualCache.class)).addDecorator(valueOrDefault(evictionClass, LruCache.class)).clearInterval(flushInterval).size(size).readWrite(readWrite).blocking(blocking).properties(props).build();configuration.addCache(cache);currentCache = cache;return cache;
}
这里我们先介绍一下CacheBuilder类,顾名思义这是一个缓存构建类。该类就是二级缓存的构建类,它里面定义了一些上图装饰器类型的属性,以及构建组合这些装饰器的行为。
// -☆- CacheBuilder
public Cache build() {setDefaultImplementations();Cache cache = newBaseCacheInstance(implementation, id);setCacheProperties(cache);if (PerpetualCache.class.equals(cache.getClass())) {// 通过LruCache对PerpetualCache进行装饰for (Class<? extends Cache> decorator : decorators) {cache = newCacheDecoratorInstance(decorator, cache);setCacheProperties(cache);}// 剩余集中Cache对LruCache依次装饰cache = setStandardDecorators(cache); } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {cache = new LoggingCache(cache);}return cache;
}private Cache setStandardDecorators(Cache cache) {try {MetaObject metaCache = SystemMetaObject.forObject(cache);if (size != null && metaCache.hasSetter("size")) {metaCache.setValue("size", size);}if (clearInterval != null) {// ScheduledCache:调度缓存,负责定时清空缓存cache = new ScheduledCache(cache);((ScheduledCache) cache).setClearInterval(clearInterval);}// 将LRU 装饰到Serializedif (readWrite) { // SerializedCache:缓存序列化和反序列化存储cache = new SerializedCache(cache); }cache = new LoggingCache(cache);cache = new SynchronizedCache(cache);if (blocking) {cache = new BlockingCache(cache);}return cache;} catch (Exception e) {throw new CacheException("Error building standard cache decorators. Cause: " + e, e);}
}
5.1.1 SynchronizedCache:线程同步缓存区
SynchronizedCache线程同步缓存区实现了线程同步功能,与序列化缓存区共同保证二级缓存线程安全。若blocking=false,则SynchronizedCache位于责任链的最前端;否则SynchronizedCache就位于BlockingCache的后面,BlockingCache就变成了位于责任链的最前端,从而保证了整条责任链是线程同步的。
public class SynchronizedCache implements Cache {private final Cache delegate;public SynchronizedCache(Cache delegate) {this.delegate = delegate;}@Overridepublic synchronized int getSize() {return delegate.getSize();}@Overridepublic synchronized void putObject(Object key, Object object) {delegate.putObject(key, object);}@Overridepublic synchronized Object getObject(Object key) {return delegate.getObject(key);}@Overridepublic synchronized Object removeObject(Object key) {return delegate.removeObject(key);}@Overridepublic synchronized void clear() {delegate.clear();}
}
源码分析:只是对于操作缓存的方法进行了线程同步功能
5.1.2 LoggingCache:统计命中率以及打印日志
LoggingCache统计命中率以及打印日志,若日志中出现了Cache Hit Ratio便表示命中了二级缓存
public class LoggingCache implements Cache {private final Log log;private final Cache delegate;protected int requests = 0;protected int hits = 0;public LoggingCache(Cache delegate) {this.delegate = delegate;this.log = LogFactory.getLog(getId());}@Overridepublic int getSize() {return delegate.getSize();}@Overridepublic void putObject(Object key, Object object) {delegate.putObject(key, object);}@Overridepublic Object getObject(Object key) {// 执行一次查询加一次requests++;// 查询缓存中是否已经存在final Object value = delegate.getObject(key);if (value != null) {// 命中一次加一次hits++;}// 开启debug日志if (log.isDebugEnabled()) {log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());}return value;}@Overridepublic Object removeObject(Object key) {return delegate.removeObject(key);}@Overridepublic void clear() {delegate.clear();}// 计算命中率private double getHitRatio() {// 命中次数 / 查询次数return (double) hits / (double) requests;}
}
5.1.3 ScheduledCache:过期清理缓存区
ScheduledCache过期清理缓存区,@CacheNamespace(flushInterval=100L)设置过期清理时间默认1个小时,若设置flushInterval为0代表永远不清除,操作缓存时都会进行检查缓存是否过期。
public class ScheduledCache implements Cache {private final Cache delegate;protected long clearInterval;protected long lastClear;public ScheduledCache(Cache delegate) {this.delegate = delegate;this.clearInterval = 60 * 60 * 1000; // 1 hourthis.lastClear = System.currentTimeMillis();}public void setClearInterval(long clearInterval) {this.clearInterval = clearInterval;}@Overridepublic int getSize() {clearWhenStale();return delegate.getSize();}@Overridepublic void putObject(Object key, Object object) {clearWhenStale();delegate.putObject(key, object);}@Overridepublic Object getObject(Object key) {return clearWhenStale() ? null : delegate.getObject(key);}@Overridepublic Object removeObject(Object key) {clearWhenStale();return delegate.removeObject(key);}@Overridepublic void clear() {lastClear = System.currentTimeMillis();delegate.clear();}private boolean clearWhenStale() {// 判断当前时间与上次清理时间差是否大于设置的过期清理时间if (System.currentTimeMillis() - lastClear > clearInterval) {// 一旦进行清理便是清理全部缓存clear();return true;}return false;}
}
源码分析:操作缓存时都会先检查缓存是否过期
5.1.4 防溢出缓存区
MyBatis提供了两种防疫处缓存实现类LruCache和FifoCache。不过没有提供LFU 缓存。
5.1.4.1 LruCache:(最近最少使用)防溢出缓存区
LruCache防溢出缓冲区,内部使用链表实现最近最少使用防溢出机制
public class LoggingCache implements Cache {private final Log log;private final Cache delegate;protected int requests = 0;protected int hits = 0;public LoggingCache(Cache delegate) {this.delegate = delegate;this.log = LogFactory.getLog(getId());}@Overridepublic int getSize() {return delegate.getSize();}@Overridepublic void putObject(Object key, Object object) {delegate.putObject(key, object);}// 每次访问都会遍历一次key进行重新排序,将访问元素放到链表尾部。@Overridepublic 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;}@Overridepublic Object removeObject(Object key) {return delegate.removeObject(key);}@Overridepublic void clear() {delegate.clear();}private double getHitRatio() {return (double) hits / (double) requests;}
}
5.1.4.2 FifoCache(先进先出)防溢出缓存区
public class FifoCache implements Cache {private final Cache delegate;private final Deque<Object> keyList;private int size;public FifoCache(Cache delegate) {this.delegate = delegate;this.keyList = new LinkedList();this.size = 1024;}public void putObject(Object key, Object value) {this.cycleKeyList(key);this.delegate.putObject(key, value);}public Object getObject(Object key) {return this.delegate.getObject(key);}private void cycleKeyList(Object key) {this.keyList.addLast(key);// 比较当前队列元素个数是否大于设定值if (this.keyList.size() > this.size) {// 移除队列头元素Object oldestKey = this.keyList.removeFirst();// 根据移除元素的key移除缓存区中的对应元素this.delegate.removeObject(oldestKey);}}
}
源码分析:内部使用队列存储key实现先进先出防溢出机制。
5.1.5 SerializedCache
SerializedCache序列化缓存实现类
public class SerializedCache implements Cache {private final Cache delegate;public SerializedCache(Cache delegate) {this.delegate = delegate;}@Overridepublic int getSize() {return delegate.getSize();}@Overridepublic void putObject(Object key, Object object) {if (object == null || object instanceof Serializable) {delegate.putObject(key, serialize((Serializable) object));} else {throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);}}@Overridepublic Object getObject(Object key) {Object object = delegate.getObject(key);return object == null ? null : deserialize((byte[]) object);}@Overridepublic Object removeObject(Object key) {return delegate.removeObject(key);}@Overridepublic void clear() {delegate.clear();}private byte[] serialize(Serializable value) {try (ByteArrayOutputStream bos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(bos)) {oos.writeObject(value);oos.flush();return bos.toByteArray();} catch (Exception e) {throw new CacheException("Error serializing object. Cause: " + e, e);}}private Serializable deserialize(byte[] value) {Serializable result;try (ByteArrayInputStream bis = new ByteArrayInputStream(value);ObjectInputStream ois = new CustomObjectInputStream(bis)) {result = (Serializable) ois.readObject();} catch (Exception e) {throw new CacheException("Error deserializing object. Cause: " + e, e);}return result;}public static class CustomObjectInputStream extends ObjectInputStream {public CustomObjectInputStream(InputStream in) throws IOException {super(in);}@Overrideprotected Class<?> resolveClass(ObjectStreamClass desc) throws ClassNotFoundException {return Resources.classForName(desc.getName());}}
}
5.1.6 PerpetualCache
PerpetualCache缓存默认实现类
public class PerpetualCache implements Cache {private final String id;private Map<Object, Object> cache = new HashMap<>();public PerpetualCache(String id) {this.id = 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();}}
5.2 查询二级缓存
由前面讲解的CacheKey源码可知,二级缓存使用命中条件:会话提交后,SQL语句和参数相同,相关的statementId,RowBounds相同,且设置为自动提交事务并不会命中二级缓存。同一命名空间进行了增删改操作会导致二级缓存失效,在查询的select标签上设置useCache=false就会不将查询结果添加到二级缓存中。
对于flushCache参数,当update、insert、delete标签设置该参数为false时,做数据更改不会导致缓存失效,该标签在select标签上与增删改标签语义相反。
由于二级索引是对同一命名空间生效,对于类似联合查询表,若更新了其他几个表,会导致缓存不能及时更新,可使用cache-ref来指定引用mapper映射文件的缓存机制,但cache-ref只能指定一个命名空间。
我们假设一种情况,若会话一与会话二是两条隔离的事务,但由于二级缓存的存在导致彼此可能发生脏读。若会话二的修改直接填充到二级缓存,会话一查询时缓存中存在即直接返回数据,此时会话二回滚会话一读到的数据就是脏数据。为了解决这一问题Mybatis二级缓存机制就引入了事务管理器暂存区,所有变动的数据都会暂时存放到事务管理器的暂存区中,只有执行提交动作后才会真正的将数据从暂存区中填充到二级缓存中,这样MyBatis就没有读脏数据的问题了。
二级缓存构建在一级缓存之上,在收到查询请求时,MyBatis 首先会查询二级缓存。若二级缓存未命中,再去查询一级缓存。与一级缓存不同,二级缓存和具体的命名空间绑定,一级缓存则是和 SqlSession 绑定。在按照 MyBatis 规范使用 SqlSession 的情况下,一级缓存不存在并发问题。二级缓存则不然,二级缓存可在多个命名空间间共享。这种情况下,会存在并发问题,因此需要针对性去处理。除了并发问题,二级缓存还存在事务问题,相关问题将在后面进行分析。下面首先来看一下访问二级缓存的逻辑:
// -☆- CachingExecutor
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameterObject);// 创建 CacheKey,前面已经讲过源码了。二级缓存和一级缓存都是用CacheKey作为缓存Map中的Key。CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {// 从 MappedStatement 中获取 Cache,注意这里的 Cache 并非是在 CachingExecutor 中创建的Cache cache = ms.getCache();// 如果配置文件中没有配置 <cache>,则 cache 为空// 判断是否开启了二级缓存if (cache != null) {// 如果开启了二级缓存,判断是否需要刷新一级缓存(例如设置了 flushInterval 等属性后,可能需要定时刷新一级缓存以保证其准确性)flushCacheIfRequired(ms);// 如果开启了二级缓存并且 ResultHandler 为空(通常意味着查询操作没有特别的需求,例如只是简单地查询出结果),则进行二级缓存的查询操作if (ms.isUseCache() && resultHandler == null) {ensureNoOutParams(ms, boundSql);// 访问二级缓存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);}// 返回查询结果列表return list;}}// 如果未开启二级缓存或者有其他特殊需求(例如有 ResultHandler 或者有其他配置),则直接向一级缓存或者数据库进行查询(这里可能存在一次数据库查询)return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}private void flushCacheIfRequired(MappedStatement ms) {Cache cache = ms.getCache();if (cache != null && ms.isFlushCacheRequired()) {tcm.clear(cache);}
}
如上,注意二级缓存是从 MappedStatement 中获取的,而非由 CachingExecutor 创建。由于 MappedStatement 存在于全局配置中,可以被多个 CachingExecutor 获取到,这样就有可能会出现线程安全问题。除此之外,若不加以控制,多个事务共用一个缓存实例,会导致脏读问题。线程安全问题可以通过 SynchronizedCache 装饰类解决,该装饰类会在 Cache 实例构造期间被添加上。相关过程可以参考我之前写的文章 MyBatis-源码分析-映射文件解析过程,这里就不多说了。至于脏读问题,需要借助其他类来处理,也就是上面代码中 tcm 对象对应的类型TransactionalCacheManager 。下面分析一下。
5.2.1 MyBatis如何解决“脏读”问题?
5.2.1.1 TransactionalCacheManager和TransactionalCache
/** 事务缓存管理器 */
public class TransactionalCacheManager {// Cache 与 TransactionalCache 的映射关系表private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();public void clear(Cache cache) {// 获取 TransactionalCache 对象,并调用该对象的 clear 方法,下同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) {// 从映射表中获取 TransactionalCacheTransactionalCache txCache = transactionalCaches.get(cache);if (txCache == null) {// TransactionalCache 也是一种装饰类,为 Cache 增加事务功能txCache = new TransactionalCache(cache);transactionalCaches.put(cache, txCache);}return txCache;}
}
TransactionalCacheManager 内部维护了 Cache 实例与 TransactionalCache 实例间的映射关系,该类也仅负责维护两者的映射关系,真正做事的还是 TransactionalCache。TransactionalCache 是一种缓存装饰器,可以为 Cache 实例增加事务功能。我在之前提到的脏读问题正是由该类进行处理的。下面分析一下该类的逻辑:
public class TransactionalCache implements Cache {private final Cache delegate;private boolean clearOnCommit;// 在事务被提交前,所有从数据库中查询的结果将缓存在此集合中private final Map<Object, Object> entriesToAddOnCommit;// 在事务被提交前,当缓存未命中时,CacheKey 将会被存储在此集合中private final Set<Object> entriesMissedInCache;// 省略部分代码public Object getObject(Object key) {// 查询 delegate 所代表的缓存Object object = delegate.getObject(key);if (object == null) {// 缓存未命中,则将 key 存入到 entriesMissedInCache 中entriesMissedInCache.add(key);}if (clearOnCommit) {return null;} else {return object;}}public void putObject(Object key, Object object) {// 将键值对存入到 entriesToAddOnCommit 中,而非 delegate 缓存中entriesToAddOnCommit.put(key, object);}public Object removeObject(Object key) {return null;}public void clear() {clearOnCommit = true;// 清空 entriesToAddOnCommit,但不清空 delegate 缓存entriesToAddOnCommit.clear();}public void commit() {// 根据 clearOnCommit 的值决定是否清空 delegateif (clearOnCommit) {delegate.clear();}// 刷新未缓存的结果到 delegate 缓存中flushPendingEntries();// 重置 entriesToAddOnCommit 和 entriesMissedInCachereset();}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()) {// 将 entriesToAddOnCommit 中的内容转存到 delegate 中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 {// 调用 removeObject 进行解锁delegate.removeObject(entry);} catch (Exception e) {log.warn("...");}}}
}
在 TransactionalCache 的代码中,我们要重点关注 entriesToAddOnCommit 集合,TransactionalCache 中的很多方法都会与这个集合打交道。该集合用于存储从查询的结果,那为什么要将结果保存在该集合中,而非 delegate 所表示的缓存中呢?主要是因为直接存到 delegate 会导致脏数据问题。下面通过一张图演示一下脏数据问题发生的过程,假设两个线程开启两个不同的事务,它们的执行过程如下:
如上图,时刻2,事务 A 对记录 A 进行了更新。时刻3,事务 A 从数据库查询记录 A,并将记录 A 写入缓存中。时刻4,事务 B 查询记录 A,由于缓存中存在记录 A,事务 B 直接从缓存中取数据。这个时候,脏数据问题就发生了。事务 B 在事务 A 未提交情况下,读取到了事务 A 所修改的记录。为了解决这个问题,我们可以为每个事务引入一个独立的缓存。查询数据时,仍从 delegate 缓存(以下统称为共享缓存)中查询。若缓存未命中,则查询数据库。存储查询结果时,并不直接存储查询结果到共享缓存中,而是先存储到事务缓存中,也就是 entriesToAddOnCommit 集合。当事务提交时,再将事务缓存中的缓存项转存到共享缓存中。这样,事务 B 只能在事务 A 提交后,才能读取到事务 A 所做的修改,解决了脏读问题。整个过程大致如下:
如上,时刻2,事务 A 和 B 同时查询记录 A。此时共享缓存中还没没有数据,所以两个事务均会向数据库发起查询请求,并将查询结果存储到各自的事务缓存中。时刻3,事务 A 更新记录 A,这里把更新后的记录 A 记为 A′。时刻4,两个事务再次进行查询。此时,事务 A 读取到的记录为修改后的值,而事务 B 读取到的记录仍为原值。时刻5,事务 A 被提交,并将事务缓存 A 中的内容转存到共享缓存中。时刻6,事务 B 再次查询记录 A,由于共享缓存中有相应的数据,所以直接取缓存数据即可。因此得到记录 A′,而非记录 A。但由于事务 A 已经提交,所以事务 B 读取到的记录 A′ 并非是脏数据。MyBatis 引入事务缓存解决了脏读问题,事务间只能读取到其他事务提交后的内容,这相当于事务隔离级别中的“读已提交(Read Committed)”。但需要注意的时,MyBatis 缓存事务机制只能解决脏读问题,并不能解决“不可重复读”问题。再回到上图,事务 B 在被提交前进行了三次查询。前两次查询得到的结果为记录 A,最后一次查询得到的结果为 A′。最后一次的查询结果与前两次不同,这就会导致“不可重复读”的问题。MyBatis 的缓存事务机制最高只支持“读已提交”,并不能解决“不可重复读”问题。即使数据库使用了更高的隔离级别解决了这个问题,但因 MyBatis 缓存事务机制级别较低。此时仍然会导致“不可重复读”问题的发生,这个在日常开发中需要注意一下。
接下来,我们再来看一下 entriesMissedInCache 集合,这个集合是用于存储未命中缓存的查询请求对应的 CacheKey。单独分析与 entriesMissedInCache 相关的逻辑没什么意义,要搞清 entriesMissedInCache 的实际用途,需要把它和 BlockingCache 的逻辑结合起来进行分析。在 BlockingCache,同一时刻仅允许一个线程通过 getObject 方法查询指定 key 对应的缓存项。如果缓存未命中,getObject 方法不会释放锁,导致其他线程被阻塞住。其他线程要想恢复运行,必须进行解锁,解锁逻辑由 BlockingCache 的 putObject 和 removeObject 方法执行。其中 putObject 会在 TransactionalCache 的 flushPendingEntries 方法中被调用,removeObject 方法则由 TransactionalCache 的 unlockMissedEntries 方法调用。flushPendingEntries 和 unlockMissedEntries 最终都会遍历 entriesMissedInCache 集合,并将集合元素传给 BlockingCache 的相关方法。这样可以解开指定 key 对应的锁,让阻塞线程恢复运行。
5.2.1.2 源码分析
我们来通过源码分析一下解决“脏读”的原理:
1、若开启二级缓存进行查询方法的时候会走到类CachingExecutor中的query方法
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {// 获得CacheCache cache = ms.getCache();if (cache != null) {// 判断是否配置了flushCache=true,若配置了则清空暂存区this.flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) {this.ensureNoOutParams(ms, boundSql);// 获得缓存List<E> list = (List)this.tcm.getObject(cache, key);// 若为空查询数据库并将数据填充到暂存区if (list == null) {list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);this.tcm.putObject(cache, key, list);}return list;}}return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
2、根据上一步中的tcm.getObject(cache, key)方法查询二级缓存
public Object getObject(Object key) {// 查询二级缓存Object object = this.delegate.getObject(key);// 如果查询缓存为空,就也先设置一个值防止缓存穿透 if (object == null) {this.entriesMissedInCache.add(key);}// 判断暂存区清空标识是否为true,若为true直接返回null,重新查询数据库防止脏读return this.clearOnCommit ? null : object;
}
5.2.1.3 测试代码
下面写点测试代码验证 MyBatis 所导致的“不可重复读”问题,首先看一下实体类:
public class Student {private Integer id;private String name;private Integer age;// 省略 getter/setter
}
对应的数据表如下:
student
+----+----------+------+
| id | name | age |
+----+----------+------+
| 1 | coolblog | 20 |
+----+----------+------+
Dao 接口与映射文件定义如下:
public interface StudentDao {Student findOne( Integer id);int update( Integer id, String name);
}
<mapper namespace="xyz.coolblog.dao.StudentDao"><!-- 注意要在映射文件中配置缓存 --><cache/><select id="findOne" resultType="xyz.coolblog.model.Student">SELECT`id`, `name`, `age`FROMstudentWHEREid = #{id}</select><update id="update">UPDATEstudentSET`name` = #{name}WHEREid = #{id}</update>
</mapper>
测试代码如下:
public class TransactionalCacheTest {private SqlSessionFactory sqlSessionFactory;private CountDownLatch countDownLatch = new CountDownLatch(1);public void prepare() throws IOException {String resource = "mybatis-transactional-cache-config.xml";InputStream inputStream = Resources.getResourceAsStream(resource);sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);inputStream.close();}public void testTransactional() throws IOException, InterruptedException, ExecutionException {ExecutorService es = Executors.newFixedThreadPool(2);// 开启两个线程Future<String> fa = es.submit(this::transactionalA);Future<String> fb = es.submit(this::transactionalB);countDownLatch.countDown();es.awaitTermination(6, TimeUnit.SECONDS);System.out.println(fa.get());System.out.println("\n -------- 分割线 ------- \n");System.out.println(fb.get());}private String transactionalA() throws Exception {SqlSession sqlSession = sqlSessionFactory.openSession();StudentDao studentDao = sqlSession.getMapper(StudentDao.class);countDownLatch.await();StringBuilder sb = new StringBuilder();sb.append("时刻1:开启事务 A\n");sb.append("时刻2:查询记录 A\n");Student s1 = studentDao.findOne(1);sb.append(s1).append("\n");sb.append("时刻3:更新记录 A\n");studentDao.update(1, "tianxiaobo");sb.append("时刻4:查询记录 A'\n");Student s2 = studentDao.findOne(1);sb.append(s2).append("\n");// 此处睡眠1秒,让事务 B 在事务 A 提交前,完成时刻4的查询请求Thread.sleep(1000);sb.append("时刻5:提交事务 A");sqlSession.commit();return sb.toString();}private String transactionalB() throws Exception {SqlSession sqlSession = sqlSessionFactory.openSession();StudentDao studentDao = sqlSession.getMapper(StudentDao.class);countDownLatch.await();StringBuilder sb = new StringBuilder();sb.append("时刻1:开启事务 B\n");sb.append("时刻2:查询数据 A\n");Student s1 = studentDao.findOne(1);sb.append(s1).append("\n");sb.append("时刻3:---------\n");sb.append("时刻4:查询数据 A\n");Student s2 = studentDao.findOne(1);sb.append(s2).append("\n");// 此处睡眠3秒,等待事务 A 提交Thread.sleep(3000);sb.append("时刻5:---------\n");sb.append("时刻6:查询数据 A'\n");Student s3 = studentDao.findOne(1);sb.append(s3).append("\n");sb.append("时刻7:提交事务 B");sqlSession.commit();return sb.toString();}
}
最后对输出结果进行简单的美化,如下:
如上,事务 B 在时刻2和时刻4读取到的记录与数据库中的记录一致,表示可重复读。但当事务 A 提交后,事务 B 在时刻6读取到的数据则是事务 A 修改的内容,这个时候就出现了“不可重复读”问题。以上测试是基于 MySql 数据可读,MySQL 默认事务级别为“可重复读”。
下面在本地开启两个 MySQL 客户端,模拟上面的执行流程。最终结果如下:
从测试结果可以看出,不可重复读问题并未发生,事务 B 三次查询结果均相同。好了,到此关于 MyBatis 二级缓存所引发的问题就分析完了。
5.2.1.3 小结
- 会话 :事务缓存管理器 :暂存区 = 1 : 1 :N
- 暂存区 :缓存区 = 1 : 1(一个暂存区对应唯一一个缓存区)
- 会话关闭,事务缓存管理器也会关闭,暂存区也会被清空
- 一个事务缓存管理器管理多个暂存区
- 有多少个暂存区取决于你访问了多少个Mapper文件(缓存的key是Mapper文件全路径ID)
5.2.2 整合第三方缓存
1、整合Redis
整合Redis,首先添加依赖,然后将全局配置文件中cache标签中type属性设置为org.mybatis.caches.redis.RedisCache
<dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-redis</artifactId><version>1.0.0-beta2</version>
</dependency>
2、整合ehcache
整合ehcache,将全局配置文件中cache标签中type属性设置为org.mybatis.caches.ehcache.EhcacheCache
<dependency><groupId>org.ehcache</groupId><artifactId>ehcache</artifactId><version>3.8.1</version>
</dependency><dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-ehcache</artifactId><version>1.2.0</version>
</dependency>
六、总结
本篇文章简单介绍了一些缓存类的实现,并对一二级缓存进行了深入分析。本文仅分析了缓存的使用过程,并未对缓存的初始化,以及 CachingExecutor 和 SimpleExecutor(继承自 BaseExecutor)创建过程进行分析。相关内容已在本系列之前的文章中已分析过,再次分析有点赘述。对于本文忽略掉的部分,希望大家可以自行探索,或者阅读我之前写的文章。
6.1 二级缓存执行流程
- 查询是实时查询缓存区的。
- 所有对二级缓存的实时变动都是通过暂存区来实现的。
- 暂存区清理完会进行标识,但此时二级缓存中数据并未清理,只有执行commit后才会真正清理二级缓存中的数据。
- 查询会实时查询缓存区,若暂存区清理标识为true就算从缓存区中查询到数据也会返回一个null,重新查询数据库(暂存区清理标识为true也会返回null是为了防止脏读,一旦提交清空掉二级缓存中的数据此时读取到的就是脏数据,因此返回null重新查询数据库得到的才是正确数据)。
相关文章:【MyBatis】MyBatis的一级缓存和二级缓存简介_mybatis 的一级缓存和二级缓存-CSDN博客
【MyBatis】史上最全的MyBatis执行SQL原理分析_mybatis sql-CSDN博客
【MyBatis】MyBatis解析动态SQL原理分析-CSDN博客
【MyBatis】一篇文章带你彻底搞懂MyBatis解析SQL映射文件底层原理_mybatis 注解映射sql 原理-CSDN博客
【MyBatis】MyBatis内置数据源_pooleddatasource-CSDN博客
【MyBatis】MyBatis解析全局配置文件源码详解_mybatis解析配置文件源码-CSDN博客
【MyBatis】MyBatis的日志实现_mybatis 日志-CSDN博客
【MyBatis】MyBatis的介绍和基本使用_mybatis db-CSDN博客