@万字长文 | MyBatis 缓存到底

回复“面试”获取全套面试资料

在 Web 应用中,缓存是必不可少的组件。通常我们都会用 Redis 或 memcached 等缓存中间件,拦截大量奔向数据库的请求,减轻数据库压力。作为一个重要的组件,MyBatis 自然也在内部提供了相应的支持。通过在框架层面增加缓存功能,可减轻数据库的压力,同时又可以提升查询速度,可谓一举两得。MyBatis 缓存结构由一级缓存和二级缓存构成,这两级缓存均是使用 Cache 接口的实现类。因此,在接下里的章节中,我将首先会向大家介绍 Cache 几种实现类的源码,然后再分析一级和二级缓存的实现。

本文主要内容:

Mybatis缓存体系结构

Mybatis跟缓存相关的类都在cache包目录下,在前面的文章中我们也提过,今天才来详细说说。其中有一个顶层接口Cache,并且只有一个默认的实现类PerpetualCache。

下面是Cached的类图:

既然PerpetualCache是默认实现类,那么我们就从他下手。

PerpetualCache

PerpetualCache这个对象会创建,所以这个叫做基础缓存。但是缓存又可以有很多额外的功能,比如说:回收策略、日志记录、定时刷新等等,如果需要的话,就可以在基础缓存上加上这些功能,如果不喜欢就不加。这里是不是想到了一种设计模式-----装饰器设计模式。PerpetualCache 相当于装饰模式中的 ConcreteComponent。

装饰器模式是指在不改变原有对象的基础之上,将功能附加到对象上,提供了比继承更有弹性的替换方案,即扩展原有对象的功能。

除了缓存之外,Mybatis也定义很多的装饰器,同样实现了Cache接口,通过这些装饰器可以额外实现很多功能。

这些缓存是怎么分类的呢?

所有的缓存可以大体归为三类:基本类缓存、淘汰算法缓存、装饰器缓存。

下面把每个缓存进行详细说明和对比:

缓存实现类源码

PerpetualCache源码

PerpetualCache 是一个具有基本功能的缓存类,内部使用了 HashMap 实现缓存功能。它的源码如下:

    public class PerpetualCache implements Cache {
    
      private final String id;
      //使用Map作为缓存
      private Map<Object, Object> cache = new HashMap<>();
    
      public PerpetualCache(String id) {
        this.id = id;
      }
    
      @Override
      public String getId() {
        return id;
      }
    
      @Override
      public int getSize() {
        return cache.size();
      }
      // 存储键值对到 HashMap
      @Override
      public void putObject(Object key, Object value) {
        cache.put(key, value);
      }
      // 查找缓存项
      @Override
      public Object getObject(Object key) {
        return cache.get(key);
      }
      // 移除缓存项
      @Override
      public Object removeObject(Object key) {
        return cache.remove(key);
      }
      //清空缓存
      @Override
      public void clear() {
        cache.clear();
      }
       //部分代码省略
    }

上面是 PerpetualCache 的全部代码,也就是所谓的基本缓存,很简单。接下来,我们通过装饰类对该类进行装饰,使其功能变的丰富起来。

LruCache

LruCache,顾名思义,是一种具有 LRU(Least recently used,最近最少使用)算法的缓存实现类。

除此之外,MyBatis 还提供了具有 FIFO 策略的缓存 FifoCache。不过并未提供 LFU (Least Frequently Used ,最近最少使用算法)缓存,也是一种常见的缓存算法 ,如果大家有兴趣,可以自行拓展。

接下来,我们来看一下 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 方法
                @Override
                protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
                    boolean tooBig = size() > size;
                    if (tooBig) {
                        // 获取将要被移除缓存项的键值
                        eldestKey = eldest.getKey();
                    }
                    return tooBig;
                }
            };
        }
    
        @Override
        public void putObject(Object key, Object value) {
            // 存储缓存项
            delegate.putObject(key, value);
            cycleKeyList(key);
        }
    
        @Override
        public Object getObject(Object key) {
            // 刷新 key 在 keyMap 中的位置
            keyMap.get(key);
            // 从被装饰类中获取相应缓存项
            return delegate.getObject(key);
        }
    
        @Override
        public Object removeObject(Object key) {
            // 从被装饰类中移除相应的缓存项
            return delegate.removeObject(key);
        }
        //清空缓存
        @Override
        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 方法,移除相应的缓存项目。

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>();
        }
    
        @Override
        public void putObject(Object key, Object value) {
            try {
                // 存储缓存项
                delegate.putObject(key, value);
            } finally {
                // 释放锁
                releaseLock(key);
            }
        }
    
        @Override
        public Object getObject(Object key) {
            // 请        // 请求锁
            acquireLock(key);
            Object value = delegate.getObject(key);
            // 若缓存命中,则释放锁。需要注意的是,未命中则不释放锁
            if (value != null) {
                // 释放锁
                releaseLock(key);
            }
            return value;
        }
    
        @Override
        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 > ?

d > 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;
    // 乘子,默认为37
    private 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 是恒定不变的 ,其他变量将在更新操作中被修改。

下面看一下更新操作的代码。

    /** 每当执行更新操作时,表示有新的影响因子参与计算 */
    public void update(Object object) {
            int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
        // 自增 count
        count++;
        // 计算校验和
        checksum += baseHashCode;
        // 更新 baseHashCode
        baseHashCode *= count;
    
        // 计算 hashCode
        hashcode = multiplier * hashcode + baseHashCode;
    
        // 保存影响因子
        updateList.add(object);
    }

当不断有新的影响因子参与计算时,hashcode 和 checksum 将会变得愈发复杂和随机。这样可降低冲突率,使 CacheKey 可在缓存中更均匀的分布。CacheKey 最终要作为键存入 HashMap,因此它需要覆盖 equals 和 hashCode 方法。

下面我们来看一下这两个方法的实现。

public boolean equals(Object object) {
    // 检测是否为同一个对象
    if (this == object) {
        return true;
    }
    // 检测 object 是否为 CacheKey
    if (!(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 变量即可。

关于 CacheKey 就先分析到这,CacheKey 在一二级缓存中会被用到,接下来还会看到它的身影。

好吧,终于把源码缓存实现类的源码拔完了。

下面我们在来说说一级缓存和二级缓存。

一级缓存

主要内容:

一级缓存也叫本地缓存(LocalCache),Mybatis的一级缓存是会话级别(SqlSession)层面进行缓存的。Mybatis的一级缓存是默认开启的。我们开发项目中不需要做任何配置,但是如果想关闭一级缓存,可以使用localCacheScopde=statement来关闭。

如何关闭一级缓存呢?

在BaseExecutor的中,请看下面代码:

为什么说是SqlSession层面缓存?

就是一级缓存的生命周期和一个SqlSession对象的生命周期一样。

下面这段中,就会使用到一级缓存。

SqlSession sqlSession1 = sqlSessionFactory.openSession();
User user1 = sqlSession1.selectOne("com.tian.mybatis.mapper.UserMapper.selectUserById", 1);
User user2 = sqlSession1.selectOne("com.tian.mybatis.mapper.UserMapper.selectUserById", 1);

结果输出:

用两张图来总结:

第一次:查数据库,放入到缓存中。

第二次:直接从缓存中获取。

下面这段代码中就使用不到缓存

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
sqlSession = sqlSessionFactory.openSession();
sqlSession1 = sqlSessionFactory.openSession();
    
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
    
System.out.println("第一次查询");
System.out.println(userMapper.selectById(1));
    
User user = new User();
user.setUserName("tian111");
user.setId(1);
userMapper1.updateAuthorIfNecessary(user);
System.out.println("第二次查询");
System.out.println(userMapper.selectById(1));

输出结果:

用三张图来总结:

第一次查询:sqlSession1查询数据库,放入到缓存中。

更新:sqlSession2进行更新,注意这里写入的是sqlSession自己的本地缓存。

第二次查询:sqlSession1第二次查询。

记住是一级缓存只能是同一个SqlSession对象就行了。

一级缓存维护在哪里的?

既然一级缓存的生命周期和SqlSession一致,那么我们可以猜想,这个缓存是不是就维护在SqlSession中呢?

SqlSession的默认实现类DefaultSqlSession,在DefaultSqlSession中只有两个属性可能存放缓存:

private final Configuration configuration;
private final Executor executor;

configuration是全局的,肯定不会放缓存。

那就只能把希望寄托于Executor了。由于Executor是个接口,我们可以看看他的实现类:

另外这里有个BaseExecutor。有各类也得瞄瞄。一看居然有东西。

public abstract class BaseExecutor implements Executor {
  private static final Log log = LogFactory.getLog(BaseExecutor.class);
  protected Transaction transaction;
  protected Executor wrapper;
  protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
  //熟悉的家伙,基本缓存
  protected PerpetualCache localCache;
  protected PerpetualCache localOutputParameterCache;
  protected Configuration configuration;
    
  protected int queryStack;
  private boolean closed;
    
  protected BaseExecutor(Configuration configuration, Transaction transaction) {
    this.transaction = transaction;
    this.deferredLoads = new ConcurrentLinkedQueue<>();
    this.localCache = new PerpetualCache("LocalCache");
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    this.closed = false;
    this.configuration = configuration;
    this.wrapper = this;
  }
    
}

再看看BaseExecutor类图:

所以这就证明了,这个缓存是维护在SqlSession里。

一级缓存什么时候被清空?

在执行update、insert、delete、flushCache="true"、commit、rollback、LocalCacheScope.STATEMENT等情况下,一级缓存就都会被清空。

@Override
public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
}

update时,一级缓存会被清空。delete和insert都是调用这个update。可以从SqlSession的insert、update、delete方法跟踪。

LocalCacheScope.STATEMENT时,一级缓存会被清空。在BaseExecutor里的query方法中:

事务提交回滚时,一级缓存会被清空。

flushCache="true"时,一级缓存会被清空。

一级缓存key是什么?

下面就是一级缓存key的创建过程

@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(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
      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;
  }

id:com.tian.mybatis.mapper.UserMapper.selectById

key的生成策略:id + offset + limit + sql + param value + environment id,这些值都相同,生成的key就相同。

示例:

一级缓存总结

一级缓存的生命周期和SqlSession对象的生命周期一致。所以缓存维护在SqlSession中的属性executor里。

一级缓存默认开启。可以通过修改配置项把一级缓存关掉。

清空一级缓存的方式有:

  • update、insert、delete
  • flushCache="true"
  • commit、rollback
  • LocalCacheScope.STATEMENT

二级缓存

主要内容:

二级缓存构建在一级缓存之上,在收到查询请求时,MyBatis 首先会查询二级缓存。若二级缓存未命中,再去查询一级缓存。与一级缓存不同,二级缓存和具体的命名空间绑定,一级缓存则是和 SqlSession 绑定。

在按照 MyBatis 规范使用 SqlSession 的情况下,一级缓存不存在并发问题。二级缓存则不然,二级缓存可在多个命名空间间共享。这种情况下,会存在并发问题,因此需要针对性去处理。除了并发问题,二级缓存还存在事务问题。

二级缓存如何开启?

配置项

<configuration>
  <settings>
    <setting name="cacheEnabled" value="true|false" />
  </settings>
</configuration>

cacheEnabled=true表示二级缓存可用,但是要开启话,需要在Mapper.xml内配置。

<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
或者 简单方式
<cache/>

对配置项属性说明:

  • flushInterval="60000",间隔60秒清空缓存,这个间隔60秒,是被动触发的,而不是定时器轮询的。
  • size=512,表示队列最大512个长度,大于则移除队列最前面的元素,这里的长度指的是CacheKey的个数,默认为1024。
  • readOnly="true",表示任何获取对象的操作,都将返回同一实例对象。如果readOnly="false",则每次返回该对象的拷贝对象,简单说就是序列化复制一份返回。
  • eviction:缓存会使用默认的Least Recently Used(LRU,最近最少使用的)算法来收回。FIFO:First In First Out先进先出队列。

在Configuration类的newExecutor方法中是否开启二级缓存

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
      //是否开启二级缓存
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

二级缓存通过CachingExecutor来实现的,原理是缓存里存在,就返回,不存在就调用Executor ,如果一级缓存未关闭,则先查一级缓存,不存在,再到数据库中查询。

下面使用一张图来表示:

下面是源码:

@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 {
    // 调用 MappedStatement#getCache() 方法,获得 Cache 对象,
    //即当前 MappedStatement 对象的二级缓存。
    Cache cache = ms.getCache();
    if (cache != null) { // <2> 
        // 如果需要清空缓存,则进行清空
        flushCacheIfRequired(ms);
        //当 MappedStatement#isUseCache() 方法,返回 true 时,才使用二级缓存。默认开启。   
        //可通过@Options(useCache = false) 或 <select useCache="false"> 方法,关闭。
        if (ms.isUseCache() && resultHandler == null) { // <2.2>
            // 暂时忽略,存储过程相关
            ensureNoOutParams(ms, boundSql);
            @SuppressWarnings("unchecked")
            //从二级缓存中,获取结果
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) {
                // 如果不存在,则从数据库中查询
                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);
}

二级缓存key是如何生成的?

也是使用的是BaseExecutor类中的createCacheKey方法生成的,所以二级缓存key和一级缓存生成规则是一样的。

二级缓存范围

二级缓存有一个非常重要的空间划分策略:

namespace="com.tian.mybatis.mappers.UserMapper"

namespace="com.tian.mybatis.mappers.RoleMapper"

即,按照namespace划分,同一个namespace,同一个Cache空间,不同的namespace,不同的Cache空间。

比如:

在这个namespace下的二级缓存是同一个。

二级缓存什么时候会被清空?

每当执行insert、update、delete,flushCache=true时,二级缓存都会被清空。

事务不提交,二级缓存不生效?

SqlSession sqlSession = sqlSessionFactory.openSession();
System.out.println("第一次查询"); 
User user = sqlSession.selectOne("com.tian.mybatis.mapper.UserMapper.selectById", 1);
System.out.println(user);
    
//sqlSession.commit();
                
SqlSession  sqlSession1 = sqlSessionFactory.openSession();
System.out.println("第二次查询");
User  user2 = sqlSession1.selectOne("com.tian.mybatis.mapper.UserMapper.selectById", 1);
System.out.println(user2);

因为二级缓存使用的是TransactionalCaheManager(tcm)来管理的,最后又调用了TranscatinalCache的getObject()、putObject()、commit方法。

TransactionalCache里面又持有真正的Cache对象,比如:经过层层装饰的PrepetualCache。

在putObject的时候,只是添加到entriesToAddOnCommit里面。

//TransactionalCache类中 
@Override
public void putObject(Object key, Object object) {
    // 暂存 KV 到 entriesToAddOnCommit 中
    entriesToAddOnCommit.put(key, object);
}

只有conmit方法被调用的时候,才会调用flushPendingEntries方法,真正写入到缓存里。DefaultSqlSession调用commit方法的时候就会调到这个commit方法。

//TransactionalCache类中   
public void commit() {
    //如果 clearOnCommit 为 true ,则清空 delegate 缓存
    if (clearOnCommit) {
      delegate.clear();
    }
    // 将 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);
      }
    }
}
private void reset() {
    // 重置 clearOnCommit 为 false
    clearOnCommit = false;
    // 清空 entriesToAddOnCommit、entriesMissedInCache
    entriesToAddOnCommit.clear();
    entriesMissedInCache.clear();
}

为什么增删该操作会清空二级缓存呢?

因为在CachingExecutor的update方法中

@Override
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();
    // 是否需要清空缓存
    //通过 @Options(flushCache = Options.FlushCachePolicy.TRUE) 或 <select flushCache="true"> 方式,
    //开启需要清空缓存。
    if (cache != null && ms.isFlushCacheRequired()) {
        //调用 TransactionalCache#clear() 方法,清空缓存。
        //注意,此时清空的仅仅,当前事务中查询数据产生的缓存。
        //而真正的清空,在事务的提交时。这是为什么呢?
        //还是因为二级缓存是跨 Session 共享缓存,在事务尚未结束时,不能对二级缓存做任何修改。
        tcm.clear(cache);
    }
}

如何实现多个namespace的缓存共享?

关于多个namespace的缓存共享的问题,可以使用来解决。

比如:

<cache-ref namespace="com.tian.mybatis.mapper.RoleMapper"

cache-ref代表引用别名的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。在关联的表比较少或者按照业务可以对表进行分组的时候可以使用。

「注意」:在这种情况下,多个mapper的操作都会引起缓存刷新,所以这里的缓存的意义已经不是很大了。

如果将第三方缓存作为二级缓存?

Mybatis除了自带的二级换以外,我们还可以通过是想Cache接口来自定义二级缓存。

添加依赖

     <dependency>
         <groupId>org.mybatis.caches</groupId>
         <artifactId>mybatis-redis</artifactId>
         <version>1.0.0-beta2</version>
    </dependency>

redis基础配置项

host=127.0.0.1
port=6379
connectionTimeOut=5000
soTimeout=5000
datebase=0

在我们的UserMapper.xml中添加

<cache type="org.mybatis.caches.redis.RedisCache"
       eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

RedisCache类图,Cache就是Mybatis中缓存的顶层接口。

二级缓存应用场景

对于访问多的查询请求且用户对查询结果实时性要求不高,此时可采用mybatis二级缓存技术降低数据库访问量,提高访问速度,业务场景比如:耗时较高的统计分析sql、电话账单查询sql等。

缓存查询顺序

先查二级缓存,不存在则坚持一级缓存是否关闭,没关闭,则再查一级缓存,还不存在,最后查询数据库。

二级缓存总结

二级缓存开启方式有两步:

第一步:在全局配置中添加配置

<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

第二步,在Mapper中添加配置

<cache type="org.mybatis.caches.redis.RedisCache"
           eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

二级换是默认开启的,但是针对每一个Mapper的二级缓存是需要手动开启的。

二级缓存的key和一级缓存的key是一样的。

每当执行insert、update、delete,flushCache=true时,二级缓存都会被清空。

我们可以继承第三方缓存来作为Mybatis的二级缓存。

总结

本文先从整体分析了Mybatis的缓存体系结构,然后就是对每个缓存实现类的源码进行分析,有详细的讲述一级缓存和二级缓存,如何开启关闭,缓存的范围的说明,缓存key是如何生成的,对应缓存是什么时候会被清空,先走二级缓存在走以及缓存,二级缓存使用第三方缓存。

参考:http://www.tianxiaobo.com/2018/08/25/

推荐阅读

掌握Mybatis动态映射,我可是下了功夫的
《写给大忙人看的JAVA核心技术》.pdf
美女面试官问我:能说几个常见的Linux性能调优命令吗?
(0)

相关推荐

  • temp1

    补充:多对多关联关系 1.如果不使用中间表 在某一个表中,使用一个字段保存多个"外键"值,这将导致无法使用SQL语句进行关联查询. 2.使用中间表 这样就可以使用SQL进行关联查询 ...

  • Mybatis缓存及注意点

    一级缓存 一级缓存是SqlSession级别的缓存.在操作数据库时需要构造sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据.不同的sqlSession之间的缓存数据区 ...

  • Java学习-Mybatis

    Mybatis Maven依赖: <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis --> <dependenc ...

  • MyBatis学习06(动态SQL和缓存)

    10.动态SQL 10.1 什么是动态SQL 动态SQL指的是根据不同的查询条件 , 生成不同的Sql语句. 官网描述: MyBatis 的强大特性之一便是它的动态 SQL.如果你有使用 JDBC 或 ...

  • SSM MyBatis二级缓存和第三方Ehchache配置

    ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 二级缓存 Mybatis中,默认二级缓存是开启的.可以关闭. 一级缓存开启的.可以被卸载吗?不可以的.一级缓存 ...

  • 万字长文聊缓存(下)- 应用级缓存

    摘要 在上一篇文章 万字长文聊缓存(上)中,我们主要如何围绕着Http做缓存优化,在后端服务器的应用层同样有很多地方可以做缓存,提高服务的效率:本篇我们就来继续聊聊应用级的缓存. 缓存的命中率 缓存的 ...

  • 万字长文——互联网广告到底是如何运行的?

    最近,我们花了很多时间进行研究企业私域的发展和进程,致力于帮助企业完成数字化转型. 在研究到公域的信息流广告部分,我们的老朋友卫夕也发布了一篇讲述互联网广告运营的文章.我们看完后,都觉得非常不错,想把 ...

  • 什么是数据库的“缓存池”?(万字长文,绝对干货)

    什么是数据库的“缓存池”?(万字长文,绝对干货)

  • 吴春波:学华为到底学什么?华为向军队学习了什么?(万字长文)

    任正非就是一块大海绵,华为就是一棵洋葱头. "学不学华为"是个问题.你可以不学华为.况且,即使学华为,也不一定学得会. 学华为到底能不能学会?诸多企业带着这个疑惑依旧学着华为.任正 ...

  • 深度长文:光到底是粒子还是波?一个延续了几百年的争论!(一万字,请保持耐心)

    光到底是粒子还是波?直到目前也没有完全定论.而这个争论几乎囊括了所有物理学大佬,延续了几百年!几乎物理学所有大牛都加入了进来: 物理学发展到17世纪,才算是真正的拨开了迷雾.在那个开宗立派的名字闪闪发 ...

  • 万字长文说清减脂这件事情到底该怎么做?

    先科普一下脂肪被燃烧和氧化的过程.脂肪细胞中富含着一个甘油和三个脂肪酸组成的我们称之为甘油三酯的有机物,接下来开始脂肪水解的过程,先将甘油和脂肪酸分离,而脂肪酶当中的甘油三酯,脂肪酶也在协助这个过程. ...

  • 误诊率高达70%的疾病,患者能死里逃生吗?1万字长文,惊醒了许多人!

    很快,凌晨1点,接到急诊科老马医生的电话,说有个60岁的男性病人,呼吸困难.血氧不是很好,可能需气管插管上呼吸机,问ICU有没有床位. 正好是华哥值班,华哥刚忙完病人躺下休息了半个小时,这下又接到老马 ...

  • 万字长文:如何学习商业分析(PPT 说明)

    概述商业分析 商业分析是什么呢?摘录一段百科:商业分析指的是对方案进行经济效益分析,从财务上进一步判断它是否符合企业目标.如果符合,产品概念就可进入产品研制阶段了.包括审视预计的销售额.成本和利润是否 ...

  • 万字长文:新经销对行业&渠道认知的10个理论基础

    新经销一直以来对快速消费品行业渠道领域不断做深度的解读,这种解读背后,是有一定的认知模型做为理论指导的,这也是新经销指导内部编辑同事对行业认识的一个基础内容,今天也把这些基础框架知识,整理成一篇文章, ...