MyBatis缓存机制原理

本文参考:http://blog.csdn.net/luanlouis/article/details/41390801


Mybatis为了方式用户频繁的进行数据库查询操作。导致性能的下降,内部封装了缓存对象,通过缓存的机制,来提高查询的速度和效率。

Mybatis缓存分为两级:一级缓存和二级缓存


一级缓存

一级缓存是会话级别的缓存,通过Cache对象来进行封装查询标识与查询结果。由于在数据库操作时,是通过创建sqlSession会话来实现的。所以一级缓存的生命周期从创建SqlSesion开始到销毁SqlSession结束,并且不允许用户自己进行配置。

一级缓存是怎样实现的呢?

当用户创建sqlsession时会创建一个Executor对象,Executor创建一个cache对象,执行一个sql查询时,首先会根据查询的唯一标识到缓存中查找,如果找到结果集,则直接返回给用户,否则到数据库当中进行查找,将查询到的结果缓存到Cache中然后返回。

一级缓存示意图

那么从用户操作,缓存存储,缓存查询,返回结果集,这一部分到底是如何来完成的呢?

前面说到,通过mybatis的一系列操作,都是从创建一个sqlsession会话开始的。
我们只需要通过sqlsession调用select,insert等操作就能获取到信息。
其实在mybatis中,sqlsession只是一个调用的接口,sqlSession创建时会创建一个Executor执行器,由这个执行器来执行操作。
而缓存的信息就存在这个执行器里,并且把对缓存的添加删除操作封装到了Cache对象当中。他们之间的关系如下图所示。

这里写图片描述

Executor接口的实现类BashExector拥有PerpetualCache对象,通过这个对象来对缓存进行维护。而PerpetualCache又实现了Cache接口

PerpetualCache代码实现其实非常简单。底层的结构就是通过一个HashMap来完成的,通过Key来存储查询操作的唯一标识,通过Value存储查询的结果集

实际上一级缓存就是通过PerpetualCache这个实现类来维护的。

package org.apache.ibatis.cache.impl;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;

public class PerpetualCache implements Cache {

  private String id;

  private Map<Object, Object> cache = new HashMap<Object, Object>();

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

  public String getId() {
    return id;
  }

  public int getSize() {
    return cache.size();
  }

  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  public Object getObject(Object key) {
    return cache.get(key);
  }

  public Object removeObject(Object key) {
    return cache.remove(key);
  }

  public void clear() {
    cache.clear();
  }

  public ReadWriteLock getReadWriteLock() {
    return null;
  }

  public boolean equals(Object o) {
    if (getId() == null) throw new CacheException("Cache instances require an ID.");
    if (this == o) return true;
    if (!(o instanceof Cache)) return false;

    Cache otherCache = (Cache) o;
    return getId().equals(otherCache.getId());
  }

  public int hashCode() {
    if (getId() == null) throw new CacheException("Cache instances require an ID.");
    return getId().hashCode();
  }
}

缓存的生存周期从sqlsession会话创建直到销毁结束。
用户也可以手动的来调用sqlsession.close()来释放并关闭一级缓存。一级缓存将不可用。
如果调用sqlsession.clearCahce();方法,将会释放掉当前存储的所有缓存内容。但是一级缓存还是可用的。
由于对数据库进行update操作时,会改变数据内容,所以会清空掉缓存内容。一级缓存依然可用。


整个一级缓存的工作流程前面已经说过了

  1. 根据查询条件获得唯一查询标识key。
  2. 通过key到Cache中获取结果集。
  3. 如果查询到直接返回给用户。
  4. 如果未查询到则到数据库中获取,存入缓存中,返回给用户。

这里写图片描述


接下来就是最重点的内容。cache既然通过一个HashMap来维护缓存,那么缓存的key值,也就是唯一的查询条件是如何确定的呢?为何cache能够匹配到两次相同的查询。

缓存的key值,其实是由四部分来组成的:

1、statementID:也就是在mybatis中你要执行的sql语句唯一标识。(XML文件中配置的sql语句id )
2、rowBounds:查询时返回的结果集的范围。Mybatis自带的分页功能就是由rowBounds来实现的,通过rowBounds.offset和rowBounds.limit来过滤查询的结果集。
3、boundSql:就是最终通过xml映射参数之后生成的sql查询语句。 boundSql.getSql()。
4、preparedStatement:传递给JDBC的参数值。(并不是Dao方法所传递的参数值,而是在实际映射sql时的参数值,例如XML中 id = #{id} )

CacheKey由上述四部分来进行生成,这样在下次的查询中,如果判断这四个条件一样,则认为两次的查询是一致的。

由于preparedStatement参数值并不是接口中传入的,而是在JDBC 通过sql映射时的参数,所以如果多几个无效的参数并不影响缓存的key值判断。

CacheKey的生成源码在Executor的实现类BaseExecutor中


  /**
   * 所属类:  org.apache.ibatis.executor.BaseExecutor
   * 功能   :   根据传入信息构建CacheKey
   */
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) throw new ExecutorException("Executor was closed.");
    CacheKey cacheKey = new CacheKey();
    //1.statementId
    cacheKey.update(ms.getId());
    //2. rowBounds.offset
    cacheKey.update(rowBounds.getOffset());
    //3. rowBounds.limit
    cacheKey.update(rowBounds.getLimit());
    //4. SQL语句
    cacheKey.update(boundSql.getSql());
    //5. 将每一个要传递给JDBC的参数值也更新到CacheKey中
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    for (int i = 0; i < parameterMappings.size(); i++) { // mimic DefaultParameterHandler logic
      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);
        }
        //将每一个要传递给JDBC的参数值也更新到CacheKey中
        cacheKey.update(value);
      }
    }
    return cacheKey;
  }    

  //而cacheKey的HashCode生成源码:
  public void update(Object object) {
    if (object != null && object.getClass().isArray()) {
      int length = Array.getLength(object);
      for (int i = 0; i < length; i++) {
        Object element = Array.get(object, i);
        doUpdate(element);
      }
    } else {
      doUpdate(object);
    }
  }

  private void doUpdate(Object object) {

    //1. 得到对象的hashcode;  
    int baseHashCode = object == null ? 1 : object.hashCode();
    //对象计数递增
    count++;
    checksum += baseHashCode;
    //2. 对象的hashcode 扩大count倍
    baseHashCode *= count;
    //3. hashCode * 拓展因子(默认37)+拓展扩大后的对象hashCode值
    hashcode = multiplier * hashcode + baseHashCode;
    updateList.add(object);
  }

由于Cache的数据结构只是通过一个HashMap来进行存储的,所以不存在是否过期的判断和更新缓存的实现。

由于一级缓存生存周期是根据sqlsession来确定的。一般sqlSession生存周期比较端,所以一般不会发生内存溢出等问题。
用户当执行对表的update操作时,会清空一级缓存。
用户也可以通过clearCache()方法来清空一级缓存。

二级缓存:

一级缓存是会话级别的,而二级缓存是存在整个应用级别的。
优先级:二级缓存 > 一级缓存 > DB 。
整个缓存的工作模式如图所示:

这里写图片描述

如上图所示,用户在执行一个操作时,首先会创建一个sqlSession会话,然后sqlSession会创建一个Executor对象来进行具体的执行操作。

但是,如果用户在配置文件中通过 cacheEnable = true 的开关开启了二级缓存,这时会创建一个CachingExecutor对象来进行具体的执行操作,而CachingExecutor实现了Executor,是Executor的装饰者。这里采用了设计模式中的装饰者模式。CachingExecutor首先会根据查询的标识到二级缓存中查询结果,如果查询直接返回,否则交给Executor去查询( 一级缓存查询流程 )。

二级缓存的划分流程更加细致,mybatis可以对Mapper级别进行缓存的开启。也就是每个Mapper一个缓存,通过节点配置。(这个Mapper在实际中就是对一个表的curd操作,这样可以对每个表都进行单独的缓存操作)也可以多个mapper共用一个缓存,通过节点配置。

这样可以总结,如果想要对一个mapper使用二级缓存必须要的几个配置:

1、配置myBatis二级缓存开关。cacheEnable = true
2、在Mapper中配置 或节点。
3、在select语句上,开启参数useCache = true。

解释一下第三点,如果不开启useCache = true,mybatis不会对所有的查询进行缓存。

Mybatis允许用户自定义实现二级缓存,只要实现org.apache.ibatis.cache.Cache接口,然后在节点的type上配置即可。
Mybatis也允许集成第三方缓存库。

Mybatis自身提供了多种缓存的刷新策略。

1、LRU ,最少使用算法,当cache容量满的时候,将最少使用的删除。
2、FIFO,先进先出算法,容量满时,最先添加的最先删除。
3、Scheduled,指定时间算法,指定一个时间间隔来将cache数据删除。

在实际的工作中,一般会使用第三方的缓存库。例如持久化的redis等。

  • 8
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值