Mybatis 的缓存是如何实现?

Mybatis缓存的实现原理及扩展

 作者简介:大家好,我是码炫码哥,前中兴通讯、美团架构师,现任某互联网公司CTO,兼职码炫课堂主讲源码系列专题


代表作:《jdk源码&多线程&高并发》,《深入tomcat源码解析》,《深入netty源码解析》,《深入dubbo源码解析》,《深入springboot源码解析》,《深入spring源码解析》,《深入redis源码解析》等


联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬。码炫课堂的个人空间-码炫码哥个人主页-面试,源码等

回答

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

    // ... 其他代码省略
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值