前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Mybatis一级缓存问题

Mybatis一级缓存问题

作者头像
政采云前端团队
发布2023-12-20 15:17:43
1650
发布2023-12-20 15:17:43
举报
文章被收录于专栏:采云轩采云轩

MyBatis 缓存问题

引言

很多 Java 后端服务都选择使用 MyBatis 作为它们的 ORM 框架,帮助简化开发。但是基本上大家都不会太关心 MyBatis 的缓存机制,基本都在使用 MyBatis 缓存的默认配置,在不了解 MyBatis 缓存机制的情况下进行开发,可能就会发生一些意料之外的事情。下面帮助大家了解 MyBatis 一级缓存和二级缓存的机制,以及如何避免 MyBatis 缓存中的坑。

缓存

众所周知数据库的请求中大部分都是查询请求,使用缓存可以大大减少数据库的压力,提高系统的性能,但是如果使用不当可能就会产生数据一致性的问题。

一级缓存

MyBatis 的一级缓存又被叫做本地缓存,一级缓存默认作用在 Session 级别,并且不能被关闭,只能修改一级缓存的作用域。

一级缓存的作用域有两种,分别是 SESSIONSTATEMENT ,可以通过修改配置项 localCacheScope 来设置,默认的 SESSION 会缓存一个会话中执行的所有查询,用来加速重复的嵌套查询,当在这个会话中执行更新操作时则会清除缓存。如果设置成 STATEMENT 则值作用在执行语句中,当语句执行完成就会清除缓存。

问题

下面看一下两个一级缓存导致的问题:

问题 1

localCacheScope 被设置为 SESSION 的时候,并且当前服务有多个实例时就可能会导致查询到的数据不一致。

代码语言:javascript
复制
// 会话 1
SqlSession session1 = factory.openSession(true);
// 会话 2
SqlSession session2 = factory.openSession(true);

TemplateInfoMapper templateInfoMapper1 = session1.getMapper(TemplateInfoMapper.class);
TemplateInfoMapper templateInfoMapper2 = session2.getMapper(TemplateInfoMapper.class);

TemplateInfo a1 = templateInfoMapper1.get(1L);
TemplateInfo a2 = templateInfoMapper2.get(1L);
// 数据一致
Assert.assertEquals(a1.getTemplateName(), a2.getTemplateName());

a1.setTemplateName("a1");
templateInfoMapper1.updateByPrimaryKey(a1);

TemplateInfo b1 = templateInfoMapper1.get(1L);
TemplateInfo b2 = templateInfoMapper2.get(1L);
// 数据不一致
Assert.assertEquals(b1.getTemplateName(), b2.getTemplateName());

上面这段代码模拟了多实例情况下数据不一致的场景。

问题 2

这个问题也是在被配置为 SESSION 时导致的问题,不过这个问题是在同一个会话当中发生的,下面我们看一段代码:

代码语言:javascript
复制
SqlSession session = factory.openSession(true);
TemplateInfoMapper templateInfoMapper = session.getMapper(TemplateInfoMapper.class);
TemplateInfo a1 = templateInfoMapper.get(1L);
a1.setTemplateName("a1");
TemplateInfo a2 = templateInfoMapper.get(1L);
a2.setTemplateName("a2");
// 数据一致
Assert.assertEquals(a1.getTemplateName(), a2.getTemplateName());
session.close();

上面这段代码我们分别获得结果集 a1 和 a2,并且分别对 a1 和 a2 结果集中的 templateName 进行了修改,但是最后比较时 a1 和 a2 的 templateName 反而数据是一致的。

原因

问题 1 中开启了两个会话(在分布式环境下,一个服务有多个实例很常见,这里可以把会话看作实例),每个会话都会有自己的一级缓存,也就是两个会话都会缓存 ID 等于 1 的数据到一级缓存当中,但是当 会话 1 去更新了 ID 为 1 的数据时,会话 1 中的一级缓存会被清理,会话 1 再去查询 ID 为 1 的数据时就会查询数据库,但是会话 2 中去查询 ID 为 1 的数据时还是会命中缓存,所以就会导致两个数据不一致的问题。

问题 2 在同一个会话当中第一次查询 ID 为 1 的数据时会把查询到的结果集对象放到一级缓存当中,当第二次查询 ID 为 1 的数据时会把缓存的对象直接返回,因为 MyBatis 的一级缓存使用的是 Java 的 HashMap 缓存数据这里返回的都是对象的引用地址,这就导致结果集 a1 和 a2 其实都指向了同一个对象,所以不管 a1 还是 a2 修改了对象的字段都会导致双方的数据被修改。

原理

这里我们先简单了解一下使用 MyBatis 操作数据库时大概的一个流程:

基于上面这个流程图可以看出 SqlSession 用于创建和连接数据库 Executor 用于执行 SQL,那 MyBatis 的一级缓存在哪里呢?下面我们看一下类图和源码。

代码语言:javascript
复制

public class DefaultSqlSession implements SqlSession {

  private final Configuration configuration;
  private final Executor executor;
  private final boolean autoCommit;
  private boolean dirty;
  private List<Cursor<?>> cursorList;

  public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
    this.configuration = configuration;
    this.executor = executor;
    this.dirty = false;
    this.autoCommit = autoCommit;
  }

  public DefaultSqlSession(Configuration configuration, Executor executor) {
    this(configuration, executor, false);
  }

  private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      dirty |= ms.isDirtySelect();
      return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

  @Override
  public void clearCache() {
    executor.clearLocalCache();
  }
}
代码语言:javascript
复制
public abstract class BaseExecutor implements Executor {

  private static final Log log = LogFactory.getLog(BaseExecutor.class);
  protected Transaction transaction;
  protected Executor wrapper;
  protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
  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<>();
    this.localCache = new PerpetualCache("LocalCache");
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    this.closed = false;
    this.configuration = configuration;
    this.wrapper = this;
  }

  @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();
   // 当 localCacheScope 设置为 statement 时,查询后会清除本地缓存
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache();
      }
    }
    return list;
  }

  @Override
  public void clearLocalCache() {
  // 清除本地缓存
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }
}
代码语言:javascript
复制
public class PerpetualCache implements Cache {

  private final String id;
 // 使用 Map 来缓存数据。
  private final Map<Object, Object> cache = new HashMap<>();

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

}

DefaultSqlSessionBaseExecutor 分别是 SqlSessionExecutor 的默认实现。PerpetualCache 则提供了基础的缓存功能,使用 HashMap 存放缓存对象,MyBatis 给 PerpetualCache 提供了多种装饰器类用于增强 PerpetualCache 的功能,但是一级缓存都不会用到,这里就先不介绍了。

通过这个类图我们可以了解到 SqlSession 会使用 Executor 去执行 SQL,而 Executor 中存在 localCache 对象,通过源码可以了解到 Executor 会先尝试从缓存中获取,如果获取不到才会查询数据库。

通过上面这个流程图,我们可以了解到在同一个 SqlSession 下相同的查询只有第一次会查询数据库,后续的重复查询或者嵌套查询都会使用缓存。但是当 localCacheScope 设置为 STATEMENT 时在返回查询结果前反而会去清空缓存,到这里基本介绍完了 MyBatis 一级缓存的所有内容。

这里拓展一点 MyBatis 在 Spring 当中的情况,因为上面都是在说 SqlSession,但是在 Spring 当中使用 MyBatis 的时候大部分都是直接使用 Mapper 类或者使用 SqlSessionTemplate 来操作数据库,那在 Spring 中 SqlSession 去哪里了呢?其实在 Spring 当中 MyBatis 的 SqlSession 基本和 Spring 的事务进行了绑定,下面看一下基本的流程。

总结

最后我们了解一下如何避免一级缓存的坑:

  1. 最简单的办法,把 localCacheScope 设置为 STATEMENT 这样每次执行完查询后都会清除缓存,基本上就是把一级缓存关闭了,就不会导致上述的两个问题了。分布式应用推荐都把 localCacheScope 设置为 STATEMENT。
  2. 如果只是要避免问题 2 也可以简单的把查询的数据进行深拷贝,避免因为浅拷贝的问题造成数据异常的问题。但是最好还是在写代码的时候注意不要直接修改任何查询到的数据,因为大部分的本地缓存框架都会使用 HashMap 进行数据缓存,这样的缓存都会存在浅拷贝的问题。

参考

聊聊 MyBatis 缓存机制

MyBatis 3 源码深度解析

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-12-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 政采云技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • MyBatis 缓存问题
    • 引言
      • 缓存
        • 一级缓存
      • 总结
        • 参考
    相关产品与服务
    数据库
    云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档