mybatis 的缓存坑

背景

严选供应商系统需要对供应商数据库进行加密,插入数据时加密插入,select后解密。发现有一个Dao操作查询出来的数据已经是被解密的导致异常。

排查

怀疑是某个缓存导致。因此先验证:

  1. 对select出来的对象拷贝再解密。发现问题消失。
  2. revert对象拷贝的代码,再次部署,问题出现。

因此基本可以判断是缓存的问题。

代码分析

通过跟踪selectByExample的代码,发现在BaseExecutor的query方法里有如下代码:

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

即它会优先从localCache里查找,找不到则查询数据库。localCache结构如下:

Map<Object, Object> cache

再看查询数据库的实现:

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

这里查询结果被放到了localCache里。

那么问题来了,为什么在测试过程中连续2次select没有命中这个缓存。 来看一下缓存被清空的时机:

  public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }

下个断点,发现在 sqlSession.commit(true); 时被调到。也就是说,当两次select在同一个事务里时,缓存将会命中

通过对测试代码加@Transactional注解,复现了这个问题,通过debug观察两次返回的对象其实是同一个对象,这也是由于Java语言的特性。

最后又顺便验证了delete或update操作是会清空缓存的。

总结

  • mybatis 会使用map缓存查询结果。
  • 缓存会在commit时 或者 update/delete 操作时被清空。
  • 如果命中了缓存,mybatis不会进行拷贝,而是直接返回缓存对象的引用。

因此,要防止以下这种代码:

@Transactional
fooBug() {
    op();
    ...
    op();
}

op() {
    Foo f = dao.selectFoo(id);
    if (f.flag == 0) {
        // 第二次将执行不到这里
        f.flag = 1;
        ...
    }
}

这种情况下,前一次op()操作将会对下一次产生影响。 由于被封装到了一个函数里,很难发现问题的原因。另外, 这个缓存似乎没有开关可以关掉。

本文来自网易实践者社区,经作者郁利涛授权发布。