哈哈哈,终于考完试了,用了大概两天时间肝了这篇文章!!!
关于今天要讲的mybatis缓存机制,其实之前我已经有看过也用过,只不过平常不太留意,最近在看mybatis源码,就来讲一下这个缓存机制

前言

​ 本次分析的代码和数据表在gitee上,地址:https://gitee.com/professor_mai/mybatis_cache_demo

​ 关于这个Mybatis缓存,推荐这篇文章 https://tech.meituan.com/2018/01/19/mybatis-cache.html,下面的内容是基于这篇文章来写的,我写的内容是更偏重于原理级别的。

再次提醒,一定要把上面推荐的文章再来看下面的内容,不然你会很懵逼

一级缓存

​ 在进行数据库查询之前,MyBatis 首先会检查以及缓存中是否有相应的记录,若有的话直接返回即可。一级缓存是数据库的最后一道防护,若一级缓存未命中,查询请求将落到数据库上。一级缓存是在 BaseExecutor 被初始化的。

实验一:开启一级缓存,调用三次相同的查询操作

​ 通过上面文章大致了解了一级缓存后(再次提醒,一定要看上面推荐的文章),可以看看查询一级缓存的逻辑。

​ 经过上图的调用之后,最终是在BaseExecutorquery方法上执行。

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 = 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,我们可以简单的把 CacheKey 看做是一个查询请求的 id

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;
}

​ 在上面代码中,若一级缓存为命中(很明显在我们的实验中,这个实验是参考我们上面这个文章的),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;
}

​ 那执行完之后,就已经存进这个localcache变量里面了,下次我们只需要直接getobject拿就行了。

所以就可以一级缓存的总结如下图

实验二:增加了对数据库的修改操作,验证在一次数据库会话中,如果对数据库发生了修改操作,一级缓存是否会失效

这里重点说一下,为什么一级缓存会失效:

​ 缓存究竟是在哪里拿的?

上面我们说过是从localCache.getObject中拿的,我们再往深一层,会发现调用的是PerpetualCache #getObject方法。

我们可以再看看,插入数据后的localCache发生了什么变化,这是插入前的

插入后,再次查询会发现localCache已经没有了,因为其最大的共享范围就是一个 SqlSession 内部

关于SqlSession,可以简单看看这个图

也可以简单看一下这个调用栈,Executor是不直接暴露接口的,是通过Sqlsession接口的。

可能到这里你还是有点懵,这里再来简单说一下吧,先简单看看这个调用栈

insert语句会调用BaseExecutor#update方法,有朋友就很奇怪,为什么insert语句是调用update方法呢?为什么不是调用insert方法呢?这里简单提一下,答案是:只提供一个 update 方法从实现上完全可行,但是从接口的语义化的角度来说,这样做并不好。一般情况下,使用者觉得 update 接口方法应该仅负责执行 UPDATE 语句,如果它还兼职执行其他的 SQL 语句,会让使用者产生疑惑。对于对外的接口,接口功能越单一,语义越清晰越好。在日常开发中,我们为客户端提供接口时,也应该这样做。

接下来就来看看这个update方法做了什么操作

@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());if (closed) {throw new ExecutorException("Executor was closed.");}// 刷新一级缓存(看到方法名就知道是清空缓存了)clearLocalCache();return doUpdate(ms, parameter);
}@Overridepublic void clearLocalCache() {if (!closed) {localCache.clear();localOutputParameterCache.clear();}}
// 其实在这之前还会先调用CachingExecutor#update方法,看上面调用栈就知道了
public int update(MappedStatement ms, Object parameterObject) throws SQLException {// 刷新二级缓存(这里也会清除掉二级缓存)flushCacheIfRequired(ms);return delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {Cache cache = ms.getCache();if (cache != null && ms.isFlushCacheRequired()) {      tcm.clear(cache);}}

后面会出一篇Mybatis的执行Sql命令的完整流程,估计看完你就不懵了,敬请期待

实验三:开启两个SqlSession,在sqlSession1中查询数据,使一级缓存生效,在sqlSession2中更新数据库,验证一级缓存只在数据库会话内部共享。

通过调试,其实这两个sqlSession是两个不同的sqlSession更新语句,只能清楚sqlSession2的缓存,并不能清除sqlSession1的缓存,所以会出现脏读,所以一级缓存只在数据库会话内部共享。

总结

  1. MyBatis一级缓存的生命周期和SqlSession一致。
  2. MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
  3. MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。这里再来简单看看Statement,STATEMENT级别是一种缓存级别,可以理解为缓存只对当前执行的这一个Statement有效,可以看看BaseExecutor#query方法最后会用到这个判断来判断是不是STATEMENT如果是就会清空缓存!
  4. 我们也可以见到那看一下上面的脏读现象使用STATEMENT级别之后,有没有会出现,很明显是不会出现的,同时,一级缓存也失效了!

二级缓存

​ 在上文中提到的一级缓存中,其最大的共享范围就是一个 SqlSession 内部,如果多个 SqlSession 之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用 CachingExecutor 装饰 Executor ,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示。

​ 二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。

当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。

实验一:测试二级缓存效果,不提交事务,sqlSession1查询完数据后,sqlSession2相同的查询是否会从缓存中获取数据。

在讲结论之前,先简单介绍一下,支持二级缓存的 Executor 的实现类CachingExecutor


private Executor delegate;
// TransactionalCacheManager 对象,支持事务的缓存管理器。因为二级缓存是支持跨 Session 进行共享,此处需要考虑事务,那么,必然需要做到事务提交时,才将当前事务中查询时产生的缓存,同步到二级缓存中。这个功能,就通过 TransactionalCacheManager 来实现。
private TransactionalCacheManager tcm = new TransactionalCacheManager();public CachingExecutor(Executor delegate) {this.delegate = delegate;delegate.setExecutorWrapper(this);
}//里面有个query方法,重点
// CachingExecutor.java
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {// 获得 BoundSql 对象BoundSql boundSql = ms.getBoundSql(parameterObject);// 创建 CacheKey 对象CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);// 查询return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {// <1> 调用 MappedStatement#getCache() 方法,获得 Cache 对象,即当前 MappedStatement 对象的二级缓存。Cache cache = ms.getCache();if (cache != null) { // <2>  如果有 Cache 对象,说明该 MappedStatement 对象,有设置二级缓存// <2.1> 如果需要清空缓存,则进行清空,注意这里清空缓存,只是清空未提交事务之前的缓存,而真正的清空,在事务的提交时。这是为什么呢?还是因为二级缓存是跨 Session 共享缓存,在事务尚未结束时,不能对二级缓存做任何修改flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) { // <2.2>// 暂时忽略,存储过程相关ensureNoOutParams(ms, boundSql);@SuppressWarnings("unchecked")// <2.3> 从二级缓存中,获取结果List<E> list = (List<E>) tcm.getObject(cache, key);if (list == null) {// <2.4.1> 如果不存在,则从数据库中查询list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);// <2.4.2> 缓存结果到二级缓存中tcm.putObject(cache, key, list); // issue #578 and #116}// <2.5> 如果存在,则直接返回结果return list;}}// <3> 不使用缓存,则从数据库中查询,如果没有 Cache 对象,说明该 MappedStatement 对象,未设置二级缓存,则调用 delegate 属性的 #query方法,直接从数据库中查询return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

再看看TransactionalCacheManagerTransactionalCache 管理器

// TransactionalCacheManager.java
/** * Cache 和 TransactionalCache 的映射 */
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

这个类的主要作用如上图所示,就是要维护Cache 和TransactionalCache的关系

TransactionalCache 是怎么创建的呢?答案在 #getTransactionalCache(Cache cache) 方法,代码如下:

// TransactionalCacheManager.java
private TransactionalCache getTransactionalCache(Cache cache) {   return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
  • 优先,从 transactionalCaches 获得 Cache 对象,对应的 TransactionalCache 对象。
  • 如果不存在,则创建一个 TransactionalCache 对象,并添加到 transactionalCaches 中。

#commit() 方法,提交所有 TransactionalCache 。代码如下:

// TransactionalCacheManager.javapublic void commit() {for (TransactionalCache txCache : transactionalCaches.values()) {txCache.commit();}
}
  • 通过调用该方法,TransactionalCache 存储的当前事务的缓存,会同步到其对应的 Cache 对象。

再来看看上面提到的TransactionalCache

// TransactionalCache.java/*** 委托的 Cache 对象。** 实际上,就是二级缓存 Cache 对象。*/
private final Cache delegate;
/*** 提交时,清空 {@link #delegate}** 初始时,该值为 false* 清理后{@link #clear()} 时,该值为 true ,表示持续处于清空状态*/
private boolean clearOnCommit;
/*** 待提交的 KV 映射,在事务被提交前,所有从数据库中查询的结果将缓存在此集合中*/
private final Map<Object, Object> entriesToAddOnCommit;
/*** 查找不到的 KEY 集合, 在事务被提交前,当缓存未命中时,CacheKey 将会被存储在此集合中*/
private final Set<Object> entriesMissedInCache;public TransactionalCache(Cache delegate) {this.delegate = delegate;this.clearOnCommit = false;this.entriesToAddOnCommit = new HashMap<>();this.entriesMissedInCache = new HashSet<>();
}@Override
public Object getObject(Object key) {// <1> 从 delegate 中获取 key 对应的 valueObject object = delegate.getObject(key);// <2> 如果不存在,则添加到 entriesMissedInCache 中,这个操作真的是神操作啊!!!!,看看下面的commit() 和 rollback() 方法if (object == null) {entriesMissedInCache.add(key);}// <3> 如果 clearOnCommit 为 true ,表示处于持续清空状态,则返回 nullif (clearOnCommit) {return null;// <4> 返回 value} else {return object;}
}public void commit() {// <1> 如果 clearOnCommit 为 true ,则清空 delegate 缓存if (clearOnCommit) {delegate.clear();}// 将 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate 中// 调用 flushPendingEntries() 方法,将 entriesToAddOnCommit、entriesMissedInCache 同步到 delegate 中flushPendingEntries();// 重置reset();
}private void flushPendingEntries() {// 将 entriesToAddOnCommit 刷入 delegate 中for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {delegate.putObject(entry.getKey(), entry.getValue());}// 将 entriesMissedInCache 刷入 delegate 中for (Object entry : entriesMissedInCache) {if (!entriesToAddOnCommit.containsKey(entry)) {delegate.putObject(entry, null);}}
}public void rollback() {// <1> 从 delegate 移除出 entriesMissedInCacheunlockMissedEntries();// <2> 重置reset();
}

大致介绍完了上面的类,可以先说一下结论了当sqlsession没有调用commit()方法时,二级缓存并没有起到作用,我们先看看下面的提交了事务之后为什么可以查到缓存说起吧。

看完下面的内容,应该这里就很容易理解了,也就是未提交之前,查询会存在entriesMissedInCache或者entriesToAddOnCommit上,还没有刷回去delegate集合 ,所以就查不到缓存了!!

​ 其实,这里我一直有个疑惑,为什么还要这么麻烦,提交事务之后又要刷一次缓存,为什么不直接用这个entriesMissedInCache或者entriesToAddOnCommit上的缓存呢?

​ 这里参考了一下别人的博客的这张图最终是明白了,这里我把这张图放出来,相信你也会明白

这里如果直接用的话,像上面那样子就会出现脏数据问题。

而把缓存刷回去的话就像下面那样解决了脏读问题

但需要注意的时,MyBatis 缓存事务机制只能解决脏读问题,并不能解决“不可重复读”问题。再回到上图,事务 B 在被提交前进行了三次查询。前两次查询得到的结果为记录 A,最后一次查询得到的结果为 A′。最有一次的查询结果与前两次不同,这就会导致“不可重复读”的问题。MyBatis 的缓存事务机制最高只支持“读已提交”,并不能解决“不可重复读”问题。即使数据库使用了更高的隔离级别解决了这个问题,但因 MyBatis 缓存事务机制级别较低。此时仍然会导致“不可重复读”问题的发生,这个在日常开发中需要注意一下。

-from 田小波的博客

测试二级缓存效果,当提交事务时,sqlSession1查询完数据后,sqlSession2相同的查询是否会从缓存中获取数据。

sqlSession1.close();实验代码中,有这个代码是代表关闭这个Session,然后就会自动提交事务

下面主要分析一下这个方法的调用栈

// DefaultSqlSession
@Override
public void close() {try {// 进去这个方法executor.close(isCommitOrRollbackRequired(false));closeCursors();dirty = false;} finally {ErrorContext.instance().reset();}
}@Overridepublic void close(boolean forceRollback) {try {//issues #499, #524 and #573// 会先判断是不是会滚操作if (forceRollback) { tcm.rollback();} else {// 提交操作tcm.commit();}} finally {delegate.close(forceRollback);}}// 本质:又到了这里了,下面就不用说了,就是把那些缓存刷进去
public void commit() {// <1> 如果 clearOnCommit 为 true ,则清空 delegate 缓存if (clearOnCommit) {delegate.clear();}// 将 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate 中// 调用 flushPendingEntries() 方法,将 entriesToAddOnCommit、entriesMissedInCache 同步到 delegate 中flushPendingEntries();// 重置reset();
}

然后,我们可以看是否可以查询二级缓存成功,结果肯定没问题了,通过这个TransactionalCache#getObject方法拿到缓存

实验3:测试update操作是否会刷新该namespace下的二级缓存。

先来看看调用栈:

看看这个刷新缓存的本质:

// TransactionalCache.java
@Override
public void clear() {//方便下面清空真正的缓存clearOnCommit = true;// 清空事务未提交的缓存entriesToAddOnCommit.clear();
}public void commit() {if (clearOnCommit) {// 当事务提交的时候会清空真正的缓存delegate.clear();}flushPendingEntries();reset();
}

结论很明显了:,在sqlSession3更新数据库,并提交事务后,sqlsession2StudentMapper namespace下的查询走了数据库,没有走Cache

实验四和实验五就不多说了,挺简单的

具有 LRU 策略的缓存 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 方法@Overrideprotected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {boolean tooBig = size() > size;if (tooBig) {// 获取将要被移除缓存项的键值eldestKey = eldest.getKey();}return tooBig;}};}@Overridepublic void putObject(Object key, Object value) {// 存储缓存项delegate.putObject(key, value);cycleKeyList(key);}@Overridepublic Object getObject(Object key) {// 刷新 key 在 keyMap 中的位置keyMap.get(key);// 从被装饰类中获取相应缓存项return delegate.getObject(key);}@Overridepublic Object removeObject(Object key) {// 从被装饰类中移除相应的缓存项return delegate.removeObject(key);}@Overridepublic 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;}}// 省略部分代码
}

上面我们用到的缓存都是可保证线程安全的缓存 SynchronizedCache,那关于这个LRU缓存就简单拓展一下JDK的LinkedHashMap

LinkedHashMap

LinkedHashMap ,在 HashMap 的基础之上,提供了顺序访问的特性:

  1. 按照 key-value 的插入顺序进行访问

  2. 按照 key-value 的访问顺序进行访问

谈谈LinkedHashMap 的Entry

下面还是通过一个具体的案例来理解这个类的作用吧

基于 LinkedHashMap 实现LRU缓存

关于LRU就不多说了,可以我写过的内存管理,那里有简单提到过!

public class SimpleCache<K, V> extends LinkedHashMap<K, V> {private static final int MAX_NODE_NUM = 100;private int limit;public SimpleCache(){this(MAX_NODE_NUM);}public SimpleCache(int limit) {super(limit, 0.75f, true);this.limit = limit;}public V save(K key, V val){return put(key, val);}public V getOne(K key) {return get(key);}public boolean exists(K key) {return containsKey(key);}/*** 判断节点数是否超限* @param eldest* @return 超限返回 true,否则返回 false*/@Overrideprotected boolean removeEldestEntry(Map.Entry<K, V> eldest) {return size() > limit;}}// 测试类public class SimpleCacheTest {@Testpublic void test() throws Exception {SimpleCache<Integer, Integer> cache = new SimpleCache(3);for (int i = 0; i < 10; i++) {cache.save(i, i * i);}System.out.println("插入10个键值对后,缓存内容:");System.out.println(cache + "\n");System.out.println("访问键值为7的节点后,缓存内容:");cache.getOne(7);System.out.println(cache + "\n");System.out.println("插入键值为1的键值对后,缓存内容:");cache.save(1, 1);System.out.println(cache);}
}

我们不妨可以先看看结果,到底这个LinkedHashMap有什么功能那么强大

​ 在测试代码中,设定缓存大小为3。在向缓存中插入10个键值对后,只有最后3个被保存下来了,其他的都被移除了。然后通过访问键值为7的节点,使得该节点被移到双向链表的最后位置。当我们再次插入一个键值对时,键值为7的节点就不会被移除。

我们可以不妨debug进去源码看看它是怎么插入和顺序是怎么调整的!!

  1. 首先是看看LinkedHashMap的构造函数

    public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) {super(initialCapacity, loadFactor);// 增加了一个标志顺序的变量this.accessOrder = accessOrder;
    }
    
  2. 然后是这个save方法,内部调用的还是hashmap的putVal方法

    // LinkedHashMap并没有覆写该方法,但在 HashMap 中,put 方法插入的是 HashMap 内部类 Node 类型的节点,该类型的节点并不具备与 LinkedHashMap 内部类 Entry 及其子类型节点组成链表的能力
    public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
    }
    

    调用过程:

    1. HashMapputVal方法中先调用newNode,而newNode方法被LinkedHashMap覆写,最终调用的是LinkedHashMapnewNode方法,我们来看看这个方法

      Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {LinkedHashMap.Entry<K,V> p =new LinkedHashMap.Entry<K,V>(hash, key, value, e);// 这里又调用了linkNodeLast方法将 Entry 接在双向链表的尾部,实现了双向链表的建立linkNodeLast(p);return p;
      }private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {LinkedHashMap.Entry<K,V> last = tail;tail = p;if (last == null)head = p;else {p.before = last;last.after = p;}}
      

    ​ 总结一下这个调用流程就是:

  3. 再来看看getOne方法是怎么访问一个节点的。默认情况下,LinkedHashMap是按插入顺序维护链表。不过我们可以在初始化 LinkedHashMap,指定 accessOrder 参数为 true,即可让它按访问顺序维护链表。

    // LinkedHashMap 覆写了get方法
    public V get(Object key) {Node<K,V> e;if ((e = getNode(hash(key), key)) == null)return null;// 默认情况是trueif (accessOrder)afterNodeAccess(e);return e.value;}// 只需要将这些方法访问的节点移动到链表的尾部即可
    void afterNodeAccess(Node<K,V> e) { // move node to lastLinkedHashMap.Entry<K,V> last;if (accessOrder && (last = tail) != e) {LinkedHashMap.Entry<K,V> p =(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;p.after = null;if (b == null)head = a;elseb.after = a;if (a != null)a.before = b;elselast = b;if (last == null)head = p;else {p.before = last;last.after = p;}tail = p;++modCount;}
    }
    
  4. 最后再看看这个删除过程,我们在这里调用了cache.save(1, 1);,看看插入的过程会怎么淘汰里面的节点

    可以看看调用栈:前面的过程都是一样的,唯独这里的处理有点不同,这里调用了LinkedHashMap实现的afterNodeInsertion方法

    void afterNodeInsertion(boolean evict) { // possibly remove eldestLinkedHashMap.Entry<K,V> first;// 根据条件判断是否移除最近最少被访问的节点if (evict && (first = head) != null && removeEldestEntry(first)) {K key = first.key;removeNode(hash(key), key, null, false, true);}
    }// 移除最近最少被访问条件之一,通过覆盖此方法可实现不同策略的缓存
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {return false;
    }/*** 自己实现的策略,判断节点数是否超限* @param eldest* @return 超限返回 true,否则返回 false*/@Overrideprotected boolean removeEldestEntry(Map.Entry<K, V> eldest) {return size() > limit;}
    // 很明显这里已经超过了限制的数量,就要调用HashMap#removeNode方法// HashMap 中实现
    final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {Node<K,V>[] tab; Node<K,V> p; int n, index;if ((tab = table) != null && (n = tab.length) > 0 &&(p = tab[index = (n - 1) & hash]) != null) {Node<K,V> node = null, e; K k; V v;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))node = p;else if ((e = p.next) != null) {if (p instanceof TreeNode) {...}else {// 遍历单链表,寻找要删除的节点,并赋值给 node 变量do {if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {node = e;break;}p = e;} while ((e = e.next) != null);}}if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {if (node instanceof TreeNode) {...}// 将要删除的节点从单链表中移除else if (node == p)tab[index] = node.next;elsep.next = node.next;++modCount;--size;afterNodeRemoval(node);    // 调用删除回调方法进行后续操作return node;}}return null;
    }// LinkedHashMap 中覆写
    void afterNodeRemoval(Node<K,V> e) { // unlinkLinkedHashMap.Entry<K,V> p =(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;// 将 p 节点的前驱后后继引用置空p.before = p.after = null;// b 为 null,表明 p 是头节点if (b == null)head = a;elseb.after = a;// a 为 null,表明 p 是尾节点if (a == null)tail = b;elsea.before = b;
    }
    

    删除的过程并不复杂,上面这么多代码其实就做了三件事:

    1. 根据 hash 定位到桶位置
    2. 遍历链表或调用红黑树相关的删除方法
    3. 从 LinkedHashMap 维护的双链表中移除要删除的节点

个人唠叨

​ 大家应该能猜到我这期个人唠叨要说什么了吧!没错,就是关于本次操作系统的期末考试,想想也很久没有这种做不完试卷的感觉了,回想起来,这种感觉也要追溯到两年前的高考了,当时做数学就是这种感觉了,做题的感觉是真的美妙,看起来都会,写上去就会出现各种各样的错误!!!好了,废话不多说了,马上进入主题!

谈谈我对这次考试的看法

​ 相信参加了本次考试的同学都很清楚这次考试的难度是什么样的?以我来看,本次考试确实是正常难度,题量相对来说是偏多了,老师出题的目的很简单,就是想通过这份卷子来巩固我们一个学期下来学习到的知识点,所以,题量是会多了的!

​ 其实我是很赞同他的这种想法的,有些知识点确实是你糊弄地学会了,你并不是真正地学会,而这些必修的知识点是非常重要的,一定不能随便搞!这也让我想起超哥,他当初也是这样子,为了是我们可以学得更好。

考完之后,我才发现我是真的菜,这也许是少刷题的原因吧,导致每道题有思路,但是写得都好慢,生怕会出错的样子,想不到时间也越来越紧迫了,最后导致自己的心态都崩了!嗯,确实崩了,可能是自从上大学之后太久没遇到过这种情况,导致出一点点差错,心态就容易崩。直到现在,心态才开始慢慢恢复上来!

我们应该抱着一种什么心态来看这种考试呢

​ 回想一下,当初我高考完知道分数后,知道自己彻底完蛋了,一直持续了这种颓废的状态好久好久好久。。

​ 回到这次考试上,如果你也像我这样,考崩了,我可以教你一个方法,那就是你要尝试把这个结果推迟一年,一年不行推迟五年,试想一下,我们挂科了,推迟一年之后,这个结果会影响你的什么?嗯,影响只不过是我要去补考,我拿不到奖学金,推迟五年,对我影响可以说是没了的,因为什么拿不到奖学金,补考这些东西对你将来的工作有影响吗?没有了,所以,我根本就不用焦虑,改过的生活还是照常过,该学习还是要继续学习

​ 除了这个方法之外,还教大家一个方法,比如:我的目标是进大厂,那考试挂科对你进大厂有没影响呢?哈哈哈哈,这里就以我个人看法来谈谈有没影响,不喜勿喷,我觉得是根本没有影响的,你去面试的时候难道真的会认真看你简历上的成绩单吗(简历上甚至是不会放成绩单的,985 211 除外),难道真的会因为你挂过科不会给你机会吗?是不会的

谈谈笔试

​ 我们都知道,很多大厂面试之前都是要进行笔试的,那我上面说到的那种考试的方式其实是和笔试差不多的,所以,我们不能排斥这个考试,每一次考试都要认真准备,那其实这个笔试跟我们的应试考试是有点差别的,笔试是为了筛选人才(好像这一次操作系统的考试,有筛选人才的意思,因为这个题量,你没有一定的熟练程度是没办法做完并且拿高分的),而我们的应试考试只是一个形式而已!!

谈谈我对挂科的看法

​ 好了,这里又引申到另一个主题了,我对挂科的看法,很多人都说“没有挂过科的大学是不完整的!”,哈哈哈哈,我是完全否认这句话的,能不挂科一定不要挂科,真的,因为后续的工作是很麻烦的!当然啦,那什么是能不挂科尽量不挂科?就是你有认真在学习,有认真刷题,认真弄懂每一个知识点,但是老师就是要为难你,那你没办法,那说回这次考试,大家都觉得自己会挂科,那自己平时真的有弄懂每一个知识点吗?真的有认真刷题吗?如果你回答是,那我无话可说,挂科只能怪老天,不能怪你,但我相信绝大多数朋友(包括我在内)都不怎么会认真对待这门课,因为这门课又枯燥,又难啃,别说还另外拿时间出来刷题了!所以,挂科了也不能怨别人了,只能怪自己并没有好好重视这门课!

​ 那挂了科真的代表你不行了吗?我觉得,考试考得好只能说明你做题真的很牛逼!但是你试想一下,你真正干活的时候,真的是像考试做题那样吗?那其实并不是这样的,我们会发现很多成绩特别特别优异的同学,他们的编程能力其实并不好(我觉得我大一的时候就是这样一种人,过分追求分数了!!)。在大学的时候,那些编程能力最强的往往是那些成绩比较一般的。

为什么会这样呢?

​ 我觉得主要是一个思维的转变问题。很多人学习编程的时候,总是想着我要把这个 API 、把这种题型记下来,把这个库的用法记下来。这样学习,导致的结果只有一个那就是你会很难受!因为,这些根本不是要死记硬背的东西啊!真还当这是上课考试啊!**你要从如何用你学的东西来解决实际编程问题出发,站在做一个实际的项目的角度来学习。**所以,我认为做项目,写课程设计报告真的是一种很好的学习方式!

好了,总结一下,我们不要把学习编程还当做一场应试考试来看了!!!!

​ 花了近一个小时来写这个个人唠叨,目的不为啥,就是为了想让你们能和我一样尽快建立一种对考试的正确认知!!不要让挂科覆盖了你的整个人生!最后,希望大家不要以应试考试的方式学习编程、多实践、造轮子是一种特别能够提高自己系统编程能力的手段等等。说了这么多,如果你没有将这些学习编程的正确姿势用到自己平时学习中的话,这篇文章对你的帮助可能非常有限。

​ 接下来这个暑假,希望大家能一起加油,一起变强,共勉!!!

[Professor麦]深入剖析Mybatis缓存机制相关推荐

  1. MyBatis复习笔记6:MyBatis缓存机制

    MyBatis缓存机制 MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制.缓存可以极大的提升查询效率. MyBatis系统中默认定义了两级缓存. 一级缓存和二级缓存. 默认情 ...

  2. 【转】MyBatis缓存机制

    转载:https://blog.csdn.net/bjweimengshu/article/details/79988252. 本文转载自公众号 美团技术点评 前言 MyBatis是常见的Java数据 ...

  3. MyBatis缓存机制之一级缓存

    MyBatis缓存机制之一级缓存 前言 MyBatis内部封装了JDBC,简化了加载驱动.创建连接.创建statement等繁杂的过程,是我们常见的持久性框架.缓存是在计算机内存中保存的临时数据,读取 ...

  4. 13.MyBatis缓存机制

    13.MyBatis缓存机制 1. 为什么使用缓存? 当用户频繁查询某些固定的数据时,第一次将这些数据从数据库中查询出来,保存在缓存中.当用户再次查询这些数据时,不用再通过数据库查询,而是去缓存里面查 ...

  5. 《深入理解mybatis原理》 MyBatis缓存机制的设计与实现

    本文主要讲解MyBatis非常棒的缓存机制的设计原理,给读者们介绍一下MyBatis的缓存机制的轮廓,然后会分别针对缓存机制中的方方面面展开讨论. MyBatis将数据缓存设计成两级结构,分为一级缓存 ...

  6. 五、mybatis缓存机制

    文章目录 五.缓存机制 一级缓存(默认开启) 二级缓存 注意 缓存顺序 mybatis基础教程[5小时36讲全套] 五.缓存机制 一级缓存(默认开启) ​ 同一个sqlsession会话对象,执行同样 ...

  7. MyBatis缓存机制学习

    与Hibernate一样,MyBatis 同样提供了一级缓存和二级缓存的支持. 一级缓存: 基于PerpetualCache 的 HashMap本地缓存,其存储作用域为 Session,当 Sessi ...

  8. Mybatis缓存机制理解及配置

    2019独角兽企业重金招聘Python工程师标准>>> 1.     Ehcache EHCache是来自sourceforge(http://ehcache.sourceforge ...

  9. mybatis缓存机制

    文章目录 缓存介绍 一级缓存 二级缓存 二级缓存使用步骤 二级缓存和一级缓存的区别: 缓存介绍 缓存主要是对查询起作用,减轻数据库的压力,提高数据库的性能 mybatis中提供了一级缓存.二级缓存 一 ...

最新文章

  1. R构建列联表(Contingency Table or crosstabs)
  2. 【字符串哈希】【哈希表】Aizu - 1370 - Hidden Anagrams
  3. 软件工程—团队作业1
  4. 【APICloud系列|32】iOS 上架去除Icon图像中的alpha通道或透明度
  5. linux dup跨进程使用,linuxC多进程通讯---无名管道dup
  6. java studentmanager_StudentManager.java
  7. BERT源码分析PART I
  8. hdu 2046 骨牌铺方格
  9. excel随机抽取_Python自制班级点名器让Excel表格用起来
  10. canvas-画图改进版
  11. 输出保留3位小数的浮点数
  12. 不要迷信微服务,微服务就是个传说
  13. background 渐变背景
  14. 人工智能能否在翻译中胜过人类?
  15. 网站图片挂马检测及PHP与python的图片文件恶意代码检测对比
  16. c/c++源码学习和实践资源,万丈高楼平地起
  17. CUDA加速计算的基础C/C++
  18. 来到传统行业做程序员,从准备提桶跑路到引领技术风潮?背景
  19. (批处理学习)句柄备份——个人见解之“>nul 3>nul“——记录学习过程(详细)
  20. 3D打印技术新进展,正带来哪些产业新机会?

热门文章

  1. ecshop开发的外贸网站欣赏
  2. 架构搜索文献笔记(11):《ATOMNAS: FINE-GRAINED END-TO-END NEURAL ARCHITECTURE SEARCH》
  3. 通知 | 国内运营商递归DNS解析出现问题
  4. 自己的想法、书的封面、扉页和版权页
  5. PermitRootLogin yes无效问题
  6. 数字水印算法matlab源程序 matlab版数字水印算法 /DCT/DWT/LSB/HVS/W-SVD数字水印源码 数字水印的嵌入和提取 W-SVD数字水印实现
  7. 阿里高级面试题 2019
  8. 【学习笔记】设计模式-原型模式/克隆模式(Prototype)
  9. HW4:Unity3D中游戏场景的创建
  10. 女程序猿的苦恼:“26岁后,分手对我来说不是件容易的事情”