2024-12-22  阅读(626)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/mianshi/baodian/detail/1425024503

回答

Mybatis 内置了强大了事务性查询缓存机制。默认情况下,Mybatis 定义了两级缓存,并且提供Cache接口扩展,提高了框架的扩展性。

  • 一级缓存:也叫“本地缓存”,默认情况下开启(SqlSession 级别的缓存)
  • 二级缓存:也叫“全局缓存”,基于 namespace 级别的缓存,需要手动开启和配置。

实现原理

一级缓存

Mybatis 一级缓存由PerpetualCache实现,采用 Map 结构存储数据。其作用域是 SqlSession,各个 SqlSession 之间的缓存相互隔离。每次查询后,结果会被缓存到PerpetualCache中,后续相同查询直接取缓存数据。

核心实现类和方法

  1. BaseExecutor:负责执行 SQL,管理一级缓存。
public abstract class BaseExecutor implements Executor {
  
  // ... 其他代码省略
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 生成缓存 key  
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // 查询结果  
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }

  @SuppressWarnings("unchecked")
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    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--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }
  // ... 其他代码省略
}
  1. PerpetualCache:缓存实现类,采用 Map 结构存储数据。
public class PerpetualCache implements Cache {
  
  // ... 其他代码省略
  private final String id;

  // Map 结果本地缓存结果
  private final Map<Object, Object> cache = new HashMap<>();

  public PerpetualCache(String id) {
    this.id = id;
  }

  @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);
  }
  // ... 其他代码省略
}

二级缓存

Mybatis 二级缓存是基于命名空间(namespace)的缓存,存储在CachingExecutor。它在每次执行查询之前,先从二级缓存中查找,如果找到则返回缓存结果,否则执行查询并将结果缓存。

默认不打开二级缓存,要开启二级缓存,需通过配置打开,并且使用二级缓存的属性类需要实现 Serializable 序列化接口(可用来保存对象的状态)。配置如下:

<configuration>
    <!-- 其他配置 -->
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>
</configuration>

<mapper namespace="com.damingge.mapper.EmployeeMapper">
    <!-- 启用二级缓存 -->
    <cache 
        eviction="LRU" 
        flushInterval="60000" 
        size="512" 
        readOnly="true"/>
</mapper>

核心实现类和方法

  1. CachingExecutor:二级缓存执行器。
public class CachingExecutor implements Executor {
  
  // ... 其他代码省略  
  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }
    
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 生成缓存key
    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 {
    // 从映射声明器获取 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) {
          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);
  }

// ... 其他代码省略    
}
  1. Cache:缓存接口,Mybatis 默认提供了多种缓存实现,如 PerpetualCache、LruCache、FifoCache、BlockingCache 等。

扩展

一级缓存的 cache key 如何生成?

Mybatis 一级缓存键值通过 CacheKey 实现,生成 Cache Key 包含 SQL 语句ID、查询参数、分页参数等。

  • CacheKey:CacheKey 类通过 update 方法将各个属性添加到缓存键中。
public class CacheKey implements Cloneable, Serializable {
  // ... 其他代码省略
  public CacheKey(Object[] objects) {
    this();
    updateAll(objects);
  }

  public int getUpdateCount() {
    return updateList.size();
  }

  //核心方法  
  public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);

    count++;
    checksum += baseHashCode;
    baseHashCode *= count;

    hashcode = multiplier * hashcode + baseHashCode;
    // 所有属性存放至一个List集合对象
    updateList.add(object);
  }

  public void updateAll(Object[] objects) {
    for (Object o : objects) {
      update(o);
    }
  }

  @Override
  public boolean equals(Object object) {
    if (this == object) {
      return true;
    }
    if (!(object instanceof CacheKey)) {
      return false;
    }

    final CacheKey cacheKey = (CacheKey) object;

    if (hashcode != cacheKey.hashcode) {
      return false;
    }
    if (checksum != cacheKey.checksum) {
      return false;
    }
    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;
  }
 // ... 其他代码省略
}
  • BaseExecutor:BaseExecutor 类通过 createCacheKey 方法生成 CacheKey
public abstract class BaseExecutor implements Executor {
    // ... 其他代码省略

    @Override
    public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        CacheKey cacheKey = new CacheKey();
        //SQL语句ID
        cacheKey.update(ms.getId());
        // 分页参数
        cacheKey.update(rowBounds.getOffset());
        cacheKey.update(rowBounds.getLimit());
        // SQL
        cacheKey.update(boundSql.getSql());
        // 入参
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        Configuration configuration = ms.getConfiguration();
        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
        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);
            }
        }
        return cacheKey;
    }

    // ... 其他代码省略
}

Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文