Mybatis源码解析--缓存

Mybatis源码解析--缓存

背景

Mybatis作为一个持久层框架,极大的简化了使用JDBC对数据库操作的繁琐流程,只需定义接口编写SQL模板即可操作数据库,消除了手动获取连接、创建Statement、设置参数、解析结果集等。Mybatis除了对数据库操作进行简化之外,还在性能上做了优化,提供了缓存。开始之前,先思考下面这个场景:

一般用户会向应用发起一个请求,通过应用处理之后会响应一个预期结果给用户。设想一下由于业务比较复杂,可能这次请求需要对数据库访问十几次才能获取完本次业务处理所需数据,而一般我们的应用和数据库会分别部署在同一局域网的两台机器上。请求处理示意图如下:

没有缓存示意图

上面每次都要从数据库获取数据,如果存在对同一数据获取多次的场景就会有性能上的损耗;这种情况下,如果加个缓存,将查询的结果临时缓存起来,每次先从缓存获取,如果有结果直接使用该结果;缓存中没有,再查询数据库并放入缓存。这样整个请求处理过程中数据的获取一部分走缓存一部分走数据库从而提高响应效率。

有缓存示意图

Mybatis缓存的抽象

Mybatis内部设计了两级缓存

Mybatis考虑到这种情况,所以提供了一级缓存和二级缓存机制以提高系统的性能

Mybatis缓存设计

缓存接口的定义

public interface Cache {
  // 获取当前缓存的标识
  String getId();
  // 向缓存中存放数据
  void putObject(Object key, Object value);
  // 从缓存中获取数据
  Object getObject(Object key);
  // 从缓存中移除该key的数据
  Object removeObject(Object key);
  // 清空缓存
  void clear();
  // 当前缓存已存放数据的大小
  int getSize();
​
  default ReadWriteLock getReadWriteLock() {
    return null;
  }
}

内置的缓存实现

以上实现可以分为缓存的实现类(PerpetualCache)和对其他缓存的装饰类(除PerpetualCache的缓存类)

 

一级缓存

Mybatis一级缓存示意图

上图是在没有开启二级缓存时Mybatis的缓存架构(即仅有一级缓存),下面我们看一下具体代码

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);
  }
  // 如果开启二级缓存会使用CachingExecutor对原Executor进行包装  
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  // Mybatis插件功能对Executor的增强  
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

此处我们只讨论一级缓存所以我们认为二级缓存没有开启且没有对Executor做任何增强

protected BaseExecutor(Configuration configuration, Transaction transaction) {
  this.transaction = transaction;
  this.deferredLoads = new ConcurrentLinkedQueue<>();
  // 初始化一级缓存  
  this.localCache = new PerpetualCache("LocalCache");
  this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
  this.closed = false;
  this.configuration = configuration;
  this.wrapper = this;
}

在创建Executor时会在内部初始化PerpetualCache作为一级缓存

一次会话查询时序图

PerpetualCache具体实现

public class PerpetualCache implements Cache {
​
  private final String id;
​
  private final Map<Object, Object> cache = new HashMap<>();
​
  public PerpetualCache(String id) {
    this.id = id;
  }
​
  @Override
  public String getId() {
    return id;
  }
​
  @Override
  public int getSize() {
    return cache.size();
  }
​
  @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);
  }
​
  @Override
  public void clear() {
    cache.clear();
  }
}

可以看到PerpetualCache是通过HashMap实现的

一级缓存的生命周期

Mybatis在对数据库进行操作时会创建一个SqlSession对象(SqlSession是接口,真正创建的是DefaultSqlSession),该SqlSession会持有一个Executor的引用(创建DefaultSqlSession需要创建一个Executor),在创建Executor时会在内部创建PerpetualCache对象(该对象内部使用HashMap实现),所以这3者的关系图如下:

所以当会话结束时,SqlSession对象及其内部Executor对象及PerpetualCache会一并释放掉。

 

CacheKey的定义与设计

如下图所示,MyBatis定义了一个org.apache.ibatis.cache.Cache接口作为其Cache提供者的SPI(Service Provider Interface) ,所有的MyBatis内部的Cache缓存,都应该实现这一接口。MyBatis定义了一个PerpetualCache实现类实现了Cache接口,实际上,在SqlSession对象里的Executor对象内维护的Cache类型实例对象,就是PerpetualCache子类创建的。

MyBatis内部还有很多Cache接口的实现,一级缓存只会涉及到这一个PerpetualCache子类,Cache的其他实现将会放到二级缓存中介绍)。

我们知道,Cache最核心的实现其实就是一个Map,将本次查询使用的特征值作为key,将查询结果作为value存储到Map中。

现在最核心的问题出现了:怎样来确定一次查询的特征值?

换句话说就是:怎样判断某两次查询是完全相同的查询?

也可以这样说:如何确定Cache中的key值?

MyBatis认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询:

  1. 传入的 statementId

  2. 查询时要求的结果集中的结果范围 (结果的范围通过rowBounds.offsetrowBounds.limit表示);

  3. 这次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql()

  4. 传递给java.sql.Statement要设置的参数值

现在分别解释上述四个条件:

  1. 传入的statementId,对于MyBatis而言,你要使用它,必须需要一个statementId,它代表着你将执行什么样的Sql

  2. MyBatis自身提供的分页功能是通过RowBounds来实现的,它通过rowBounds.offsetrowBounds.limit来过滤查询出来的结果集,这种分页功能是基于查询结果的再过滤,而不是进行数据库的物理分页;

由于MyBatis底层还是依赖于JDBC实现的,那么,对于两次完全一模一样的查询,MyBatis要保证对于底层JDBC而言,也是完全一致的查询才行。而对于JDBC而言,两次查询,只要传入给JDBCSQL语句完全一致,传入的参数也完全一致,就认为是两次查询是完全一致的。

上述的第3个条件正是要求保证传递给JDBCSQL语句完全一致;第4条则是保证传递给JDBC的参数也完全一致;

3、4讲的有可能比较含糊,举一个例子:

<select id="selectByCritiera" parameterType="java.util.Map" resultMap="BaseResultMap">
​
     select employee_id,first_name,last_name,email,salary
   
     from louis.employees
​
     where  employee_id = #{employeeId}
​
     and first_name= #{firstName}
​
     and last_name = #{lastName}
​
     and email = #{email}
​
</select>

如果使用上述的"selectByCritiera"进行查询,那么,MyBatis会将上述的SQL中的#{} 都替换成 ? 如下:

     select employee_id,first_name,last_name,email,salary
​
     from louis.employees
​
     where  employee_id = ?
​
     and first_name= ?
​
     and last_name = ?
​
     and email = ?

MyBatis最终会使用上述的SQL字符串创建JDBCjava.sql.PreparedStatement对象,对于这个PreparedStatement对象,还需要对它设置参数,调用setXXX()来完成设值,第4条的条件,就是要求对设置JDBCPreparedStatement的参数值也要完全一致。

即3、4两条MyBatis最本质的要求就是:

调用JDBC的时候,传入的SQL语句要完全相同,传递给JDBC的参数值也要完全相同。

综上所述,CacheKey由以下条件决定:

statementId + rowBounds + 传递给JDBC的SQL + 传递给JDBC的参数值

CacheKey的创建

对于每次的查询请求,Executor都会根据传递的参数信息以及动态生成的SQL语句,将上面的条件根据一定的计算规则,创建一个对应的CacheKey对象。

我们知道创建CacheKey的目的,就两个:

  1. 根据CacheKey作为key,去Cache缓存中查找缓存结果;

  2. 如果查找缓存命中失败,则通过此CacheKey作为key,将从数据库查询到的结果作为value,组成key,value对存储到Cache缓存中。

CacheKey的构建被放置到了Executor接口的实现类BaseExecutor中,定义如下:

/**
 * 所属类:  org.apache.ibatis.executor.BaseExecutor
 * 功能 :   根据传入信息构建CacheKey
 */
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
  // 1.检查Executor是否已关闭
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  // 2.创建CacheKey对象并设置属性
  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) {
    cacheKey.update(configuration.getEnvironment().getId());
  }
  // 返回创建的CacheKey
  return cacheKey;
}

CacheKey的hashcode生成算法

刚才已经提到,Cache接口的实现,本质上是使用的HashMap<k,v>,而构建CacheKey的目的就是为了作为HashMap<k,v>中的key值。而HashMap是通过key值的hashcode 来组织和存储的,那么,构建CacheKey的过程实际上就是构造其hashCode的过程。下面的代码就是CacheKey的核心hashcode生成算法,感兴趣的话可以看一下:

  public void updateAll(Object[] objects) {
    for (Object o : objects) {
      update(o);
    }
  }
public void update(Object object) {
  //1. 得到对象的hashcode;   
  int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
  //对象计数递增
  count++;
  checksum += baseHashCode;
  //2. 对象的hashcode 扩大count倍  
  baseHashCode *= count;
  //3. hashCode * 拓展因子(默认37)+拓展扩大后的对象hashCode值
  hashcode = multiplier * hashcode + baseHashCode;

  updateList.add(object);
}

二级缓存

二级缓存的划分

Mybatis并不是简单整个Application只有一个Cache缓存对象,他将缓存划分的更细,既是Mapper级别的,即每个Mapper都会拥有一个Cache对象,具体如下

  • 为每个Mapper分配一个Cache缓存对象(使用<cache>标签配置)

  • 多个Mapper共享一个Cache缓存对象(使用<cache-ref>标签配置)

为每个Mapper分配一个Cache缓存对象(使用<cache>标签配置)

Mybatis将Application级别缓存细分到Mapper级别,即对每个Mapper.xml,如果在其中使用了<cache>标签,则Mybatis会为这个Mapper创建一个Cache缓存对象,如下图所示:

对于namespace1和namespace2两个Mapper,都配置了<cache>会分别为这两个Mapper根据配置信息创建一个Cache对象;而namespace3的Mapper,没有配置<cache>标签,则对于Mapper namespace3就没有对应的二级缓存,定义如下:

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

多个Mapper共享一个Cache缓存对象(使用<cache-ref>标签配置)

如果想让多个Mapper共享一个Cache的话,需要使用<cache-ref namespace="">标签来指定这个Mapper使用哪个Mapper的Cache对象

Mapper namespace2的<cache-ref>定义的namespace="namespace1",说明Mapper namespace2将使用Mapper namespace1中的缓存对象,这时要求namespace1中一定要有<cache>标签的定义,定义如下:

<cache-ref namespace="com.someone.application.data.SomeMapper"/>

二级缓存生效条件

Mybatis对二级缓存的支持的粒度很细,他会指定某一条查询语句是否使用二级缓存

虽然在Mapper中配置了<cache>,并且为此Mapper分配了Cache对象,这并不代表我们使用Mapper中定义的查询语句查到的结果都会放置到Cache对象中,我们必须指定Mapper中的某条选择语句是否支持缓存。即如下所示,在<select>标签中配置useCache=true,Mapper才会对此Select的查询支持缓存特性,否则,不会对此select查询,不会经过缓存。

总之,要想使某条select查询支持二级缓存,必须同时保证一下条件同时满足:

  1. MyBatis支持二级缓存的总开关:全局配置变量参数 cacheEnabled=true

  2. 该select语句所在的Mapper,配置了<cache><cached-ref>节点,并且有效

  3. 该select语句的参数 useCache=true

二级缓存实现的选择

MyBatis对二级缓存的设计非常灵活,它自己内部实现了一系列的Cache缓存实现类,并提供了各种缓存刷新策略如LRU,FIFO等等;另外,MyBatis还允许用户自定义Cache接口实现,用户是需要实现org.apache.ibatis.cache.Cache接口,然后将Cache实现类配置在<cache type="">节点的type属性上即可;除此之外,MyBatis还支持跟第三方内存缓存库如Memecached的集成,总之,使用MyBatis的二级缓存有三个选择:

1.MyBatis自身提供的缓存实现;

2. 用户自定义的Cache接口实现;

3.跟第三方内存缓存库的集成;

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Mybatis是一个轻量级的Java持久层开源框架,它封装了JDBC操作数据库的底层细节,提供了一个简单易用的数据库访问方式。 Mybatis源码分为核心模块和附加模块两部分,核心模块主要包括配置解析、SQL解析、SQL执行等功能,附加模块包括连接池、缓存、事务管理等功能。 在Mybatis源码中,配置解析是其中的关键部分。通过解析mybatis-config.xml配置文件,可以获取到数据库连接信息、映射器配置、插件配置等。在配置解析过程中,Mybatis会对配置文件进行校验,确保配置的正确性。 SQL解析Mybatis的另一个重要功能。Mybatis通过解析Mapper接口中的注解或XML配置文件中的SQL语句,将SQL语句解析为ParameterMapping、BoundSql等对象,并将其封装成一个MappedStatement对象,供后续的SQL执行使用。 SQL执行是Mybatis的核心功能之一。在SQL执行阶段,Mybatis会根据MappedStatement中的信息,获取数据库连接,并执行对应的SQL语句。在执行过程中,Mybatis会通过TypeHandler对参数进行类型转换,并使用ResultSetHandler将查询结果封装成Java对象。 除了核心模块,Mybatis源码还包括了连接池、缓存、事务管理等附加模块的实现。连接池模块负责管理数据库连接的获取和释放,缓存模块负责缓存查询结果以提高性能,而事务管理模块则负责管理数据库的事务处理。 总之,Mybatis源码解析涉及多个关键模块的实现,包括配置解析、SQL解析、SQL执行、连接池、缓存、事务管理等。通过了解这些模块的实现原理,我们可以更好地理解和使用Mybatis框架。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值