记录一次mybatis localCache引起的bug

记录一次mybatis localCache引起的bug

情况描述:有一个多次查询条件相同,查询结果脏读的bug。多次查询在同一个事务中.这里进行源码分析。
这里没有过多描述我的环境配置,更加关注源码的实现。

必备知识

  • java基本语法了解
  • hashMap理解原理,以及了解java实现.

这里主要要介绍几个

  • SqlSession
  • Executor
  • Cache
  • Transaction

下面使用语言描述其关系

sqlSession:应该可以说是mybatis的核心了,执行方法全在里面.Executor Cache Transaction都是它的功能.
Executor:执行器,真正的sql语句在其中。transaction就是它的属性。这次的bug,也是出现在这里的.就是因为同一个SqlSession.
Cache:缓存,这一篇幅只讨论PerpetualCache localCache,它的实现很简单就是一个hashMap还有一个name名称.
Transaction:负责事务部分.

类结构图

cache
sqlSession
executor

bug发生原因

我们进入BaseExecutor的select方法来看就一目了然了。


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

  @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();
      }
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache();
      }
    }
    return list;
  }

    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);
    }
    //把查询结果插入缓存  上面清空了  也相当于 刷新了一遍刷新缓存 为什么不直接putObject?这里也就是出现问题的,根源,直接把查询结果引用返回给我了。
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

然后我还阅读了,mybatis-Spring的文档。这样写到。
在事务处理期间,一个单独的 SqlSession 对象将会被创建 和使用。当事务完成时,这个 session 会以合适的方式提交或回滚。
前面我也说了,我的整个流程在一个事务中。通过上面的源码,给我的感觉它除了最开始的查询是走了数据库,之后就全走了缓存。在我的业务代码中,我和localCache对象同时持有一个对象。我还修改了对象中的,数据。造成了后续流程的错误。

解决方案

  • 执行查询前,调用BaseExecutor.clearLocalCache清空缓存
  • 拆分大事务为,一个个小事务。

bug总结

事务臃肿
对mybatis的不熟悉

发现了,不正确的地方,请不要吝啬您的评论。特此感谢,一起进步。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值