Mybatis的一级缓存遇到的问题

73 阅读3分钟

问题

开发过程中遇到了一个问题 入库之前想要得到旧对象的状态值,再将新状态值更新到数据库中,后续有个根据旧状态值判断的逻辑。但是发现逻辑走不下去 发现到if判断那里该状态值更新为新的了,伪代码如下(简化后)

MesBwGroupOrderEntity mesBwGroupOrderOld = mesBwGroupOrderService.getById(dto.getMainId());
//此时 status 为0
//由于方法断层 导致该查询执行了两次 (问题所在)
MesBwGroupOrderEntity mesBwGroupOrder = mesBwGroupOrderService.getById(dto.getMainId());
mesBwGroupOrder.setStatus(1);
mesBwGroupOrderService.saveOrUpdate(mesBwGroupOrder);
if(mesBwGroupOrderOld.getStatus() == 1) {
    //发送kafka消息,发现消息一直未发送
}


猜测和验证:

mybatis一级缓存导致的。 实验: image.png 会发现输出true,getById得到的是同一个对象

源码探究

调用方法栈路线 image.png

org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql) 关键代码:

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

得知先从缓存对象中获取 缓存对象为空,才从数据库中获取 org.apache.ibatis.executor.BaseExecutor#queryFromDatabase

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    //mybatis 防止缓存穿透 占位符占坑一级缓存
    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;
}

思考

提出疑问1: 先查询 再更新,再查询如果还是缓存中对象的话,更新后的数据能否被查询出来?mybatis框架层面肯定保证了最新数据能被查出来,那是怎么实现的呢? 猜测1: 缓存同步更新为最新数据 猜测2: 更新的时候会删除缓存 这样下次查询的时候还是会走数据库 而不会命中缓存。 实验:

MesBwGroupOrderEntity old = mesBwGroupOrderUpdateService.getById(3);
MesBwGroupOrderEntity up = new MesBwGroupOrderEntity();
up.setMainId(old.getMainId());
up.setRemark("mybatis");
mesBwGroupOrderUpdateService.saveOrUpdate(up);
MesBwGroupOrderEntity newE = mesBwGroupOrderUpdateService.getById(3);
System.err.println(old == newE);

输出为false,猜测2正确(删除缓存确实简单粗暴,只要执行更新操作就清空所有缓存),下面来看下源码: 通过断点调试追踪栈 发现最终会调用到如下方法: org.apache.ibatis.executor.BaseExecutor#update

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

那么再次提出疑问2 这个缓存是以什么维度保存的呢?(sqlSession)

sqlSession

此处是ChatGPT的描述

image.png

那么我们验证一下,关闭事务的话,每条sql就是单独的sqlSession,我们来验证一下: image.png 发现输出结果为false,符合预期。

总结

一级缓存的作用: 当使用同一个sqlSession对数据库做相同的查询时,第一次查询的结果会放入缓存,在缓存中是以Map的形式方便存放多个查询结果,当后面相同的查询到来时就会去缓存中取数据,而不再查询数据库。(Mybatis 默认开启一级缓存)

SqlSession和事务的关系: 一级缓存是SqlSession级别的,sqlSession级别的缓存,意味着伴随着sqlSession的生死。 同一个事务下共享一个SqlSession,意味着缓存也共享。

缓存失效的场景: 两次查询中间夹杂着更新操作则会清除缓存;或者不用事务。