Mybatis原理剖析之一级缓存(四)

概述

当用户频繁查询某些固定的数据时,第一次将这些数据从数据库中查询出来,保存在缓存(内存,高速磁盘)中。

当下次用户再次查询这些数据时,不用再通过数据库查询,而是去缓存里面查询。

这么做的目的,一是提升查询速度,二是降低数据库的并发请求压力。

在Mybatis中,缓存分为两种 : 一级缓存和二级缓存。
一级缓存是SqlSession级别的,二级缓存是Mapper级别的。

一级缓存

如果你读过我之前写的Mybatis相关文章,那一定知道最后我们的请求都是由SqlSession来执行的,
确切的说,应该是由DefaultSqlSession来执行的,
更确切的说,其实是由DefaultSqlSession的Executor执行器来执行的。

Query

我们随便找一个DefaultSqlSession的查询方法,看看它的源代码

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {    
    } 
}

方法首先根据statement获取MappedStatement,MappedStatement其实就是sql操作的具体参数。
然后使用执行器executor执行查询方法。

看一下executor的query()方法,这里默认使用的是BaseExecutor。

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
   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--;
    }
    return list;
}

首先看这一句

CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);

执行器首先创建了CacheKey,由字面意思不难推断,这是一个用于追踪缓存的唯一ID,也就是说这个CacheKey可以唯一确定一次请求。然后执行重载的query()方法。
重点看一下几行代码。

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

代码的大概意思是说,首先通过cachekey去localCache里面去寻找我们之前的缓存结果,若结果为空。则去数据库里查询。
显然localCache就是我们所说的一级缓存,打开源代码看一下。

public class PerpetualCache implements Cache {
 	private Map<Object, Object> cache = new HashMap<Object, Object>();

	...
}

原来是一个PerpetualCache类,实现了Cache接口,为什么要实现Cache接口呢,因为缓存有多种,PerpetualCache只是其中一种。往下看,发现其实真正的缓存就是用HashMap来存储的。

我们知道HashMap不是一个线程安全的数据结构,那为什么这里还要用HashMap呢,博主也百思不得其解。

Update

我们再看一下executor的update方法

@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    clearLocalCache();
    return doUpdate(ms, parameter);
}

@Override
public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
}

我们发现,当Mybatis执行update方法时,首先要做的就是清空缓存,其实update、delete、add等方法都是清空缓存,这在一定程度上保证了缓存数据的有效性。

CacheKey

我们再来看一下CacheKey是如何生成的,因为CacheKey是获取缓存结果的唯一ID,所以它的值的生成过程比较重要。看一下代码。

@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(Integer.valueOf(rowBounds.getOffset()));
    cacheKey.update(Integer.valueOf(rowBounds.getLimit()));
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    for (int i = 0; i < parameterMappings.size(); i++) {
      ParameterMapping parameterMapping = parameterMappings.get(i);
      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());
    }
    return cacheKey;
}   

CacheKey的update方法

public void update(Object object) {
    if (object != null && object.getClass().isArray()) {
        //如果是数组,则循环调用doUpdate
      int length = Array.getLength(object);
      for (int i = 0; i < length; i++) {
        Object element = Array.get(object, i);
        doUpdate(element);
      }
    } else {
        //否则,doUpdate
      doUpdate(object);
    }
}

CacheKey的doUpdate方法

private void doUpdate(Object object) {
    int baseHashCode = object == null ? 1 : object.hashCode();

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

    hashcode = multiplier * hashcode + baseHashCode;
    updateList.add(object);
}

再看一下CacheKey的toString()方法

@Override
 public String toString() {
    StringBuilder returnValue = new StringBuilder().append(hashcode).append(':').append(checksum);
    for (int i = 0; i < updateList.size(); i++) {
      returnValue.append(':').append(updateList.get(i));
    }

    return returnValue.toString();
}

根据以上代码,我们可以得出结论,CacheKey主要有一下几部分组合,并生成了一个hash码:

  • statementId
  • rowBounds.offset和rowBounds.limit 查询时要求的结果集中的结果范围
  • Sql语句字符串 : boundSql.getSql()
  • 传递给JDBC的参数 : parameterMappings

注意:doUpdate方法的最后一行updateList.add(object);
以及toString方法的returnValue.append(’:’).append(updateList.get(i));
含义为:不同的请求有极小的概率会生成相同的hash码,在toString方法中生成hash的过程中加入每个对象的实际内容,作为区分。

生命周期

MyBatis在开启一个数据库会话时,会创建一个新的SqlSession对象,SqlSession对象持有一个Executor对象,
Executor对象持有一个PerpetualCache对象;

  • 当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。

  • 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。

  • 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。

  • SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用。

存在问题

  • 一级缓存并没有做粒度控制,而是在一次会话中对数据库中所有表的sql操作均做了缓存,这显然是不合理的。
  • 任何更新(增、删、改)操作都会清除一级缓存,其实在有些情况下,这完全没有必要,比如两个完全不相关的表1和表2,如果你更新了表1,那为什么也要把表2的缓存也清除呢?
  • 即便更新操作置空了当前SqlSession的一级缓存,也只是能够保证当前SqlSession的缓存失效,保证数据一致性,但是也不能保证其他SqlSession的缓存数据失效,这会导致数据不一致问题。
  • 一级缓存只适用于单点,而不支持分布式部署。

综合以上一级缓存存在的问题,Mybatis设计开发了二级缓存,我们将在下一篇文章中介绍。

温馨提示

  • 如果您对本文有疑问,请在评论部分留言,我会在最短时间回复。
  • 如果本文帮助了您,也请评论,作为对我的一份鼓励。
  • 如果您感觉我写的有问题,也请批评指正,我会尽量修改。
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页
实付 9.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值