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 的方法进行增强,这种编码风格在日常编码中也可以使用

相关推荐
小灰灰__17 分钟前
IDEA加载通义灵码插件及使用指南
java·ide·intellij-idea
夜雨翦春韭20 分钟前
Java中的动态代理
java·开发语言·aop·动态代理
程序媛小果41 分钟前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
追风林1 小时前
mac m1 docker本地部署canal 监听mysql的binglog日志
java·docker·mac
芒果披萨1 小时前
El表达式和JSTL
java·el
duration~2 小时前
Maven随笔
java·maven
zmgst2 小时前
canal1.1.7使用canal-adapter进行mysql同步数据
java·数据库·mysql
跃ZHD2 小时前
前后端分离,Jackson,Long精度丢失
java
blammmp2 小时前
Java:数据结构-枚举
java·开发语言·数据结构
暗黑起源喵3 小时前
设计模式-工厂设计模式
java·开发语言·设计模式