Mybatis缓存机制爬坑记录

背景

笔者某次业务开发中遇到一个诡异的数据不一致现象:使用mybatis进行前后两次相同的sql查询,得到的结果居然不同,而中途并没有修改数据库。具体表现为:

java 复制代码
public class Test {
    public static void main(String[] args) {
        User user = userMapper.selectByName("A");
        System.out.println(user.getName()); // 输出结果:A

        user1.setName("B");

        System.out.println(userMapper.selectByName("A").getName()); // 输出结果:B
    }
}

这似乎是挺反直觉的事。仔细核对代码才发现,第二次查询前,对第一次查询的对象进行了修改。而前后两次查询似乎返回的是同一个对象。此时才突然想起mybatis还有缓存的功能。由于mybatis二级缓存默认关闭,只能是一级缓存导致了这种现象。关闭一级缓存后,两次查询结果便相同了。所以问题的本质在于:MyBatis一级缓存返回的是对象引用,而非数据副本,若缓存对象被修改,后续查询获取到的是被污染的缓存对象。原来mybatis在背后默默付出了这么多。使用了很久mybatis,今天一不小心摔进了坑里,还是需要稍微了解一下缓存机制。

一级缓存机制解析

为什么一级缓存不返回深拷贝的值

虽然mapper返回的是旧对象是很反直觉的事,但深拷贝会消耗额外的时间和内存,为了提升性能和简化设计,mybatis一级缓存直接返回了同一引用。如果需要修改缓存对象,又担心后人需要使用缓存,可以先进行防御性拷贝

缓存的作用

实际开发中,并不一定会把所有数据都进行参数传递,在不同的方法执行多次查询条件完全相同的SQL很正常,这时候MyBatis 的一级缓存就派上了用场,如果是相同的SQL语句,不会再去数据库查询,而是直接返回缓存中的数据

存储结构与查询

一级缓存是BaseExecutor 类中的一个 PerpetualCache 对象。key由Mapper方法全限定名、SQL语句(包括查询参数、分页)等构成

java 复制代码
// org.apache.ibatis.executor.BaseExecutor
public abstract class BaseExecutor implements Executor {
    protected PerpetualCache localCache;
}

缓存查询流程

第一次执行时从数据库查询,并将结果对象放入缓存。第二次查询时,由于cacheKey相同,所以返回了之前缓存的对象。

java 复制代码
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    try {
        queryStack++;
        // 先尝试从缓存localCache查询
        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) {
        // 如果缓存作用范围是单条语句(对应枚举为STATEMENT),则清空缓存
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            clearLocalCache();
        }
    }
    return list;
}

缓存控制策略

如何关闭一级缓存(作用域配置)

考虑到很多开发可能对mybatis缓存不够熟悉,为了避免后人踩坑,在缓存命中率不高的情况下,最好还是关闭一级缓存。但mybatis其实并不支持关闭一级缓存,只能修改一级缓存作用范围。SESSION表示当前sqlSession均可用,是默认的作用范围;STATEMENT表示仅当前语句有效。改为STATEMENT就达到了预期的效果

yaml 复制代码
mybatis:
  configuration:
    local-cache-scope: statement  # 修改一级缓存作用范围

如何让一级缓存失效(精确控制)

如果项目中之前有依靠一级缓存运行的代码,便不适合全局关闭一级缓存(session级),但现在又需要让某条语句的一级缓存失效,该如何操作?

  1. 声明式清除:在 Mapper.xml 的 SQL 语句中设置 flushCache="true",查询后强制清理一级缓存。
java 复制代码
<select id="selectByName" resultMap="BaseResultMap" flushCache="true">
    ...
</select>
  1. 编程式清除:查询前mapper执行更新操作(如insert/update/delete),会触发一级缓存清理
java 复制代码
public int update(MappedStatement ms, Object parameter) throws SQLException {
    // 清理一级缓存
    clearLocalCache();

    return doUpdate(ms, parameter);
}

public void clearLocalCache() {
    if (!closed) {
        localCache.clear();
    }
}
  1. 反射清除:调用 SqlSessionclearCache() 方法
java 复制代码
Field sqlSessionField = Proxy.getInvocationHandler(userShopMapper).getClass().getDeclaredField("sqlSession");
sqlSessionField.setAccessible(true);
SqlSession sqlSession = (SqlSession) sqlSessionField.get(Proxy.getInvocationHandler(userShopMapper));

sqlSession.clearCache();

二级缓存与一级缓存的区别

二级缓存是指定Mapper或sql级别的缓存,可跨SqlSession共享数据,适用于多线程访问相同数据,并且数据读多写少的场景。但实用性较低,默认是关闭的

特性 一级缓存 二级缓存
作用范围 SqlSession Mapper级,跨SqlSession共享
默认开启
适用场景 单次会话中频繁查询 多线程访问相同数据,且读多写少
性能影响 频繁修改时影响较高

总结

框架的便利性往往伴随着隐形成本。在提升开发效率的同时,也暗藏认知债务。当开发者不了解其原理时,犹如在冰面起舞。所谓知己知彼,多去理解框架的实现细节,才能避免落入这种诡异的陷阱。

相关推荐
撸猫7913 分钟前
HttpSession 的运行原理
前端·后端·cookie·httpsession
yychen_java6 分钟前
上云API二开实现三维可视化控制中心
java·无人机
理智的煎蛋7 分钟前
keepalived+lvs
java·开发语言·集成测试·可用性测试
CopyLower21 分钟前
Java与AI技术结合:从机器学习到生成式AI的实践
java·人工智能·机器学习
生命不息战斗不止(王子晗)30 分钟前
mybatis中${}和#{}的区别
java·服务器·tomcat
.生产的驴31 分钟前
Docker 部署Nexus仓库 搭建Maven私服仓库 公司内部仓库
java·运维·数据库·spring·docker·容器·maven
嘵奇33 分钟前
Spring Boot中HTTP连接池的配置与优化实践
spring boot·后端·http
子燕若水1 小时前
Flask 调试的时候进入main函数两次
后端·python·flask
程序员爱钓鱼1 小时前
跳转语句:break、continue、goto -《Go语言实战指南》
开发语言·后端·golang·go1.19
橙子199110161 小时前
Kotlin 中的 Unit 类型的作用以及 Java 中 Void 的区别
java·开发语言·kotlin