mybaits一级缓存和二级缓存的实现

近来看了一下关于mybaits的一些缓存讲解的文章,然后自己也顺着文章看了一下mybaits底层的源码。

(本文部分内容转载于 美团的  聊聊MyBatis缓存机制 文章

目前而言,myabits是最流行的dao层框架,简洁,易上手,开箱即用,但是如果不了解一下底层的东西,可能会造成其他的问题。比如造成的脏数据

首先谈的肯定是mybaits的工作原理,大家都知道mybaits有一级缓存和二级缓存,一级缓存是默认开启的,二级缓存需要配置开启。

mybatis.configuration.cache-enabled = true

还有其他的配置,比如懒加载什么的。

 

一级缓存:

刚才提及到,一级缓存是默认开启的,所以我们开箱即用就好了。

当server端发起请求时,mybatis会默认先向缓存中请求结果,当缓存未命中时,像mysql查询结果,当从mysql查询到结果时,框架会把查询到的结果信息存到缓存中去,方便我们下次使用,然后返回给用户的是缓存中的信息

(Tips:mybaits每次返回的信息都是缓存中的数据,即使未命中缓存的情况下,mybaits也会将mysql查询的结果先放到缓存中,然后再从缓存中获取数据)

一级缓存最主要的是2个区别:对缓存范围的影响。默认是session级别的,就是当前mybaits执行的所有语句都有效,另一种是STATEMENT级别的,可以理解为缓存只对当前执行的这一个Statement有效。

刚才提交到mybaits会有脏数据的产生,具体的可以看下这个脏数据是怎么出现的。

@Test
public void testLocalCacheScope() throws Exception {
        SqlSession sqlSession1 = factory.openSession(true); 
        SqlSession sqlSession2 = factory.openSession(true); 

        StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

        System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
        System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
        System.out.println("studentMapper2更新了" + studentMapper2.updateStudentName("小岑",1) + "个学生的数据");
        System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
        System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}

从console中可以看到,我们再session2中更改了该条记录的值,但是session1所读取出来的值还是未更改之前的值,这就导致了“脏数据”的产生,同时验证了一级缓存的session范围性

附一张mybaits的工作流程图片,就是我上面陈述的部分

先看一下mybaits的底层实现原理,再看二级缓存的实现

mybaits的核心类是SqlSession,Executor,Cache

SqlSession的默认实现是DefaultSqlSession,该类提供的是用户与数据库之间的操作方法,不仅仅有curd,还有事务回滚和事务提交的操作。

Executor是一个更具体的操作,sqlSession负责下发用户的指令,Executor负责和数据库进行具体的交互。Executor大致可以分为两种实现类,一种是BaseExecutor(其他的实现类都是继承了这个抽象类),一种是CachingExecutor。前者主要用于一级缓存,后者是二级缓存。

Cache:顾名思义,和缓存有关。负责了所有有关缓存相关的操作。

介绍完了基本类,我们可以看下底层的实现逻辑。

首先我们需要通过DefaultSqlSessioFactory初始化一个sqlSession

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

在初始化的过程中会去new一个Executor

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);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

根据不同的ExecutorType会返回不同的执行器,但是这些类都是基于 BaseExecutor 的。

当 cacheEnabled 为true时,会new CachingExecutor 。这边是不是很眼熟,正是我们刚才需要开启二级缓存的注解,当设置二级缓存为 true 时,会返回一个不同的执行器。

拿select方法来说,当执行 sqlSession.selectOne() 方式时,都会执行到 selectList 方法,然后取到第一条

@Override
  public <T> T selectOne(String statement) {
    return this.<T>selectOne(statement, null);
  }

  @Override
  public <T> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    List<T> list = this.<T>selectList(statement, parameter);
    if (list.size() == 1) {
      return list.get(0);
    } else if (list.size() > 1) {
      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
      return null;
    }
  }

执行selectList方法时,正和我们刚才说的一样,会交给执行器去进行查询处理

MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
@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);
 }

当执行器去执行查询语句时候,我们注意到会先调用  createCacheKey() ,方法内部是对 CacheKey类的操作,

CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
// for循环对参数进行优化
cacheKey.update(value);
if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
}

将MappedStatement的Id、sql的offset、Sql的limit、Sql本身以及Sql中的参数传入了CacheKey这个类,最终构成CacheKey。

继续往下看query方法

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--;
    }
handleLocallyCachedOutputParameters 这个方法从其他地方看到是对于存储过程的使用。这个功力不够,还为能理解其中。

我们可以看到,query查询时候会优先根据刚才生成的cacheKey去缓存中查询,如果缓存命中,那么就取缓存中的数据,如果缓存未命中,那么会去查询dateBase

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所对应的缓存清除,将新的list放入缓存中,以便下次查询使用。下面判断statementType是为了在存储过程中获取相对应的参数。

sqlSession的insert和delete方法最后会执行update方法

@Override
  public int insert(String statement) {
    return insert(statement, null);
  }

  @Override
  public int insert(String statement, Object parameter) {
    return update(statement, parameter);
  }

@Override
  public int delete(String statement) {
    return update(statement, null);
  }

  @Override
  public int delete(String statement, Object parameter) {
    return update(statement, parameter);
  }

而update也是一样,sqlSession触发update方法,交给Executor去执行对应的update方法。每次update时,都会先清除缓存,清除的是执行结果的缓存和key,paramter的参数

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


@Override
  public void clearLocalCache() {
    if (!closed) {
      //清除缓存的结果
      localCache.clear();
      //清除缓存的参数
      localOutputParameterCache.clear();
    }
  }

上述就是mybaits的一级缓存的机制。

二级缓存的执行过程

二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。

二级缓存和一级缓存的本质区别刚才已经叙说了,主要就是加载的执行器不同,一级缓存的执行器都是基于 BaseExecutor的,而二级缓存是基于 CachingExecutor

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          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);
  }

每次在执行query时候,二级缓存都是根据MappedStatement是否含有缓存以及其子对象的属性去判断是否需要清空缓存

而后面的ensureNoOutParams同样也是基于存储过程的方法。

当缓存命中时,会从tcm中获取数据。如果tcm获取到数据,那么返回tcm的值,否则的话会给当前的cache设立一个标记位,防止报空指针错误。

如果未从tcm去获取到那么会继续走执行器的query步骤,这个执行器的query步骤就是相当于我们走回了一级缓存的query步骤,查询到数据以后会将数据再放入到tcm中。

所以,二级缓存最后的查询步骤都会回归于一级缓存的实际操作。

(什么是tcm?tcm即TransactionalCacheManager的简称,这个Map保存了Cache和用TransactionalCache包装后的Cache的映射关系。TransactionalCache实现了Cache接口,CachingExecutor会默认使用他包装初始生成的Cache,作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。在TransactionalCache的clear,有以下两句。清空了需要在提交时加入缓存的列表,同时设定提交时清空缓存)

总结

  1. MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
  2. MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
  3. 在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis,Memcached等分布式缓存可能成本更低,安全性也更高。
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值