找回密码
 会员注册
查看: 21|回复: 0

Mybatis一级缓存问题

[复制链接]

4

主题

0

回帖

13

积分

新手上路

积分
13
发表于 2024-10-12 21:12:20 | 显示全部楼层 |阅读模式
Mybatis一级缓存问题 407 MyBatis 缓存问题引言很多 Java 后端服务都选择使用 MyBatis 作为它们的 ORM 框架,帮助简化开发。但是基本上大家都不会太关心 MyBatis 的缓存机制,基本都在使用 MyBatis 缓存的默认配置,在不了解 MyBatis 缓存机制的情况下进行开发,可能就会发生一些意料之外的事情。下面帮助大家了解 MyBatis 一级缓存和二级缓存的机制,以及如何避免 MyBatis 缓存中的坑。缓存众所周知数据库的请求中大部分都是查询请求,使用缓存可以大大减少数据库的压力,提高系统的性能,但是如果使用不当可能就会产生数据一致性的问题。一级缓存MyBatis 的一级缓存又被叫做本地缓存,一级缓存默认作用在 Session 级别,并且不能被关闭,只能修改一级缓存的作用域。一级缓存的作用域有两种,分别是 SESSION 和 STATEMENT ,可以通过修改配置项 localCacheScope 来设置,默认的 SESSION 会缓存一个会话中执行的所有查询,用来加速重复的嵌套查询,当在这个会话中执行更新操作时则会清除缓存。如果设置成 STATEMENT 则值作用在执行语句中,当语句执行完成就会清除缓存。问题下面看一下两个一级缓存导致的问题:问题 1当 localCacheScope 被设置为 SESSION 的时候,并且当前服务有多个实例时就可能会导致查询到的数据不一致。// 会话 1SqlSession session1 = factory.openSession(true);// 会话 2SqlSession 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 时导致的问题,不过这个问题是在同一个会话当中发生的,下面我们看一段代码: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 的一级缓存在哪里呢?下面我们看一下类图和源码。public class DefaultSqlSession implements SqlSession {  private final Configuration configuration;  private final Executor executor;  private final boolean autoCommit;  private boolean dirty;  private List> 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  List 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();  }}public abstract class BaseExecutor implements Executor {  private static final Log log = LogFactory.getLog(BaseExecutor.class);  protected Transaction transaction;  protected Executor wrapper;  protected ConcurrentLinkedQueue deferredLoads;  protected erpetualCache localCache;  protected erpetualCache 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 erpetualCache("LocalCache");    this.localOutputParameterCache = new erpetualCache("LocalOutputParameterCache");    this.closed = false;    this.configuration = configuration;    this.wrapper = this;  }  @SuppressWarnings("unchecked")  @Override  public  List 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 list;    try {      queryStack++;   // 使用本地缓存中的数据。      list = resultHandler == null ? (List) 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();    }  }}public class erpetualCache implements Cache {  private final String id; // 使用 Map 来缓存数据。  private final Map cache = new HashMap();  public erpetualCache(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();  }}DefaultSqlSession 和 BaseExecutor 分别是 SqlSession 和 Executor 的默认实现。PerpetualCache 则提供了基础的缓存功能,使用 HashMap 存放缓存对象,MyBatis 给 erpetualCache 提供了多种装饰器类用于增强 erpetualCache 的功能,但是一级缓存都不会用到,这里就先不介绍了。通过这个类图我们可以了解到 SqlSession 会使用 Executor 去执行 SQL,而 Executor 中存在 localCache 对象,通过源码可以了解到 Executor 会先尝试从缓存中获取,如果获取不到才会查询数据库。通过上面这个流程图,我们可以了解到在同一个 SqlSession 下相同的查询只有第一次会查询数据库,后续的重复查询或者嵌套查询都会使用缓存。但是当 localCacheScope 设置为 STATEMENT 时在返回查询结果前反而会去清空缓存,到这里基本介绍完了 MyBatis 一级缓存的所有内容。这里拓展一点 MyBatis 在 Spring 当中的情况,因为上面都是在说 SqlSession,但是在 Spring 当中使用 MyBatis 的时候大部分都是直接使用 Mapper 类或者使用 SqlSessionTemplate 来操作数据库,那在 Spring 中 SqlSession 去哪里了呢?其实在 Spring 当中 MyBatis 的 SqlSession 基本和 Spring 的事务进行了绑定,下面看一下基本的流程。总结最后我们了解一下如何避免一级缓存的坑:最简单的办法,把 localCacheScope 设置为 STATEMENT 这样每次执行完查询后都会清除缓存,基本上就是把一级缓存关闭了,就不会导致上述的两个问题了。分布式应用推荐都把 localCacheScope 设置为 STATEMENT。如果只是要避免问题 2 也可以简单的把查询的数据进行深拷贝,避免因为浅拷贝的问题造成数据异常的问题。但是最好还是在写代码的时候注意不要直接修改任何查询到的数据,因为大部分的本地缓存框架都会使用 HashMap 进行数据缓存,这样的缓存都会存在浅拷贝的问题。参考聊聊 MyBatis 缓存机制MyBatis 3 源码深度解析
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-26 12:47 , Processed in 0.502206 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表