MyBatis 缓存

MyBatis 缓存

MyBatis 是现在国内比较流行的 ORM 框架,在学习 MyBatis 的时候,不得不了解 MyBatis 的两级缓存,要了解 MyBatis 的缓存,先要了解 MyBatis 几个重要的对象

  • SqlSession - 对应的一次数据库会话,由 SqlSessionFactory 的 openSession 创建,一次会话并不代表只能执行一条 SQL
  • MappedStatement - 存储了 SQL 对应的所有信息,XMLStatementBuilder 解析 XML 或者注解的时候,由 parseStatementNode 方法生成,放入到 configuration 中保存
  • Executor - 真正对数据库操作的对象,由 Configuration 的 newExecutor 创建
  • namespace - 用来区分 sql 命令,和 statementid 一起生成的 key 值作为 sql 的唯一标识

MyBatis 一级缓存

首先,一级缓存的配置有两种

  • SESSION(默认)
  • STATEMENT
properties 复制代码
<configuration>
    <settings>
    	<setting name="localCacheScope" value="SESSION"/>
    </settings>
<configuration>

所以 MyBatis 的一级缓存可以是 SqlSession 级别的,也可以是 Statement 级别的

原理

当客户端执行 SQL 的时候,会将查询结果封装到 SqlSession 的 Executor(BaseExecutor) 中的 localCache 属性中(Executor 的 query 方法),其底层是一个 HashMap

java 复制代码
protected PerpetualCache localCache;

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

key 值为 MappedStatementId + Offset + Limit + SQL + SQL 中的参数一起构成 CacheKey(Executor 的 createCacheKey 方法),生成 Key 的方法

java 复制代码
  @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;
  }

作为在市场叱咤了这么多年的框架,当然会考虑在数据更新之后查到缓存的问题,所以在更新数据的时候会将缓存清除(此处是无差别攻击)

java 复制代码
@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);
}

如果想跳过一级缓存,可以配置 <select flushCache = ture> 即可

思考

现在系统都是分布式集群,这种一级缓存应该也是有问题的

MyBatis 二级缓存

配置方式

  • config 配置
properties 复制代码
<settings>
     <setting name="cacheEnabled" value="true"/>
</settings>
  • Mapper.xml 配置
xml 复制代码
<cache />
  • mapper 接口
java 复制代码
@Mapper
@CacheNamespace // 接口级别
public interface TestDao {
    @Options(useCache = true) // 方法级别
    @Select({"select * from test"})
    Test getTest();
}
  • statement 语句中配置
xml 复制代码
<select id ="xxx" useCache="true"> ... </select>

可以理解 MyBatis 的二级缓存是 namespace 级别或者可以理解是 mapper 级别的

原理

MyBatis 的二级缓存是可以扩展很多的,它的核心接口是 org.apache.ibatis.cache.Cache

java 复制代码
public interface Cache {

  /**
   * @return The identifier of this cache
   */
  String getId();

  /**
   * @param key
   *          Can be any object but usually it is a {@link CacheKey}
   * @param value
   *          The result of a select.
   */
  void putObject(Object key, Object value);

  /**
   * @param key
   *          The key
   * @return The object stored in the cache.
   */
  Object getObject(Object key);

  /**
   * As of 3.3.0 this method is only called during a rollback
   * for any previous value that was missing in the cache.
   * This lets any blocking cache to release the lock that
   * may have previously put on the key.
   * A blocking cache puts a lock when a value is null
   * and releases it when the value is back again.
   * This way other threads will wait for the value to be
   * available instead of hitting the database.
   *
   *
   * @param key
   *          The key
   * @return Not used
   */
  Object removeObject(Object key);

  /**
   * Clears this cache instance.
   */
  void clear();

  /**
   * Optional. This method is not called by the core.
   *
   * @return The number of elements stored in the cache (not its capacity).
   */
  int getSize();

  /**
   * Optional. As of 3.2.6 this method is no longer called by the core.
   * <p>
   * Any locking needed by the cache must be provided internally by the cache provider.
   *
   * @return A ReadWriteLock
   */
  default ReadWriteLock getReadWriteLock() {
    return null;
  }

如果开启了二级缓存,最后执行的是 CachingExecutor,但是它其实是将 BaseExecutor 包装了一层的实现

java 复制代码
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) {
        // 传入 BaseExecutor 进行包装
      	executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

二级缓存存储代码

java 复制代码
private final TransactionalCacheManager tcm = new TransactionalCacheManager();

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
  throws SQLException {
    // 此处需要于 MappedStatement 绑定的 Cache,如果打了标签默认是 
    Cache cache = ms.getCache();
    if (cache != null) {
      	flushCacheIfRequired(ms);
      	if (ms.isUseCache() && resultHandler == null) {
        	ensureNoOutParams(ms, boundSql);
        	@SuppressWarnings("unchecked")
            // 先查询的是二级缓存
        	List<E> list = (List<E>) tcm.getObject(cache, key);
        	if (list == null) {
                // 这里是调用 BaseExecutor 的 query 方法
          		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);
}

其中 TransactionalCacheManager 中的缓存属性为

java 复制代码
// TransactionCache是装饰器对象,对Cache进行增强
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

TranactionalCache 的中缓存的属性为

java 复制代码
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;
    // 缓存中没有命中的数据
    private final Set<Object> entriesMissedInCache;
  ...
}

默认的 Cache 是在构建器 XMLMapperBuilder 解析 mapper 的时候动态插入的

java 复制代码
private void cacheElement(XNode context) {
    if (context != null) {
      	String type = context.getStringAttribute("type", "PERPETUAL");
      	Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      	String eviction = context.getStringAttribute("eviction", "LRU");
      	Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      	Long flushInterval = context.getLongAttribute("flushInterval");
      	Integer size = context.getIntAttribute("size");
      	boolean readWrite = !context.getBooleanAttribute("readOnly", false);
      	boolean blocking = context.getBooleanAttribute("blocking", false);
      	Properties props = context.getChildrenAsProperties();
		// 此处构建对应二级缓存的 Cache
      	builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

构建 Cache 的类型为被层层包装过了的 Cache

java 复制代码
public Cache useNewCache(Class<? extends Cache> typeClass,
  Class<? extends Cache> evictionClass,
  Long flushInterval,
  Integer size,
  boolean readWrite,
  boolean blocking,
  Properties props) {
    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();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
}

public Cache build() {
    setDefaultImplementations();
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
    if (PerpetualCache.class.equals(cache.getClass())) {
      	for (Class<? extends Cache> decorator : decorators) {
        	cache = newCacheDecoratorInstance(decorator, cache);
        	setCacheProperties(cache);
      	}
      	cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
      	cache = new LoggingCache(cache);
    }
    return cache;
}

// 这里将 Cache 一层一层往里面包装,看方法名称也知道是装饰器模式加强
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);
      	if (blocking) {
        	cache = new BlockingCache(cache);
      	}
      	return cache;
    } catch (Exception e) {
      throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
}

思考

二级缓存使用装饰者模式对 BaseExecutor 的方法进行增强,这种编码风格在日常编码中也可以使用

相关推荐
码农阿豪12 分钟前
从零开始搭建高效文档管理系统Moredoc打造私人某度文库
java·coplar
毕业设计-0114 分钟前
0042.大学校园生活信息平台+论文
java·spring boot·后端·毕业设计·源代码管理
等一场春雨1 小时前
Java 23 集合框架详解:ArrayList、LinkedList、Vector
java·开发语言
Hello Dam1 小时前
分布式环境下定时任务扫描时间段模板创建可预订时间段
java·定时任务·幂等性·redis管道·mysql流式查询
javaweiming1 小时前
根据中文名称首字母进行分组
java·汉字转拼音
水宝的滚动歌词7 小时前
设计模式之建造者模式
java·设计模式·建造者模式
孤蓬&听雨7 小时前
Java SpringBoot使用Apache POI导入导出Excel文件
java·spring boot·apache·excel导出·excel导入
紫琪软件工作室8 小时前
自定义有序Map
java
刘婉晴8 小时前
【蓝桥杯研究生组】第14届Java试题答案整理
java·蓝桥杯
Upuping9 小时前
「全网最细 + 实战源码案例」设计模式——外观模式
java·后端·设计模式