目录
为什么要用缓存
一级缓存
一级缓存的生命周期
一级缓存失效情况
二级缓存
二级缓存失效
自定义缓存
Cache组件
PerpetualCache
BlockingCache
FifoCache和LruCache
SoftCache和WeakCache
CacheKey
在计算机的世界中,缓存无处不在,操作系统有操作系统的缓存,数据库也会有数据库的缓存,各种中间件如Redis也是用来充当缓存的作用,编程语言中又可以利用内存来作为缓存。 MyBatis作为一款优秀的ORM框架,也用到了缓存,本文的目的就是探究一下MyBatis的缓存是如何实现的。尤其是I/O操作,除了那种CPU密集型的系统,其余大部分的业务系统性能瓶颈最后或多或少都会出现在I/O操作上,所以为了减少磁盘的I/O次数,缓存是必不可少的,通过缓存的使用我们可以大大减少I/O操作次数,从而在一定程度上弥补了I/O操作和CPU处理速度之间的鸿沟。在ORM框架中引入缓存的目的就是为了减少读取数据库的次数,从而提升查询的效率。
前面了解了SqlSessionFactory、SqlSession、Excutor以及Mpper执行SQL过程,下面来了解下myabtis的缓存,它的缓存分为一级缓存和二级缓存。 使用MyBatis开启一次和数据库的会话,MyBatis会创建出一个SqlSession对象表示一次数据库会话,在对数据库的一次会话中, 有可能会反复地执行完全相同的查询语句,每一次查询都会去查一次数据库,为了减少资源浪费,mybaits提供了一种缓存的方式(一级缓存)。 mybatis的SQL执行最后是交给了Executor执行器来完成的,看下BaseExecutor类的源码:
@Override
public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
@Override
public List 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 list;
try {
queryStack++;
list = resultHandler == null ? (List) localCache.getObject(key) : null;//localCache 本地缓存
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); //如果缓存没有就走DB
}
} 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;
}
private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List 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;
}
它的本地缓存使用的是PerpetualCache类,内部是一个HashMap作了一个封装来存数据。缓存Key的生成:
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(Integer.valueOf(rowBounds.getOffset()));
cacheKey.update(Integer.valueOf(rowBounds.getLimit()));
cacheKey.update(boundSql.getSql());
List parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
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) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
通过statementId,params,rowBounds,BoundSql来构建一个key值,根据这个key值去缓存Cache中取出对应缓存结果。
比如要执行一个查询操作时,Mybatis会创建一个新的SqlSession对象,SqlSession对象找到具体的Executor, Executor持有一个PerpetualCache对象;当查询结束(会话结束)时,SqlSession、Executor、PerpetualCache对象占有的资源一并释放掉。
如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。
如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。
SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用。
注意:一级缓存为sqlSession级别的缓存,默认开启的,不能关闭。一级缓存失效的四种情况:
1)sqlSession不同,缓存失效。
2)sqlSession相同,查询条件不同,缓存失效,因为缓存中可能还没有相关数据。
3)sqlSession相同,在两次查询期间,执行了增删改操作,缓存失效。
4)sqlSession相同,但是手动清空了一级缓存,缓存失效。
清除缓存情况:
1、就是获取缓存之前会先进行判断用户是否配置了flushCache=true属性(参考一级缓存的创建代码截图),
如果配置了则会清除一级缓存。
2、MyBatis全局配置属性localCacheScope配置为Statement时,那么完成一次查询就会清除缓存。
3、在执行commit,rollback,update方法时会清空一级缓存
Mybatis默认对二级缓存是关闭的,一级缓存默认开启,如果需要开启只需在mapper上加入配置就好了。Executor是执行查询的最终接口,它有两个实现类一个是BaseExecutor另外一个是CachingExecutor。CachingExecutor(二级缓存查询),一级缓存因为只能在同一个SqlSession中共享,所以会存在一个问题,在分布式或者多线程的环境下,不同会话之间对于相同的数据可能会产生不同的结果,因为跨会话修改了数据是不能互相感知的,所以就有可能存在脏数据的问题,正因为一级缓存存在这种不足,需要一种作用域更大的缓存,这就是二级缓存。
CachingExecutor实现类里面的query查询方法:
@Override
ublic List 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 list = (List) tcm.getObject(cache, key);//从缓存中读取
if (list == null) {
//这段走到一级缓存或者DB
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116 //数据放入缓存
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
一个事务方法运行时,数据查询出来,缓存在一级缓存了,但是没有到二级缓存,当事务提交后(sqlSession.commit()),数据才放到二级缓存。查询的顺序是,先查二级缓存再查一级缓存然后才去数据库查询。
一级缓存作用域是SqlSession级别,所以它存储的SqlSession中的BaseExecutor之中,但是二级缓存目的要实现作用范围更广,所以要实现跨会话共享,MyBatis二级缓存的作用域是namespace,专门用了一个装饰器来维护,这就是:CachingExecutor。
二级缓存相关的配置有三个地方:
1、mybatis-config中有一个全局配置属性,这个不配置也行,因为默认就是true。
想详细了解mybatis-config的可以点击这里。
2、在Mapper映射文件内需要配置缓存标签:
或
想详细了解Mapper映射的所有标签属性配置可以点击这里。
3、在select查询语句标签上配置useCache属性,如下:
以上配置第1点是默认开启的,也就是说我们只要配置第2点就可以打开二级缓存了,
而第3点是当我们需要针对某一条语句来配置二级缓存时候则可以使用。
1、需要commit事务之后才会生效
2、如果使用的是默认缓存,那么结果集对象需要实现序列化接口(Serializable)
如果不实现序列化接口则会报如下错误
* 二级缓存工作机制(?)
1)一个会话,查询一条数据,该数据会放在当前会话的一级缓存中。
2)如果当前会话关闭,对应的一级缓存会被保存到二级缓存中,新的会话查询信息,就可以参照二级缓存。
3)不同namespace查询出的数据会放在自己对应的缓存中。
注意:查出的数据都会默认放在一级缓存中,只有会话提交或关闭后,一级缓存的数据才会被转移到二级缓存中。
* 需要注意的是在事务提交之前,并不会真正存储到二级缓存,而是先存储到一个临时属性,
* 等事务提交之后才会真正存储到二级缓存。(?)
Mybatis缓存包装汇总:
PerpetualCache 缓存默认实现类 - 基本功能,默认携带
LruCache LRU淘汰策略缓存(默认淘汰策略) 当缓存达到上限,删除最近最少使用缓存 eviction=“LRU”
FifoCache FIFO淘汰策略缓存 当缓存达到上限,删除最先入队的缓存 eviction=“FIFO”
SoftCache JVM软引用淘汰策略缓存 基于JVM的SoftReference对象 eviction=“SOFT”
WeakCache JVM弱引用淘汰策略缓存 基于JVM的WeakReference对象 eviction=“WEAK”
LoggingCache 带日志功能缓存 输出缓存相关日志信息 基本功能,默认包装
SynchronizedCache 同步缓存 基于synchronized关键字实现,用来解决并发问题 基本功能,默认包装
BlockingCache 阻塞缓存 get/put操作时会加锁,防止并发,基于Java重入锁实现 blocking=true
SerializedCache 支持序列化的缓存 通过序列化和反序列化来存储和读取缓存 readOnly=false(默认)
ScheduledCache 定时调度缓存 操作缓存时如果缓存已经达到了设置的最长缓存时间时会移除缓存 flushInterval属性不为空
TransactionalCache 事务缓存 在TransactionalCacheManager中用于维护缓存map的value值
所有的update操作(insert,delete,uptede)都会触发缓存的刷新,从而导致二级缓存失效,所以二级缓存适合在读多写少的场景中开启。
二级缓存针对的是同一个namespace,所以建议是在单表操作的Mapper中使用,或者是在相关表的Mapper文件中共享同一个缓存。
一级缓存可能存在脏读情况,那么二级缓存是否也可能存在呢?是的,默认的二级缓存也是存储在本地缓存,对于微服务下是可能出现脏读的情况的,这时可能会需要自定义缓存,比如利用redis来存储缓存,而不是存储在本地内存当中。
MyBatis官方也提供了第三方缓存的支持引入pom文件:
org.mybatis.caches
mybatis-redis
1.0.0-beta2
然后缓存配置如下:
然后在默认的resource路径下新建一个redis.properties文件:
host=localhost
port=6379
延迟加载原理
延迟加载原理:调用的时候触发加载,不是在初始化的时候就加载信息。MyBatis支持延迟加载,设置lazyLoadingEnabled=true即可。
比如:a.getB().getName(),发现a.getB()的值为null,此时会单独触发事件,将保存好的关联B对象的SQL查询出来,
然后再调用a.setB(b),这时再调用a.getB().getName()就有值了。
MyBatis 中缓存模块相关的代码位于 org.apache.ibatis.cache 包 下,其中 Cache 接口 是缓存模块中最核心的接口,它定义了所有缓存的基本行为。
public interface Cache {
/**
* 获取当前缓存的 Id
*/
String getId();
/**
* 存入缓存的 key 和 value,key 一般为 CacheKey对象
*/
void putObject(Object key, Object value);
/**
* 根据 key 获取缓存值
*/
Object getObject(Object key);
/**
* 删除指定的缓存项
*/
Object removeObject(Object key);
/**
* 清空缓存
*/
void clear();
/**
* 获取缓存的大小
*/
int getSize();
/**
* !!!!!!!!!!!!!!!!!!!!!!!!!!
* 获取读写锁,可以看到,这个接口方法提供了默认的实现!!
* 这是 Java8 的新特性!!只是平时开发时很少用到!!!
* !!!!!!!!!!!!!!!!!!!!!!!!!!
*/
default ReadWriteLock getReadWriteLock() {
return null;
}
}
Cache接口的实现类有很多,大部分都是装饰器,只有PerpetualCache 提供了 Cache 接口 的基本实现
PerpetualCache(Perpetual:永恒的,持续的)在缓存模块中扮演着被装饰的角色,其实现比较简单,底层使用 HashMap 记录缓存项,也是通过该 HashMap 对象 的方法实现的 Cache 接口 中定义的相应方法。
public class PerpetualCache implements Cache {
// Cache对象 的唯一标识
private final String id;
// 其所有的缓存功能实现,都是基于 JDK 的 HashMap 提供的方法
private Map
cache.decorators包下提供的装饰器,它们都直接实现了Cache接口,扮演着装饰器的角色。这些装饰器会在 PerpetualCache 的基础上提供一些额外的功能,通过多个组合后满足一个特定的需求。
BlockingCache 是阻塞版本的缓存装饰器,它会保证只有一个线程到数据库中查找指定 key 对应的数据
public class BlockingCache implements Cache {
// 阻塞超时时长
private long timeout;
// 持有的被装饰者
private final Cache delegate;
// 每个 key 都有其对应的 ReentrantLock锁对象
private final ConcurrentHashMap
假设线程A在BlockingCache中未查找到 keyA 对应的缓存项时,线程 A 会获取 keyA 对应的锁,这样,线程 A 在后续查找 keyA 时,其它线程会被阻塞。
// 根据 key 获取锁对象,然后上锁
private void acquireLock(Object key) {
// 获取 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 ReentrantLock getLockForKey(Object key) {
// Java8 新特性,Map系列类 中新增的方法
// V computeIfAbsent(K key, Function super K, ? extends V> mappingFunction)
// 表示,若 key 对应的 value 为空,则将第二个参数的返回值存入该 Map集合 并返回
return locks.computeIfAbsent(key, k -> new ReentrantLock());
}
假设 线程 A 从数据库中查找到 keyA 对应的结果对象后,将结果对象放入到 BlockingCache 中,此时 线程 A 会释放 keyA 对应的锁,唤醒阻塞在该锁上的线程。其它线程即可从 BlockingCache 中获取 keyA 对应的数据,而不是再次访问数据库。
@Override
public void putObject(Object key, Object value) {
try {
// 存入 key 和其对应的缓存项
delegate.putObject(key, value);
} finally {
// 最后释放锁
releaseLock(key);
}
}
private void releaseLock(Object key) {
ReentrantLock lock = locks.get(key);
// 锁是否被当前线程持有
if (lock.isHeldByCurrentThread()) {
// 是,则释放锁
lock.unlock();
}
}
为了控制缓存的大小,系统需要按照一定的规则清理缓存。FifoCache 是先入先出版本的装饰器,当向缓存添加数据时,如果缓存项的个数已经达到上限,则会将缓存中最老(即最早进入缓存)的缓存项删除。
public class FifoCache implements Cache {
// 被装饰对象
private final Cache delegate;
// 用一个 FIFO 的队列记录 key 的顺序,其具体实现为 LinkedList
private final Deque
LruCache 是按照"近期最少使用算法"(Least Recently Used, LRU)进行缓存清理的装饰器,在需要清理缓存时,它会清除最近最少使用的缓存项。
public class LruCache implements Cache {
// 被装饰者
private final Cache delegate;
// 这里使用的是 LinkedHashMap,它继承了 HashMap,但它的元素是有序的
private Map
在分析 SoftCache 和 WeakCache 实现之前,我们再温习一下 Java 提供的 4 种引用类型,强引用 StrongReference、软引用 SoftReference、弱引用 WeakReference 和虚引用 PhantomReference。
SoftCache 的具体实现
public class SoftCache implements Cache {
// 这里使用了 LinkedList 作为容器,在 SoftCache 中,最近使用的一部分缓存项不会被 GC
// 这是通过将其 value 添加到 hardLinksToAvoidGarbageCollection集合 实现的(即,有强引用指向其value)
private final Deque hardLinksToAvoidGarbageCollection;
// 引用队列,用于记录已经被 GC 的缓存项所对应的 SoftEntry对象
private final ReferenceQueue queueOfGarbageCollectedEntries;
// 持有的被装饰者
private final Cache delegate;
// 强连接的个数,默认为 256
private int numberOfHardLinks;
// 构造方法进行属性的初始化
public SoftCache(Cache delegate) {
this.delegate = delegate;
this.numberOfHardLinks = 256;
this.hardLinksToAvoidGarbageCollection = new LinkedList<>();
this.queueOfGarbageCollectedEntries = new ReferenceQueue<>();
}
private static class SoftEntry extends SoftReference {
private final Object key;
SoftEntry(Object key, Object value, ReferenceQueue garbageCollectionQueue) {
// 指向 value 的引用是软引用,并且关联了 引用队列
super(value, garbageCollectionQueue);
// 强引用
this.key = key;
}
}
@Override
public void putObject(Object key, Object value) {
// 清除已经被 GC 的缓存项
removeGarbageCollectedItems();
// 添加缓存
delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
}
private void removeGarbageCollectedItems() {
SoftEntry sv;
// 遍历 queueOfGarbageCollectedEntries集合,清除已经被 GC 的缓存项 value
while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
delegate.removeObject(sv.key);
}
}
@Override
public Object getObject(Object key) {
Object result = null;
@SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache
// 用一个软引用指向 key 对应的缓存项
SoftReference softReference = (SoftReference) delegate.getObject(key);
// 检测缓存中是否有对应的缓存项
if (softReference != null) {
// 获取 softReference 引用的 value
result = softReference.get();
// 如果 softReference 引用的对象已经被 GC,则从缓存中清除对应的缓存项
if (result == null) {
delegate.removeObject(key);
} else {
synchronized (hardLinksToAvoidGarbageCollection) {
// 将缓存项的 value 添加到 hardLinksToAvoidGarbageCollection集合 中保存
hardLinksToAvoidGarbageCollection.addFirst(result);
// 如果 hardLinksToAvoidGarbageCollection 的容积已经超过 numberOfHardLinks
// 则将最老的缓存项从 hardLinksToAvoidGarbageCollection 中清除,FIFO
if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
hardLinksToAvoidGarbageCollection.removeLast();
}
}
}
}
return result;
}
@Override
public Object removeObject(Object key) {
// 清除指定的缓存项之前,也会先清理被 GC 的缓存项
removeGarbageCollectedItems();
return delegate.removeObject(key);
}
@Override
public void clear() {
synchronized (hardLinksToAvoidGarbageCollection) {
// 清理强引用集合
hardLinksToAvoidGarbageCollection.clear();
}
// 清理被 GC 的缓存项
removeGarbageCollectedItems();
// 清理最底层的缓存项
delegate.clear();
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
removeGarbageCollectedItems();
return delegate.getSize();
}
public void setSize(int size) {
this.numberOfHardLinks = size;
}
}
WeakCache 的实现与 SoftCache 基本类似,唯一的区别在于其中使用 WeakEntry(继承了 WeakReference)封装真正的 value 对象,其他实现完全一样。
另外,还有 ScheduledCache、LoggingCache、SynchronizedCache、SerializedCache 等。ScheduledCache 是周期性清理缓存的装饰器,它的 clearInterval 字段 记录了两次缓存清理之间的时间间隔,默认是一小时,lastClear 字段 记录了最近一次清理的时间戳。 ScheduledCache 的 getObject()、putObject()、removeObject() 等核心方法,在执行时都会根据这两个字段检测是否需要进行清理操作,清理操作会清空缓存中所有缓存项。
LoggingCache 在 Cache 的基础上提供了日志功能,它通过 hit 字段 和 request 字段 记录了 Cache 的命中次数和访问次数。在 LoggingCache.getObject()方法 中,会统计命中次数和访问次数 这两个指标,井按照指定的日志输出方式输出命中率。
SynchronizedCache 通过在每个方法上添加 synchronized 关键字,为 Cache 添加了同步功能,有点类似于 JDK 中 Collections 的 SynchronizedCollection 内部类。
SerializedCache 提供了将 value 对象 序列化的功能。SerializedCache 在添加缓存项时,会将 value 对应的 Java 对象 进行序列化,井将序列化后的 byte[]数组 作为 value 存入缓存 。 SerializedCache 在获取缓存项时,会将缓存项中的 byte[]数组 反序列化成 Java 对象。不使用 SerializedCache 装饰器 进行装饰的话,每次从缓存中获取同一 key 对应的对象时,得到的都是同一对象,任意一个线程修改该对象都会影响到其他线程,以及缓存中的对象。而使用 SerializedCache 每次从缓存中获取数据时,都会通过反序列化得到一个全新的对象。 SerializedCache 使用的序列化方式是 Java 原生序列化。
在 Cache 中唯一确定一个缓存项,需要使用缓存项的 key 进行比较,MyBatis 中因为涉及 动态 SQL 等多方面因素, 其缓存项的 key 不能仅仅通过一个 String 表示,所以 MyBatis 提供了 CacheKey 类 来表示缓存项的 key,在一个 CacheKey 对象 中可以封装多个影响缓存项的因素。 CacheKey 中可以添加多个对象,由这些对象共同确定两个 CacheKey 对象 是否相同。
public class CacheKey implements Cloneable, Serializable {
private static final long serialVersionUID = 1146682552656046210L;
public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
// 参与计算hashcode,默认值DEFAULT_MULTIPLYER = 37
private final int multiplier;
// 当前CacheKey对象的hashcode,默认值DEFAULT_HASHCODE = 17
private int hashcode;
// 校验和
private long checksum;
private int count;
// 由该集合中的所有元素 共同决定两个CacheKey对象是否相同,一般会使用一下四个元素
// MappedStatement的id、查询结果集的范围参数(RowBounds的offset和limit)
// SQL语句(其中可能包含占位符"?")、SQL语句中占位符的实际参数
private List updateList;
// 构造方法初始化属性
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
this.updateList = new ArrayList<>();
}
public CacheKey(Object[] objects) {
this();
updateAll(objects);
}
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
// 重新计算count、checksum和hashcode的值
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
// 将object添加到updateList集合
updateList.add(object);
}
public int getUpdateCount() {
return updateList.size();
}
public void updateAll(Object[] objects) {
for (Object o : objects) {
update(o);
}
}
/**
* CacheKey重写了 equals() 和 hashCode()方法,这两个方法使用上面介绍
* 的 count、checksum、hashcode、updateList 比较两个 CacheKey对象 是否相同
*/
@Override
public boolean equals(Object object) {
// 如果为同一对象,直接返回 true
if (this == object) {
return true;
}
// 如果 object 都不是 CacheKey类型,直接返回 false
if (!(object instanceof CacheKey)) {
return false;
}
// 类型转换一下
final CacheKey cacheKey = (CacheKey) object;
// 依次比较 hashcode、checksum、count,如果不等,直接返回 false
if (hashcode != cacheKey.hashcode) {
return false;
}
if (checksum != cacheKey.checksum) {
return false;
}
if (count != cacheKey.count) {
return false;
}
// 比较 updateList 中的元素是否相同,不同直接返回 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;
}
@Override
public int hashCode() {
return hashcode;
}
@Override
public String toString() {
StringJoiner returnValue = new StringJoiner(":");
returnValue.add(String.valueOf(hashcode));
returnValue.add(String.valueOf(checksum));
updateList.stream().map(ArrayUtil::toString).forEach(returnValue::add);
return returnValue.toString();
}
@Override
public CacheKey clone() throws CloneNotSupportedException {
CacheKey clonedCacheKey = (CacheKey) super.clone();
clonedCacheKey.updateList = new ArrayList<>(updateList);
return clonedCacheKey;
}
}
MyBatis的缓存模块, Cache接口以及多个实现类的具体实现,是 Mybatis 中一级缓存和二级缓存的基础。