一. 概念
道友们在看完前两篇文章后都会知道一级缓存是本地缓存也就是会话级别的(SqlSession),当会话结束后一级缓存中的数据也就没了,为了解决缓存可以跨线程/会话,MyBatis就做了二级缓存,此缓存是应用级的范围是
namespace
级别可以被多个会话共享可以对应一个或多个namespace(xxxMapper.xml),当然想要使用二级缓存是需要手动配置的,MyBatis默认为我们开启了此缓存,但我们需要自己为使用二级缓存的Mapper和select语句进行配置。 一级缓存的什么周期是非常短暂的会话结束缓存中的东西也就没有了,所以没有必要去做容量限制,相比二级缓存是作用于整个应用的必须要做容量限制,不然大量数据放缓存中而不去清理淘汰会很快撑爆内存引发OOM(
java.lang.OutOfMemoryError
)错误。MyBatis使用LRU
最近最少使用策略为默认的缓存淘汰机制,道友们可以自行实现淘汰机制,常见的淘汰策略有:
- FIFO(First In First Out 先进先出)即按照缓存项的插入顺序来淘汰最早加入缓存的项。
- LRU(Least Recently Used):最近最少使用,即根据最近一段时间内缓存项的访问顺序来淘汰最近最少使用的缓存项。
- LFU(Least Frequently Used)最不经常使用,即根据一段时间内缓存项被访问的频率来淘汰最不经常使用的缓存项。
- 定时过期:设置缓存项的过期时间,在缓存项超过设定的过期时间后自动淘汰。
1. 执行流程
MyBatis分别有一级缓存和二级缓存,他们的执行流程是,请求进来会先到二级缓存也就是
org.apache.ibatis.executor.CachingExecutor
,如果此缓存中没有获取到则会委托给Executor的子实现去一级缓存中获取,如没有只就会去查询数据库,然后填充缓存。
上图可以很清晰的看出整个查询获取数据的流程,当然这里是没有jdbc相关的描述,后面文章如需要我会提起,上图中有一个暂存区,这个东西很多人命名都不一样,我就叫暂存区了,这块地方是在数据库获取一级缓存中获取到数据后放入到暂存区,二级缓存是跨线程的要保证数据没有被别的线程给修改或回滚所以先放在暂存区等待会话提交后才会插入到二级缓存中。
其他的东西在前两节都有说过,我就不一一细说了,道友们如有看不懂此流程图的地方可以看下上一章有详细说明一级缓存或者评论区留言哦。
二. 使用及配置
二级缓存MyBatis默认帮我们开启了,道友们只需要配置Mapper和Select语句即可,MyBatis为我们提供了xml形式配置和注解配置,我在此文章采用了原始的xml配置。
1. 二级缓存开关
配置mybatis-config.xml在setting中设置cacheEnable = false关闭二级缓存反之则开启。
<!-- 外层还有configuration标签,我给省略了,道友们可别忘了 -->
<settings>
<!-- 使用 log4j 用来输出日志信息 -->
<setting name="logImpl" value="LOG4J"/>
<setting name="defaultExecutorType" value="REUSE"/>
<!-- 二级缓存的开关 -->
<setting name="cacheEnabled" value="true"/>
</settings>
从org.apache.ibatis.session.Configuration
中可以看出MyBatis是默认开启了二级缓存的。
2. Mapper配置
在开启了二级缓存后,需要配置自己的xxxMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.glwxx.mapper.UserMapper">
<!-- 为当前namespace开启二级缓存 -->
<cache />
<select id="getOne" resultType="cn.glwxx.model.SUser">
select * from s_user where id = #{id}
</select>
<!-- <select id="get" resultType="cn.glwxx.model.SUser">-->
<!-- select * from s_user-->
<!-- </select>-->
</mapper>
- cache的配置项
-
type:指定缓存实现的类型,默认是
org.apache.ibatis.cache.impl.PerpetualCache
,也可实现org.apache.ibatis.cache.Cache
接口自行添加缓存的实现。 -
size:指定缓存项的最大数量,超过这个数量会触发缓存淘汰。
-
eviction:指定缓存的淘汰策略。
-
readOnly:指定缓存是否只读,默认为 false,如果设置为 true,则表示缓存只用于读操作,不会更新缓存。
-
flushInterval:设置缓存刷新间隔,单位为毫秒,默认值为不刷新(即默认为 0)。
-
blocking:此配置设置为
true
表示在查询缓存中查找数据时,如果有其他线程正在更新该数据,当前线程会被阻塞,直到其他线程更新完成并释放锁,然后才会继续执行查询操作,反之则不会阻塞直接获取数据。此配置项可牺牲一定的性能保证数据一致性反之牺牲数据一致性保证查询性能。有得有失吧!
3. 代码演示
public static void test06() {
SqlSession sqlSession = sqlSessionFactoryBuilder.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
SUser one = mapper.getOne(1);
// 1. sqlSession.commit();
SqlSession sqlSession1 = sqlSessionFactoryBuilder.openSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
SUser one1 = mapper1.getOne(1);
}
执行结果:
可以看到在【1】处没有提交前一个会话时,缓存命中率是0,所以必须提交缓存后才会插入到二级缓存中。放开注释后再次运行看下图:
在放开注释后第二次查询请求就命中了缓存,第二次查询的时候从日志中可以看出是没有进行预编译和查询的,在红框中也标出了缓存命中率为0.5,第一次没有命中是缓存中本身就是空的,执行到commit的时候才会提交缓存插入进去,第二次就会命中。
在上面代码中有个很明显体现二级缓存特性的就是我打开了两个会话(SqlSession),都是不同的会话,使用的同一个命名空间(Mapper),第二次查询的时候也是有命中缓存,从缓存中获取了数据。
注:还有一点是需要注意的就是我们的实体类想要放入二级缓存中就必须得实现序列化接口(
java.io.Serializable
)不然会抛异常(org.apache.ibatis.cache.CacheException
),这里实现序列化主要是为了方便扩展和自定义实现存储方式,原因如下:
- 跨进程或跨网络传输: 缓存的对象可能需要在不同的进程或者不同的服务器之间进行传输,而对象序列化可以将对象转换为字节序列,便于在网络上传输。
- 持久化: 缓存的数据可能需要持久化到磁盘或者其他介质中,而对象序列化可以将对象转换为字节序列,便于存储到文件系统或者数据库中。
- 存储到内存中: 缓存的数据通常是存储在内存中的,而内存中的数据是以字节序列的形式进行存储和操作的,因此对象序列化是将对象转换为内存中的字节序列的一种方式。
MyBatis在存储获取二级缓存的时候是有一条执行链路的也就是责任链设计模式,其中就有序列化和反序列化的实现。下面会说明执行的链路还有两个设计模式装饰器+责任链设计模式。
三. 原理
回顾:在上面执行流程的图中,道友看完了就会知道,当会话发起一个查询请求的时候会先到CachingExecutor执行器从二级缓存中获取数据,如果没有获取到就会交给
org.apache.ibatis.executor.Executor
装饰的子实现去一级缓存中找没有就去数据库查询。
1. 执行链路
这条执行链是MyBatis默认的,如果在xxxMaper.xml文件中设置了
cache
标签中的blocking = true
属性则会在SynchronizedCache
执行之前先执行BlockingCache
阻塞装饰器,同时在LruCache
位置可选择的淘汰机制有如下几个:
- FifoCache (FIFO)
- SoftCache (Soft)软引用装饰器
- WeakCache (Weak)弱引用装饰器。
在MyBatis中一共就这四个淘汰机制,如果在xxxMaper.xml文件中配置了定时清理,则会在淘汰机制装饰器前面(LruCache)添加一个
ScheduledCache
装饰器。这些道友可以自行debug调试验证下,我就不一个个去截图了,我是本地写的文档,发布出去的时候图片位置老是乱,所以我尽可能少截图。 SoftCache 和 WeakCache 我就不在这里说了,具体怎么实现的道友们可以自己看下,也没啥难得,就概念的东西,我在这里说的话还得先说Java的几种引用类型(强引用,弱引用,软引用,虚引用(也称幽灵引用))所以我就不展开说了,后面看摸鱼时间多的话再说吧。算了,简单提下吧:
- 强引用(StrongReference): 强引用是最常见的引用类型。如果一个对象具有强引用,垃圾回收器不会回收这个对象,即使内存不足时也不会回收。只有当没有任何强引用指向一个对象时,该对象才会被回收,就像Obj obj = new Obj(); 这就是强引用,简写哈。
- 软引用(SoftReference): 软引用是一种比强引用弱一些的引用类型。如果一个对象只有软引用指向它,当内存不足时,垃圾回收器可能会回收这个对象。软引用通常用于实现内存敏感的缓存。
- 弱引用(WeakReference): 弱引用比软引用更弱一些。如果一个对象只有弱引用指向它,那么在下一次垃圾回收时,不管内存是否充足,垃圾回收器都会回收这个对象。
- 虚引用(PhantomReference): 虚引用是 Java 中最弱的引用类型。虚引用主要用于跟踪对象被垃圾回收的情况。虚引用与前面三种引用不同,它的 get() 方法始终返回 null,并且虚引用必须和 ReferenceQueue 一起使用。当虚引用指向的对象被垃圾回收时,会将虚引用添加到与之关联的 ReferenceQueue 中。
以上这些引用类型在Java中都用定义,道友可以看下,我只是介绍说下,具体怎么使用以后再说吧,不然就偏题了。
在上面介绍了执行链路,可以看出这里使用了装饰器设计模式,我们可以很方便的对MyBatis二级缓存进行扩展。下面我就逐一解释下MyBatis默认的执行链,在xxxmMapper.xml的Cache标签中没有配置任何东西的前提下,就是我上图展示的那几个类。
2. 装饰器解释
1. 组装过程
MyBatis在读取完配置文件/注解标注,之后会通过
org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache(xxx)
方法将读取到的cache标签中的内容传递给org.apache.ibatis.mapping.CacheBuilder
然后通过build()
方法返回已经组装好的org.apache.ibatis.cache.Cache
。下面看下组装过程:// org.apache.ibatis.builder.MapperBuilderAssistant public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) { // 1. 创建CacheBuilder对象并传入当前的namespace就是那个xxxMapper.java的全路径类名并将参数设置给CacheBuilder 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(); // 2. 使用build组装 // 3. 并将cache加入到Configuration中 configuration.addCache(cache); // 4。 设置当前currentNamespace的cache currentCache = cache; // 这里的返回值其实没人用,可以改成void,可能是为了扩展吧 return cache; }
在【1】处创建CacheBuilder对象并组装了二级缓存的整条执行链。
最终是将cache对象添加到了
org.apache.ibatis.mapping.MappedStatement
中,这个类在上一节中都有见过,public class CacheBuilder { // 缓存的id对应xxxMapper.java类路径 private final String id; // 二级缓存最终存放使用的类路径 private Class<? extends Cache> implementation; // 对应淘汰机制的类路径列表 private final List<Class<? extends Cache>> decorators; // 缓存容量,超过这个值就会触发淘汰机制 private Integer size; // 缓存过期时间 默认是0永远不过期,单位毫秒 private Long clearInterval; // 是否只读 private boolean readWrite; // 属性值 private Properties properties; // 是否开启请求时阻塞缓存 private boolean blocking; // ... 省略了构造器和赋值的方法 public Cache build() { // 1. 设置默认的缓存存放实现类 MyBatis默认就一个PerpetualCache setDefaultImplementations(); Cache cache = newBaseCacheInstance(implementation, id); // 2. 设置Cache属性 setCacheProperties(cache); // issue #352, do not apply decorators to custom caches // 3. 只有PerpetualCache类才能设置Cache添加功能装饰器 if (PerpetualCache.class.equals(cache.getClass())) { for (Class<? extends Cache> decorator : decorators) { cache = newCacheDecoratorInstance(decorator, cache); setCacheProperties(cache); } // 3.1 添加给定的装饰器 cache = setStandardDecorators(cache); } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) { cache = new LoggingCache(cache); } return cache; } }
在【1】处是设置了默认的存储实现,
在【3.1】处是组装执行链路的地方
// org.apache.ibatis.mapping.CacheBuilder private Cache setStandardDecorators(Cache cache) { try { // 设置下缓存容量 MetaObject metaCache = SystemMetaObject.forObject(cache); if (size != null && metaCache.hasSetter("size")) { metaCache.setValue("size", size); } // 如果有设置过期时间就添加个ScheduledCache装饰器 if (clearInterval != null) { cache = new ScheduledCache(cache); ((ScheduledCache) cache).setClearInterval(clearInterval); } // 如果是只读 if (readWrite) { 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); } }
最后将组装好的cache对象放在MappedStatement中,至此组装装饰器执行链路的动作就做完了。
2. 装饰器逐一解释
在说明每个装饰器前先说下前面的一个
org.apache.ibatis.cache.TransactionalCacheManager
这个玩意,他是TransactionalCache 的管理器,在org.apache.ibatis.executor.CachingExecutor
执行器中是显示的创建了一个TransactionalCacheManager对象,然后在查询的时候调用了里面的getObject(xxx)
方法从中获取缓存数据,然后开始走装饰器的执行链路,里面代码比较简单我就不进去看,道友们可以看看,挺简单的。
1. TransactionalCache
org.apache.ibatis.cache.decorators.TransactionalCache
这个就是二级缓存的暂存区,在事务没有提交之前,到会存放在此处。
- 构造器和属性
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
// 委托的 Cache 对象,就是前面被组装的那个玩意
private final Cache delegate;
// 提交时清空
private boolean clearOnCommit;
// 事务还未体时存放在这里
private final Map<Object, Object> entriesToAddOnCommit;
// 缓存中没有命中的 Key
private final Set<Object> entriesMissedInCache;
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<>();
this.entriesMissedInCache = new HashSet<>();
}
}
- getObject()方法
public Object getObject(Object key) {
// issue #116
// 1.缓存中获取
Object object = delegate.getObject(key);
// 2. 获取不到放入set集合中
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
// 3. 如果时持续清空则返回null,因为在事务还未提交的时候都不知道会发生什么,所以
// 就设置为true来标记
if (clearOnCommit) {
return null;
}
return object;
}
- commit()方法
public void commit() {
// 1. 如果在持续清空标记位则清空
if (clearOnCommit) {
delegate.clear();
}
// 2. 将entriesToAddOnCommit和entriesMissedInCache插入到真正的缓存中
flushPendingEntries();
// 3. 重置标记位
reset();
}
在【2】处将entriesToAddOnCommit和entriesMissedInCache插入到真正的缓存中
private void flushPendingEntries() {
// 1. 将kv对放入缓存中
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
// 2. 这里是将没有获取到缓存的那个Key也插入到缓存中
// 这里老道我不是很明白设计的用意,如果是其他事务在此之前将这个Key提交到缓存中了
// 那这里在提交不久把有值的给置空了吗,或者插入个空值,最后也要读下数据库,那这里
// 将空值放入缓存是有什么意义呢??
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
【2】处老道我的疑问有大佬知道可以评论区告诉我i下可以讨论讨论。
其他的方法都没啥说的就一两行,我就不贴出来了。
2. SynchronizedCache
org.apache.ibatis.cache.decorators.SynchronizedCache
这个装饰器主要是为了在缓存操作上添加同步机制,确保多线程环境下的缓存访问安全性。见名知意。 在以前的版本中是用的
Synchronized
关键字,后面全部换成ReentrantLock
了,具体是哪个版本我没有去看,道友们可以看下MyBatis仓库的提交记录,也是为了性能,到现在Synchronized已经被优化的和ReentrantLock差不多了,甚至高于ReentrantLock 只有有些特殊情况Synchronized可能不如ReentrantLock 。 代码比较简单我直接贴出来就不写注释了,就是加锁和解锁的过程。
public class SynchronizedCache implements Cache {
private final ReentrantLock lock = new ReentrantLock();
private final Cache delegate;
public SynchronizedCache(Cache delegate) {
this.delegate = delegate;
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
lock.lock();
try {
return delegate.getSize();
} finally {
lock.unlock();
}
}
@Override
public void putObject(Object key, Object object) {
lock.lock();
try {
delegate.putObject(key, object);
} finally {
lock.unlock();
}
}
@Override
public Object getObject(Object key) {
lock.lock();
try {
return delegate.getObject(key);
} finally {
lock.unlock();
}
}
@Override
public Object removeObject(Object key) {
lock.lock();
try {
return delegate.removeObject(key);
} finally {
lock.unlock();
}
}
@Override
public void clear() {
lock.lock();
try {
delegate.clear();
} finally {
lock.unlock();
}
}
@Override
public int hashCode() {
return delegate.hashCode();
}
@Override
public boolean equals(Object obj) {
return delegate.equals(obj);
}
}
3. LoggingCache
org.apache.ibatis.cache.decorators.LoggingCache
日志打印装饰器,就是打印个换成命中率,别的也没啥都一样。
- getObject() 方法
@Override
public Object getObject(Object key) {
// 请求进来自增
requests++;
// 获取缓存
final Object value = delegate.getObject(key);
// 获取到数据hits+1
if (value != null) {
hits++;
}
// 开启了日志打印就打印日志
if (log.isDebugEnabled()) {
log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
}
return value;
}
private double getHitRatio() {
return (double) hits / (double) requests;
}
其他方法都一样,跳过。
4. SerializedCache
org.apache.ibatis.cache.decorators.SerializedCache
序列化装饰器,主要是放入缓存的时候将实体对象序列化,获取取出来的时候反序列化操作。
- putObject()方法 深拷贝???
@Override
public void putObject(Object key, Object object) {
// 1. 判断当前Object对象(实体)是否实现了Serializable接口
// 如果没有实现Serializable接口就会抛出异常
if ((object != null) && !(object instanceof Serializable)) {
throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);
}
// 2. 放入缓存的时候序列化操作
delegate.putObject(key, serialize((Serializable) object));
}
private byte[] serialize(Serializable value) {
// 使用 try-with-resource写法,可以自动关闭资源
// 创建ByteArrayOutputStream和ObjectOutputStream对象
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
// 将序列化对象写入到ByteArrayOutputStream中
oos.writeObject(value);
// 刷新,不刷新里面没对象
oos.flush();
// 返回字节数据
return bos.toByteArray();
} catch (Exception e) {
throw new CacheException("Error serializing object. Cause: " + e, e);
}
}
- getObject()方法
@Override
public Object getObject(Object key) {
// 获取数据
Object object = delegate.getObject(key);
// 反序列换并返回
return object == null ? null : deserialize((byte[]) object);
}
private Serializable deserialize(byte[] value) {
SerialFilterChecker.check();
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;
}
5. LruCache
org.apache.ibatis.cache.decorators.LruCache
最近最少使用淘汰策略装饰器,主要是使用LinkedHashMap来实现淘汰机制。
- 构造器和属性
public class LruCache implements Cache {
// 装饰器对象Cache
private final Cache delegate;
// 实现淘汰的关键
private Map<Object, Object> keyMap;
// 最老的Key
private Object eldestKey;
public LruCache(Cache delegate) {
this.delegate = delegate;
// 默认的LinkedHashMap大小是1024
setSize(1024);
}
}
- setSize()方法
public void setSize(final int size) {
// 创建一个大小为size,75%的负载因子,且按访问顺序排序的LinkedHashMap
// 这个LinkedHashMap的accessOrder参数是关键的设置为true就是访问顺序排列,反之是插入顺序
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
// 当前LinkedHashMap的大小是否大于size触发淘汰机制
boolean tooBig = size() > size;
if (tooBig) {
// 就将最后一个元素的key保存到eldestKey中,用于淘汰
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
这个Lru淘汰机制的核心就是创建LinkedHashMap是设置的true,按照访问顺序排列,这样就可以将最老的一个元素在到达设置的触发大小时给剔除掉。
看源码就是学习使用,道友们这里的操作get到吗 -.-。
- putObject()方法
@Override
public void putObject(Object key, Object value) {
// 1. 插入缓存
delegate.putObject(key, value);
// 2. 将当前Key放在LinkedHashMap中
cycleKeyList(key);
}
private void cycleKeyList(Object key) {
// 1. 把key放进去
keyMap.put(key, key);
// 2. 最老的一个存在则在缓存中剔除
if (eldestKey != null) {
delegate.removeObject(eldestKey);
eldestKey = null;
}
}
- getObject()方法
@Override
public Object getObject(Object key) {
// 1. 使用一下这个Key,刷新LinkedHashMap的排序
keyMap.get(key); // touch
// 2. 获取数据
return delegate.getObject(key);
}
6. PerpetualCache
org.apache.ibatis.cache.impl.PerpetualCache
真正的存储缓存的地方,使用HashMap实现,代码更简单。
- 构造器和属性
public class PerpetualCache implements Cache {
private final String id;
// 存放缓存的地方
private final Map<Object, Object> cache = new HashMap<>();
// 这个ID就是namespace也就是xxxMapper.java的类路径
public PerpetualCache(String id) {
this.id = id;
}
}
- equals和hashCode()方法
@Override
public 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;
return getId().equals(otherCache.getId());
}
@Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
return getId().hashCode();
}
重写这个两个方法是为了保证namespace是唯一的。这里的重点就是重写equals的注意事项,在Effective Java这本书中老道我有看到过:
- 自反性:自反性要求对象必须等于自身。换句话说,对于任何非空引用值 x,
x.equals(x)
应该返回true
。这意味着对象与自身比较应该始终返回相等。- 对称性:对称性要求如果两个对象相互比较,那么无论调用顺序如何,结果应该相同。换句话说,如果
x.equals(y)
返回true
,那么y.equals(x)
也应该返回true
。这意味着对象间的比较应该是无序的- 传递性:传递性要求如果一个对象与第二个对象相等,并且第二个对象与第三个对象相等,那么第一个对象必须与第三个对象相等。换句话说,如果
x.equals(y)
和y.equals(z)
都返回true
,那么x.equals(z)
也应该返回true
。这意味着对象之间的比较应该是传递的。- 一致性:一致性要求如果两个对象没有发生变化,那么它们之间的比较结果也不应该发生变化。换句话说,只要对象没有发生变化,多次调用
equals
方法应该始终返回相同的结果,即相等的对象应该始终返回true
。- 非空性:非空性要求任何非空引用值都不应该与
null
相等。换句话说,对于任何非空引用值 x,x.equals(null)
应该始终返回false
。这意味着对象与null
比较应该始终返回不相等。这个原则保证了对象与null
之间的比较不会导致空指针异常,并且保持了equals
方法的一致性。
好了到此二级缓存就说完了,其他的几个Cache实现类我没说,但是相信道友们是可以看懂的,如有不懂可以评论区讨论。
下一节说下和jdbc打交道的 StatementHandler 这个玩意 和 结果处理的 ResultSetHandler 这个玩意后就开始写老道我认为MyBatis中最最最重要的参数解析和绑定,今天摸鱼结束单休的我这周连上9天,补五一的班,好了收工吃饭,饿死了。
意味着对象间的比较应该是无序的
- 传递性:传递性要求如果一个对象与第二个对象相等,并且第二个对象与第三个对象相等,那么第一个对象必须与第三个对象相等。换句话说,如果
x.equals(y)
和y.equals(z)
都返回true
,那么x.equals(z)
也应该返回true
。这意味着对象之间的比较应该是传递的。- 一致性:一致性要求如果两个对象没有发生变化,那么它们之间的比较结果也不应该发生变化。换句话说,只要对象没有发生变化,多次调用
equals
方法应该始终返回相同的结果,即相等的对象应该始终返回true
。- 非空性:非空性要求任何非空引用值都不应该与
null
相等。换句话说,对于任何非空引用值 x,x.equals(null)
应该始终返回false
。这意味着对象与null
比较应该始终返回不相等。这个原则保证了对象与null
之间的比较不会导致空指针异常,并且保持了equals
方法的一致性。
好了到此二级缓存就说完了,其他的几个Cache实现类我没说,但是相信道友们是可以看懂的,如有不懂可以评论区讨论。
下一节说下和jdbc打交道的 StatementHandler 这个玩意 和 结果处理的 ResultSetHandler 这个玩意后就开始写老道我认为MyBatis中最最最重要的参数解析和绑定,今天摸鱼结束单休的我这周连上9天,补五一的班,好了收工吃饭,饿死了。