Mybatis系列之一级/二级缓存

缓存的概念大家应该都知道,所以,这里我们基于ORM框架Mybatis,来讲解一下他自带的缓存


一级缓存介绍

一级缓存是Mybatis默认开启的一个缓存机制,它跟二级缓存的区别就在于作用域大小不同,一级缓存的作用域相对比二级缓存要小,它的作用域只是基于SqlSession的(SqlSession主要是啥,后面再补充),缓存的存在主要是为了便利我们的数据查询,废话不多说,接下来我们来体验一下


一级缓存代码体验

源码

 //根据 sqlSessionFactory 产⽣ session
 SqlSession sqlSession = sessionFactory.openSession();

 UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

 //第⼀次查询,发出sql语句,并将查询出来的结果放进缓存中
 User u1 = userMapper.selectUserByUserId(1);
 System.out.println(u1);

 //第⼆次查询,由于是同⼀个sqlSession对象,所以会在缓存中查询结果
 //有两种处理逻辑,缓存如果有,则直接从缓存中取出来,不会走数据库,反之直接走数据库
 User u2 = userMapper.selectUserByUserId(1);
 System.out.println(u2);

 sqlSession.close();

观察日志

然后我们对user表在进行两次查询,和上面代码的区别就在于,在两次查询中间进行以此修改,再观察一下日志打印情况,先上源码:

 //根据 sqlSessionFactory 产⽣ session
 SqlSession sqlSession = sessionFactory.openSession();

 UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

 //第⼀次查询,发出sql语句,并将查询的结果放⼊缓存中
 User u1 = userMapper.selectUserByUserId( 1 );
 System.out.println(u1);

 //第⼆步进⾏了⼀次更新操作,sqlSession.commit()
 u1.setSex("⼥");
 userMapper.updateUserByUserId(u1);
 sqlSession.commit();

 //第⼆次查询,由于是同⼀个sqlSession,且上面的修改操作触发了sqlSession.commit(),
 //所以在commit之后会清空缓存信息
 //则此次查询也会发出sql语句
 User u2 = userMapper.selectUserByUserId(1);
 System.out.println(u2);

 sqlSession.close();

日志打印:

这两次源码进行查询的代码和日志对比就发现了:如果执行了新增、更新或者删除,sqlSession就会commit,默认会清空SqlSession中的一级缓存,这样做的目的,大家了解缓存的都清楚,这么做就是为了防止读取的数据不是最新的,避免了脏读


源码剖析 

1、查看SqlSession类中的方法

public interface SqlSession extends Closeable {
  <T> T selectOne(String statement);
  <T> T selectOne(String statement, Object parameter);
  <E> List<E> selectList(String statement);
  <E> List<E> selectList(String statement, Object parameter);
  <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);
  <K, V> Map<K, V> selectMap(String statement, String mapKey);
  <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey);
  <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds);
  <T> Cursor<T> selectCursor(String statement);
  <T> Cursor<T> selectCursor(String statement, Object parameter);
  <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds);
  void select(String statement, Object parameter, ResultHandler handler);
  void select(String statement, ResultHandler handler);
  void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler);
  int insert(String statement);
  int insert(String statement, Object parameter);
  int update(String statement);
  int update(String statement, Object parameter);
  int delete(String statement);
  int delete(String statement, Object parameter);
  void commit();
  void commit(boolean force);
  void rollback();
  void rollback(boolean force);
  List<BatchResult> flushStatements();
  @Override
  void close();
  void clearCache();
  Configuration getConfiguration();
  <T> T getMapper(Class<T> type);
  Connection getConnection();
}

乍看一下这些方法,能找到的唯一跟缓存有关系的也就是倒数第四个方法clearCache,看方法名就是清理缓存,既然只有这个方法跟缓存有关系,那我们就从它开始分析,它的父类的调用流程这里就不展示了

只需要知道有一个类PerpetualCache,看一下源码

public class PerpetualCache implements Cache {

  private String id;

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

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

  @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }

  @Override
  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());
  }

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

}

最开始会声明一个HashMap的全局变量,然后还有一个clear方法,看进去其实它调用的是上面HashMap对象的clear,清空一级缓存其实就是清空Map数据,一级缓存其实就是本地存放的一个Map对象

那清空缓存ok了,创建缓存是谁负责在什么时候创建的呢?其他拐弯抹角的话不说了,我们直接看执行器Executor

public interface Executor {

  ResultHandler NO_RESULT_HANDLER = null;

  int update(MappedStatement ms, Object parameter) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

  List<BatchResult> flushStatements() throws SQLException;

  void commit(boolean required) throws SQLException;

  void rollback(boolean required) throws SQLException;

  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

  boolean isCached(MappedStatement ms, CacheKey key);

  void clearLocalCache();

  void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);

  Transaction getTransaction();

  void close(boolean forceRollback);

  boolean isClosed();

  void setExecutorWrapper(Executor executor);

}

上面的类中,有一个createCacheKey,看名字就知道是创建缓存,我们点进去看一下这个方法的实现

@Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    //封装mapper.xml中我们写标签中的namespace+id的值,<select id="">
    cacheKey.update(ms.getId());
    //0
    cacheKey.update(Integer.valueOf(rowBounds.getOffset()));
    cacheKey.update(Integer.valueOf(rowBounds.getLimit()));
    //我们自己写的sql语句
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    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);
        }
        //拼接sql中的参数
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

这个value最终会是什么呢,主要是五个参数,分别是上面源码注释标记的那几个参数,还有数据库连接的配置,最后还有一个if,其中的update入参是configuration.getEnvironment().getId(),他就是数据库驱动配置中环境标签的id值,具体如下

<environments default="development"> 
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
             <property name="driver" value="${jdbc.driver}"/>
             <property name="url" value="${jdbc.url}"/>
             <property name="username" value="${jdbc.username}"/>
             <property name="password" value="${jdbc.password}"/>
         </dataSource>
    </environment>
</environments>

到目前为止,缓存是创建完了,我们看一下他是怎么用于查询的,还是Executor类,他里面有一个query方法,我们点进去看一下

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

我们看一下else中直接走数据库的方法实现

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    //创建缓存对象
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      //执行查询
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    //填充缓存数据
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

整体的逻辑就是:如果走数据库查询,会先创建缓存key,把数据库返回的查询结果缓存到对应的key中


到此,一级缓存的介绍、体验和源码剖析就结束啦 


二级缓存介绍

二级缓存和一级缓存大多数实现逻辑基本一样,都是第一次查询先把数据放缓存,第二次查询会先走缓存,缓存命中就从缓存拿数据,否则就直接走数据库拿数据,然后再缓存起来,唯一的区别在上面的一级缓存中也提到了:作用域不同,一级缓存的作用域是SqlSession,而二级缓存择时namespace,就是写sql的那个xml文件中的namespace,作用域在这的话,就说明多个SqlSession可以共享一个mapper中的二级缓存,并且如果两个mapper的namespace相同的话,不管他是几个xml文件,只要namespace相同,那么他们缓存就可以共享,创建,清空,查询都是同一个


开启二级缓存

一级缓存是默认开启的,二级缓存是默认关闭的,开启二级缓存有好几种方式

1、在配置xml中添加对应开启的配置

<!--开启⼆级缓存-->
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

2、在对应的mapper.xml中添加标签

<!--开启⼆级缓存-->
<cache></cache>

注意事项:开启二级缓存,需要对缓存的实体类进行序列化,实现Serializable接口,因为二级缓存数据可能存在内存中,还有可能会在硬盘里,所以我们要获取这个环境的话,就需要反序列化

参数讲解 

mybatis提供了userCache和flushCache等配置项

userCache:设置是否禁用二级缓存,默认是false,如果设置为true,每次查询不会再走缓存那一层,会直接走数据库

flushCache:默认是true,即刷新缓存,一般情况默认即可


缓存问题 

在分布式架构下,很少见到有用到二级缓存的,就哪怕是用了,两个或多个机器之间的二级缓存肯定是不共享的,但是有人会说:namespace不都是一样的嘛,那是没错,但是每个缓存所属的服务进程不一样啊,所以就造成了二级缓存不适用于分布式环境,那怎么解决呢?解决的入口就在于:怎么让Mybatis获取到唯一的缓存,且能够让所有的分布式机器共享这个缓存即可

解决方案:不适用mybatis自带的缓存机制,我们可以利用Mybatis自身拓展的机制,自己实现一个基于Redis的二级缓存即可 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值