MyBatis框架原理3:缓存

上一篇[MyBatis框架原理2:SqlSession运行过程][1]介绍了MyBatis的工作流程,其中涉及到了MyBatis缓存的使用,首先回顾一下工作流程图:

如果开启了二级缓存,数据查询执行过程就是首先从二级缓存中查询,如果未命中则从一级缓存中查询,如果也未命中则从数据库中查询。MyBatis的一级和二级缓存都是基于Cache接口的实现,下面先来看看Cache接口和其各种实现类。

Cache接口及常用装饰器

public interface Cache {
  String getId();
  //缓存中添加数据,key为生成的CacheKey,value为查询结果
  void putObject(Object key, Object value);
  //查询
  Object getObject(Object key);
  //删除
  Object removeObject(Object key);
  //清空缓存
  void clear();
  //获取缓存数量
  int getSize();
  //获取读写锁
  ReadWriteLock getReadWriteLock();
}

Cache接口位于MyBatis的cache包下,定义了缓存的基本方法,其实现类采用了装饰器模式,通过实现类的组装,可以实现操控缓存的功能。cache包结构如下:

  • PerpetualCache是Cache接口的实现类,通过内部的HashMap来对缓存进行基本的操作,通常配合装饰器类一起使用。
  • BlockingCache装饰器:保证只有一个线程到数据库中查询指定key的数据,如果该线程在BlockingCache中未查找到数据,就获取key对应的锁,阻塞其他查询这个key的线程,通过其内部ConcurrentHashMap来实现,源码如下:
public class BlockingCache implements Cache {

  //阻塞时长
  private long timeout;
  private final Cache delegate;
  //key和ReentrantLock对象一一对应
  private final ConcurrentHashMap<Object, ReentrantLock> locks;

  @Override
  public Object getObject(Object key) {
    //获取key的锁
    acquireLock(key);
    //根据key查询
    Object value = delegate.getObject(key);
    //如果命中缓存,释放锁,未命中则继续持有锁
    if (value != null) {
      releaseLock(key);
    }
    return value;
  }
 @Override
  //从数据库获取结果后,将结果放入BlockingCache,然后释放锁
  public void putObject(Object key, Object value) {
    try {
      delegate.putObject(key, value);
    } finally {
      releaseLock(key);
    }
  }
...
  • FifoCache装饰器: 先入先出规则删除最早的缓存,通过其内部的Deque实现。
  • LruCache装饰器: 删除最近使用最少的缓存, 通过内部的LinkedHashMap实现。
  • SynchronizedCache装饰器:同步Cache。
  • LoggingCache装饰器: 提供日志功能,记录和输出缓存命中率。
  • SerializedCache装饰器:序列化功能。

CacheKey

CacheKey对象是用来确认缓存项的唯一标识,由其内部ArrayList添加的所有对象来确认两个CacheKey是否相同,通常ArrayList内将添加MappedStatement的id,SQL语句,用户传递给SQL语句的参数以及查询结果集范围RowBounds等,CacheKey源码如下:

public class CacheKey implements Cloneable, Serializable {
...
  private final int multiplier;
  private int hashcode;
  private long checksum;
  private int count;
  private List<Object> updateList;

  public CacheKey() {
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLYER;
    this.count = 0;
    this.updateList = new ArrayList<Object>();
  }

  //向updateLis中添加对象
  public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); 

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

    hashcode = multiplier * hashcode + baseHashCode;

    updateList.add(object);
  }

  @Override
  //重写equals方法判断CacheKey是否相同
  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;
    }
    //比较updateList中每一项
    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;
  }
}

一级缓存

一级缓存是session级别缓存,只存在当前会话中,在没有任何配置下,MyBatis默认开启一级缓存,当一个SqlSession第一次执行SQL语句和参数查询时,将生成的CacheKey和查询结果放入缓存中,下一次通过相同的SQL语句和参数查询时,就会从缓存中获取,当进行更新或者插入操作时,一级缓存会进行清空。在上一篇中说到,MayBatis进行一级缓存查询和写入是由BaseExecutor执行的,源码如下:

  • 初始化缓存:

    一级缓存是Cache接口的PerpetualCache实现类对象

public abstract class BaseExecutor implements Executor {

  ...
  protected PerpetualCache localCache;
  protected PerpetualCache localOutputParameterCache;
  protected Configuration configuration;

  protected int queryStack;
  private boolean closed;

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

    key在CachingExecutor中生成,CacheKey的updateList中放入了MappedStatement,传入SQL的参数,结果集范围rowBounds和boundSql:

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
  • 将查询结果和CacheKey放入缓存:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    //缓存中放入CacheKey和占位符
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      //在数据库中查询操作
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    //缓存中放入CacheKey和结果集
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    //返回结果
    return list;
  }
  • 再次执行相同查询条件时从缓存获取结果:
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;
  }
  • 更新操作时清空缓存:
 public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    //清空缓存
    clearLocalCache();
    return doUpdate(ms, parameter);
  }

通过以下代码验证下,分别开两个session进行相同的查询,第一个session查询两次:

public void testSelect() {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        User user = sqlSession.selectOne("findUserById", 1);
        System.out.println(user);
        User user2 = sqlSession.selectOne("findUserById", 1);
        System.out.println(user2);
        sqlSession.close();
        System.out.println("sqlSession closed!===================================");
        //新建会话
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        User user3 = sqlSession2.selectOne("findUserById", 1);
        System.out.println(user3);
        sqlSession2.close();
    }

把日志设置为DEBUG级别得到运行日志:

DEBUG [main] - ==>  Preparing: SELECT * FROM user WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [[email protected]]
DEBUG [main] - Closing JDBC Connection [[email protected]]
DEBUG [main] - Returned connection 369241501 to pool.
sqlSession closed!===================================
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Checked out connection 369241501 from pool.
DEBUG [main] - Setting autocommit to false on JDBC Connection [[email protected]]
DEBUG [main] - ==>  Preparing: SELECT * FROM user WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [[email protected]]
DEBUG [main] - Closing JDBC Connection [[email protected]]
DEBUG [main] - Returned connection 369241501 to pool.

第一次会话中,虽然查询了两次id为1的用户,但是只执行了一次SQL,关闭会话后开启一次新的会话,再次查询id为1的用户,SQL再次执行,说明了一级缓存只存在SqlSession中,不同SqlSession不能共享。

二级缓存

二级缓存是Mapper级别缓存,也就是同一Mapper下不同的session共享二级缓存区域。

只需要在XML映射文件中增加cache标签或cache-ref标签标签就可以开启二级缓存,cache-ref标签配置的是共享其指定Mapper的二级缓存区域。具体配置信息如下:

  • blocking : 是否使用阻塞缓存
  • readOnly : 是否只读
  • eviction: 缓存策略,可指定Cache接口下装饰器类FifoCache、LruCache、SoftCache和WeakCache
  • flushInterval : 自动刷新缓存时间
  • size : 设置缓存个数
  • type : 设置缓存类型,用于自定义缓存类,默认为PerpetualCache

二级缓存是在MyBatis的解析配置文件时初始化,在XMLMapperBuilder中将缓存配置解析:

private void cacheElement(XNode context) throws Exception {
    if (context != null) {
      //指定默认类型为PerpetualCache
      String type = context.getStringAttribute("type", "PERPETUAL");
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      //默认缓存策略为LruCache
      String eviction = context.getStringAttribute("eviction", "LRU");
      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      Long flushInterval = context.getLongAttribute("flushInterval");
      Integer size = context.getIntAttribute("size");
      boolean readWrite = !context.getBooleanAttribute("readOnly", false);
      boolean blocking = context.getBooleanAttribute("blocking", false);
      Properties props = context.getChildrenAsProperties();
      //委托builderAssistant构建二级缓存
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }

构建过程:

public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    Cache cache = new CacheBuilder(currentNamespace)
        //设置缓存类型,默认为PerpetualCache
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        //设置缓存策略,默认使用LruCache装饰器
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        //设置刷新时间
        .clearInterval(flushInterval)
        //设置大小
        .size(size)
        //设置是否只读
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
  }

最终得到默认的二级缓存对象结构为:

CachingExecutor将初始化的Cache对象用TransactionalCache包装后放入TransactionalCacheManager的Map中,下面代码中的tcm就是TransactionalCacheManager对象,CachingExecutor执行二级缓存操作过程:

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    //从Configuration的MappedStatement中获取二级缓存
    Cache cache = ms.getCache();
    if (cache != null) {
     //判断是否需要刷新缓存,SELECT不刷新,INSERT|UPDATE|DELETE刷新缓存
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        //从二级缓存中获取数据
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          //委托BaseExecutor查询
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          //查询结果放入二级缓存
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

通过之前一级缓存的例子验证二级缓存,只需要在UserMapper映射文件中加入cache标签,并且让相关POJO类实现java.io.Serializable接口,运行得到日志:

DEBUG [main] - ==>  Preparing: SELECT * FROM user WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
DEBUG [main] - Cache Hit Ratio [com.kkb.mybatis.mapper.UserMapper]: 0.0
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [[email protected]]
DEBUG [main] - Closing JDBC Connection [[email protected]]
DEBUG [main] - Returned connection 1543974463 to pool.
sqlSession closed!===================================
DEBUG [main] - Cache Hit Ratio [com.kkb.mybatis.mapper.UserMapper]: 0.3333333333333333
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]

不同session查询同一条记录时,总共只执行了一次SQL语句,并且日志打印出了缓存的命中率,这时候不同session已经共享了二级缓存区域。

[1]: https://www.cnblogs.com/abcboy/p/9656302.html

原文地址:https://www.cnblogs.com/abcboy/p/9688961.html

时间: 2024-10-13 13:41:26

MyBatis框架原理3:缓存的相关文章

2.MyBatis 框架原理

1.1     Mybatis框架原理 1.1.1 框架图 通过框架图分析mybatis的原理 1.1.2 分析结论 1.  mybatis配置文件,包括数据库连接.事物管理等信息. 2.  mybatis通过配置文件信息,构造出SqlSessionFactory即会话工厂. 3.  通过SqlSessionFactory,可以创建SqlSession即会话.Mybatis是通过SqlSession来操作数据库的. 4.  SqlSession是通过底层的Executor执行器接口来操作数据库的

《深入理解mybatis原理》 MyBatis的一级缓存实现详解 及使用注意事项

0.写在前面 MyBatis是一个简单,小巧但功能非常强大的ORM开源框架,它的功能强大也体现在它的缓存机制上.MyBatis提供了一级缓存.二级缓存 这两个缓存机制,能够很好地处理和维护缓存,以提高系统的性能.本文的目的则是向读者详细介绍MyBatis的一级缓存,深入源码,解析MyBatis一级缓存的实现原理,并且针对一级缓存的特点提出了在实际使用过程中应该注意的事项. 读完本文,你将会学到: 1.什么是一级缓存?为什么使用一级缓存? 2.MyBatis的一级缓存是怎样组织的?(即SqlSes

使用redis作为mybatis的二级缓存

本次介绍一下使用mybatis-redis项目作为mybatis的二级缓存在生产项目中的配置与应用. 首先,在pom中添加一下依赖: <!-- mybatis cache --> <dependency>     <groupId>org.mybatis.caches</groupId>     <artifactId>mybatis-redis</artifactId>     <version>1.0.0-beta2&

mybatis0210 mybatis和ehcache缓存框架整合

1.1mybatis和ehcache缓存框架整合 一般不用mybatis来管理缓存而是用其他缓存框架在管理缓存,因为其他缓存框架管理缓存会更加高效,因为别人专业做缓存的而mybatis专业做sql语句的,mybatis二级缓存通过ehcache维护缓存数据. 1.1.1分布缓存 将缓存数据数据进行分布式管理.用户发起请求,首先会根据负载选择不同的服务器,如果用户在服务器1和服务器2都登录过,那么把用户的session分别放在服务器1和服务器2是不行的,所以就把用户的信息放在远程服务器集群中统一管

mybatis的sql 缓存,去除mybatis缓存

第二次用到mybaits,还是被同一个问题坑了几个小时,所以一定要把这个问题分享给大家.网友很多都是说了一大堆的配置,都是在mybatis.xml中配置.但是,我是没有用mybatis.xml的,我的配置都是在每个**mapper.xml中配置,无非就是一个关联实体类的路径,和一个别名. 在mybatis框架中,在SqlSession未关闭之前,在一个session里面,如果执行相同的select语句,mybatis不会重新查询数据库,而是直接返回缓存在内存中的查询结果,这个是与MyBatis的

MyBatis学习--查询缓存

简介 以前在使用Hibernate的时候知道其有一级缓存和二级缓存,限制ORM框架的发展都是互相吸收其他框架的优点,在Hibernate中也有一级缓存和二级缓存,用于减轻数据压力,提高数据库性能. mybaits提供一级缓存和二级缓存结构如下图: 可以看出一级缓存是sqlSession级别的,而二级缓存是Mapper级别的,同一个Mapper中的多个sqlSession可以共享缓存数据. 一级缓存是SqlSession级别的缓存.在操作数据库时需要构造 sqlSession对象,在对象中有一个数

Mybatis的二级缓存注意点

--声明:一下内容都不一定是正确的,只是自己测试的结果,请自己的动手操作得出自己的结论 1.开启Mybatis的二级缓存,不仅要在SqlMapConfig.xml中进行开启总开关,还要在对应的XXXMapper.xml中开启,缺少其中一个二级缓存都不能开启(起不到二级缓存的作用): 2.开启Mybatis的二级缓存后,一级缓存同样起作用(相同的SqlSession一级缓存,不同的SqlSession二级缓存) 3.一级缓存,只要执行了增删改,不管有没有提交,都会清空缓存,后面如果还有相同sql的

使用Redis做MyBatis的二级缓存

1. 介绍 使用mybatis时可以使用二级缓存提高查询速度,进而改善用户体验. 使用redis做mybatis的二级缓存可是内存可控<如将单独的服务器部署出来用于二级缓存>,管理方便. 2. 使用思路 2.1 配置redis.xml 设置redis服务连接各参数 2.1 在配置文件中使用 <setting> 标签,设置开启二级缓存: 2.2 在mapper.xml 中使用<cache type="com.demo.RedisCacheClass" /&g

MyBatis的二级缓存的设计原理

MyBatis的二级缓存是Application级别的缓存,它可以提高对数据库查询的效率,以提高应用的性能.本文将全面分析MyBatis的二级缓存的设计原理. 1.MyBatis的缓存机制整体设计以及二级缓存的工作模式 如上图所示,当开一个会话时,一个 SqlSession对象会使用一个 Executor对象来完成会话操作, MyBatis的二级缓存机制的关键就是对这个 Executor对象做文章.如果用户配置了" cacheEnabled=true",那么 MyBatis在为 Sql