「二」【Mybatis系列】深入理解Mybatis之多级级缓存

1 MyBatis缓存概述

MyBatis中存在两个缓存,一级缓存二级缓存

  1. 一级缓存会话级缓存,生命周期仅存在于当前会话,不可以直接关关闭。但可以通过flushCachelocalCacheScope对其做相应控制。
  2. 二级缓存应用级性缓存,缓存对象存在于整个应用周期,而且可以跨线程使用

在这里插入图片描述

1.1 一级缓存的命中场景

关于一级缓存的命中可大致分为两个场景,满足特定命中参数,第二不触发清空方法

1.1.1 缓存命中参数
  1. SQL与参数相同
  2. 同一个会话
  3. 相同的MapperStatement ID
  4. RowBounds行范围相同
1.1.2 触发清空缓存
  1. 手动调用clearCache
  2. 执行提交回滚
  3. 执行update
  4. 配置flushCache=true
  5. 缓存作用域为Statement
  • 图示
    在这里插入图片描述
1.2 一级缓存源码解析

一级缓存主要逻辑存在于BaseExecutor当中,当会话接收到查询请求之后,会交给执行器的Query方法,在这会通过SQL、参数、分页条件等参数创建一个缓存key,在基于这个key去PerpetualCachhe中查找对应的缓存值,如果有值直接返回,没有则查询数据库,然后填充缓存

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

最终缓存的实现非常简单,就是一个HashMap

1.3 一级缓存的清空

缓存的清空对应BaseExecutor中的clearLoaclCache() 方法,查看有哪些调用此方法,就能发现哪些场景会清空缓存了

如图
在这里插入图片描述

  1. update:执行任意增删改
  2. select:查询又分为两种情况,前置清空 -> flushCache=true 另一个则是后置清空,配置了缓存作用域为Statement查询结束会清空缓存
  3. commit:提交前清空
  4. rollback : 回滚前清空

clearLocalCache():是清空当前回话所有的一级缓存数据

1.4 Mybatis集成Spring后一级缓存失效问题

当项目集成Spring之后会发现一级缓存失效了,以为是Spring 的问题,真正的原因是Spring对SqlSession进行了封装通过SqlSessionTemplate,使得每次调用SQL都会创建一个SqlSession,具体可以参见SqlSessionInterceptor,一级缓存是在同一个会话中才会生效!

1.4.1 解决

给Spring添加事物,添加事物之后,SqlSessionInterceptor会去判断两次请求是否在同一事物当中,如果是就会共用一个SqlSession会话来解决

在这里插入图片描述

2 二级缓存

概述:二级缓存也称作是应用级缓存,与一级缓存不同的是它作用域范围是整个应用,而且可以夸线程使用,所以二级缓存会有更高的命中率,适合缓存一些修改较少的数据,数据访问流程上说先访问二级缓存,在访问一级缓存

在这里插入图片描述

2.1 二级缓存需求

二级缓存是一个完整的缓存解决方案,那应该包含哪些功能呢?这里为们主要分为核心功能,非核心功能两类。

2.1.1 存储【核心功能】

缓存数据存储在哪里?常用的方案如下:

  1. 内存:最简单就是内存当中,不仅实现简单,而且速度快。弊端就是不能持久化,且容量有限制
  2. 硬盘:可以持久化,容量大。但速度不如内存,一般结合内存一起使用。
  3. 第三方集成:在分布式情况,如果想和其他节点共享缓存,只能第三方软件集成,比如Redis。
2.1.2 溢出淘汰【核心功能】

无论哪种存储都必须有一个容量,当容量满的时候就要清除,清除掉算法即溢出淘汰机制。常见算法如下:

  1. FIFO:先进先出
  2. LRU:最近最少使用
  3. WeakReference:弱引用,将缓存对象进行弱引用包装,当Java进行垃圾回收的时候(GC),不论当前内存空间是否足够,这个对象都将会被回收。
  4. SoftReference:软引用,如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用
2.1.3 其他功能
  1. 过期清理:清理存放过久的数据
  2. 线程安全:保证缓存可以被多个线程使用
  3. 写安全:当拿到缓存数据后,可对其进行修改,而不影响原本的缓存数据。通常采取做法是对缓存对象进行深拷贝

在这里插入图片描述

2.2 二级缓存责任链设计

这么多的功能,如何才能简单实现,并保证它的灵活性与扩展性呢?这里Mybatis抽象出Cache接口,其只定义了缓存中最基本的方法:

  1. 设置缓存
  2. 获取缓存
  3. 清除缓存
  4. 获取缓存数量
    然后上述每一个功能都会对应一个组件类,并基于装饰者责任链的模式,将各个组件进行串联,在执行缓存的基本功能时,其他的缓存逻辑会沿着这个责任链一次往下传递,如下图
    在这里插入图片描述

优点

  1. 指责单一:各个节点只负责自己的逻辑,不需要关心其他节点
  2. 扩展性强:可根据需要扩展节点,删除节点,还可以调换顺序保证灵活性
  3. 松耦合:各节点之间没强依赖其他节点。而是通过顶层的Cache接口进行间接依赖
2.2 二级缓存的使用
2.2.1 缓存空间的声明

二级缓存默认不开启,需要为其声明缓存空间才可以使用,通过@CacheNamespace或指定的MappedStatement声明之后该缓存为该Mapper所独有,其他Mapper不能访问。如需多个Mapper共享一个缓存空间可以通过@CacheNamespaceRef或引用同一个缓存空间。CacheNamespace 配置清单:

配置说明
implementation指定缓存的存储实现类,默认是用HashMap存储在内存当中
eviction指定缓存溢出淘汰实现类,默认LRU,清除最少使用
flushInterval设置缓存定时全部清空时间
size设置缓存容量,超出后就会被eviction指定的算法进行淘汰
readWritetrue通过序列化复制,来保证缓存对象是可读写的,默认true
blocking为每个key的访问添加阻塞锁,防止缓存击穿
properties为上述组件,配置额外参数,key对应组件中的字段名称

注:Cache中责任链的组成通过@CacheNamespace指导生成,具体逻辑参见CacheBuilder

/*
 *    Copyright 2009-2012 The MyBatis Team
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.mapping;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;
import org.apache.ibatis.cache.decorators.FifoCache;
import org.apache.ibatis.cache.decorators.LoggingCache;
import org.apache.ibatis.cache.decorators.ScheduledCache;
import org.apache.ibatis.cache.decorators.SerializedCache;
import org.apache.ibatis.cache.decorators.SynchronizedCache;
import org.apache.ibatis.cache.impl.PerpetualCache;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

public class CacheBuilder {
  private String id;
  private Class<? extends Cache> implementation;
  private List<Class<? extends Cache>> decorators;
  private Integer size;
  private Long clearInterval;
  private boolean readWrite;
  private Properties properties;

  public CacheBuilder(String id) {
    this.id = id;
    this.decorators = new ArrayList<Class<? extends Cache>>();
  }

  public CacheBuilder implementation(Class<? extends Cache> implementation) {
    this.implementation = implementation;
    return this;
  }

  public CacheBuilder addDecorator(Class<? extends Cache> decorator) {
    if (decorator != null) {
      this.decorators.add(decorator);
    }
    return this;
  }

  public CacheBuilder size(Integer size) {
    this.size = size;
    return this;
  }

  public CacheBuilder clearInterval(Long clearInterval) {
    this.clearInterval = clearInterval;
    return this;
  }

  public CacheBuilder readWrite(boolean readWrite) {
    this.readWrite = readWrite;
    return this;
  }

  public CacheBuilder properties(Properties properties) {
    this.properties = properties;
    return this;
  }

  public Cache build() {
    setDefaultImplementations();
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
    if (cache.getClass().getName().startsWith("org.apache.ibatis")) {
      for (Class<? extends Cache> decorator : decorators) {
        cache = newCacheDecoratorInstance(decorator, cache);
        setCacheProperties(cache);
      }
      cache = setStandardDecorators(cache);
    }
    return cache;
  }

  private void setDefaultImplementations() {
    if (implementation == null) {
      implementation = PerpetualCache.class;
      if (decorators.size() == 0) {
        decorators.add(FifoCache.class);
      }
    }
  }

  private Cache setStandardDecorators(Cache cache) {
    try {
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      if (size != null && metaCache.hasSetter("size")) {
        metaCache.setValue("size", size);
      }
      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);
      return cache;
    } catch (Exception e) {
      throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
  }

  private void setCacheProperties(Cache cache) {
    if (properties != null) {
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      for (Map.Entry<Object, Object> entry : properties.entrySet()) {
        String name = (String) entry.getKey();
        String value = (String) entry.getValue();
        if (metaCache.hasSetter(name)) {
          Class<?> type = metaCache.getSetterType(name);
          if (String.class == type) {
            metaCache.setValue(name, value);
          } else if (int.class == type
              || Integer.class == type) {
            metaCache.setValue(name, Integer.valueOf(value));
          } else if (long.class == type
              || Long.class == type) {
            metaCache.setValue(name, Long.valueOf(value));
          } else if (short.class == type
              || Short.class == type) {
            metaCache.setValue(name, Short.valueOf(value));
          } else if (byte.class == type
              || Byte.class == type) {
            metaCache.setValue(name, Byte.valueOf(value));
          } else if (float.class == type
              || Float.class == type) {
            metaCache.setValue(name, Float.valueOf(value));
          } else if (boolean.class == type
              || Boolean.class == type) {
            metaCache.setValue(name, Boolean.valueOf(value));
          } else if (double.class == type
              || Double.class == type) {
            metaCache.setValue(name, Double.valueOf(value));
          } else {
            throw new CacheException("Unsupported property type for cache: '" + name + "' of type " + type);
          }
        }
      }
    }
  }

  private Cache newBaseCacheInstance(Class<? extends Cache> cacheClass, String id) {
    Constructor<? extends Cache> cacheConstructor = getBaseCacheConstructor(cacheClass);
    try {
      return cacheConstructor.newInstance(id);
    } catch (Exception e) {
      throw new CacheException("Could not instantiate cache implementation (" + cacheClass + "). Cause: " + e, e);
    }
  }

  private Constructor<? extends Cache> getBaseCacheConstructor(Class<? extends Cache> cacheClass) {
    try {
      return cacheClass.getConstructor(String.class);
    } catch (Exception e) {
      throw new CacheException("Invalid base cache implementation (" + cacheClass + ").  " +
          "Base cache implementations must have a constructor that takes a String id as a parameter.  Cause: " + e, e);
    }
  }

  private Cache newCacheDecoratorInstance(Class<? extends Cache> cacheClass, Cache base) {
    Constructor<? extends Cache> cacheConstructor = getCacheDecoratorConstructor(cacheClass);
    try {
      return cacheConstructor.newInstance(base);
    } catch (Exception e) {
      throw new CacheException("Could not instantiate cache decorator (" + cacheClass + "). Cause: " + e, e);
    }
  }

  private Constructor<? extends Cache> getCacheDecoratorConstructor(Class<? extends Cache> cacheClass) {
    try {
      return cacheClass.getConstructor(Cache.class);
    } catch (Exception e) {
      throw new CacheException("Invalid cache decorator (" + cacheClass + ").  " +
          "Cache decorators must have a constructor that takes a Cache instance as a parameter.  Cause: " + e, e);
    }
  }
}

2.2.2 缓存其它配置

@CacheNamespace还可以通过其他参数来控制二级缓存

字段配置域说明
cacheEnable二级缓存全局开关,默认开启
useCache<select | update | insert | delete>指定的statement是否开启,默认关闭
flushCache<select |update |insert |delete>执行sql前是否清空当前二级缓存空间,update默认true,query默认false
2.2.3 二级缓存的命中条件

二级缓存的命中场景与一级缓存相似,不同的在于二级缓存可以跨线程使用,还有二级缓存的更新,必须是在会话提交后。
在这里插入图片描述

为什么提交之后才能命中缓存?

在这里插入图片描述
如上图两个会话在修改同一数据,当会二修改后,在讲其查询出来,假如它实时填充到二级缓存,而会话一就能通过缓存获取修改之后的数据,但实质修改的数据回滚了,并没有真正的提交给数据库。
所以为了保证数据一致性,二级缓存必须是会话提交之后才会真正填充,包括对缓存的清空,也必须是正常提交之后才生效

2.3 二级缓存结构

为了实现会话提交之后才更新缓存,Mybatis为每个会话设立了若干暂存区当前会话对指定的缓存空间变更,都存在对应的暂存区,当会话提交之后才会提交每个暂存区对应的缓存空间。为了统一管理这些暂存区,每个会话都有唯一一个的事物管理器,所以这里暂存区也可以叫做事物缓存

在这里插入图片描述
最后我们通过下图了解会话,暂存区,二级缓存空间的关系:

在这里插入图片描述

2.4 二级缓存执行流程

原本会话是通过Excutor实现SQL调用,这里基于装饰器模式使用CachingExecutor对SQL调用逻辑进行拦截。嵌入二级缓存相关逻辑
在这里插入图片描述

  1. 查询操作query
    当会话调用query(),会基于查询语句、参数等数据组成缓存key,然后尝试从二级缓存中读取数据。读到就直接返回,没有就调用被装饰的Executor去查询数据库,然后在填充至对应的暂存区

注:这里的查询是实时从缓存空间里读取的,而变更,只会记录在暂存区

  1. 更新操作update
    当执行update操作时,同样会基于查询的语句和参数组成缓存key,然后在执行update之前清空缓存。这里只清空针对暂存区,同时记录清空的标记,以便会话提交之时,依据该标记去清空二级缓存空间

注:如果在查询操作中配置了 flushCache=true 也会执行相同的操作

  1. 提交操作commit
    当会话执行commit之后,会将该会话下的所有暂存区的变更,更新到对应的二级缓存空间里去
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值