spring整合mybatis之一级缓存失效问题解析

本文深入探讨了在Spring框架整合MyBatis后,MyBatis的一级缓存为何会失效的问题。通过代码跟踪和实验,揭示了每次查询后SqlSession关闭导致缓存无法复用的根本原因。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

在上一次整合完spring和mybatis之后,我们已经了解了mybatis整合spring的底层原理和执行流程,尤其是知道了mapperScan的扫描原理,接下来就要开始使用mybatis,在之前我阅读mybatis源码的时候我们知道mybatis的一级缓存是默认开启的,当连续执行两次查询的时候,mybatis会在同一个会话中的第一次查询的时候会把查询结果放到一级缓存中,当执行第二次查询的时候可以直接从缓存中返回查好的值,那么当spring整合mybatis之后,一级缓存还会像之前那样起作用吗,带着这样的疑问,我开始了实验

开始

public class MybatisTest {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext configApplicationContext = new AnnotationConfigApplicationContext(MybatisConfig.class);
        UserService service = (UserService) configApplicationContext.getBean(UserService.class);
        //由于之前已经配置好了log4j日志,在这里我就调用两次查询方法,来查看日志中sql语句的打印情况
        System.out.println(service.getUser(1));
        System.out.println(service.getUser(1));
    }
}

可以看到这里输出了两句sql语句,所以判断一级缓存在整合spring后是失效的,那么我们现在就要找出一级缓存失效的原因

@Service
public class UserService{

    @Autowired
    UserMapper mapper;
    public User getUser(int id) {
        org.apache.ibatis.logging.LogFactory.useLog4JLogging();
        return mapper.selectUser(id);
    }
}

 把断点打在selectUser方法上,断点进入,在上一篇文中我们讲到了mapper最终会被MapperProxy类所代理,所以执行mapper中的查询方法会被代理类所代理,会执行代理类的invoke方法,我们看下这个invoke方法

@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    //把mapper中的方法缓存
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //调用代理方法执行
    return mapperMethod.execute(sqlSession, args);
  }

我们一路执行,执行到cachedMapperMethod方法的时候,我打算看下这个方法的具体实现

private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }

第一次进入这个方法,mapperMethod必为空,所以执行new MapperMethod构造方法,构造出的mapperMethod对象包含一个sqlCommand对象和methodSignature对象,分别代表的含义是sql命令和方法签名

可以看到这里的命令类型是select,接下来断点进入mapperMethod的excute方法

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
      Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      //我们的方法会走到这里
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          //我们的方法是selectOne方法
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName() 
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

 

 mybatis单独使用时的sqlSession会DefaultSqlSession,在整合spring之后被sqlSessionTemplate所替换,执行selectOne方法,进入sqlSessionTemplate的selectOne方法

  public <T> T selectOne(String statement, Object parameter) {
    return this.sqlSessionProxy.<T> selectOne(statement, parameter);
  }

再一次进入代理对象的selectOne方法

 

private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      try {
        //被代理的DefaultSqlSession的selectOne方法在这里被执行
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
        if (sqlSession != null) {
          //每次都会关闭sqlSession,这就是mybatis整合spring后一级缓存会失效的原因
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

总结

通过本次实验,我们知道了spring整合mybatis之后出现的一级缓存失效的情况以及失效的原因,多谢大家的支持,希望大家多多指点。

<think>好的,我现在需要回答用户关于MyBatis一级缓存和二级缓存的使用场景、区别和适用情况的问题。首先,我需要回顾一下MyBatis的缓存机制,确保自己的理解正确。 根据用户提供的引用资料,一级缓存是默认开启的,作用域是SqlSession级别的。而二级缓存需要手动配置,作用域是Mapper级别的,可以被多个SqlSession共享。不过,用户提到了一些关键点,比如一级缓存在Spring Boot中默认可能无效,需要事务支持才能生效,而二级缓存在分布式环境下可能会有脏读问题,建议使用Redis等外部缓存来解决。 接下来,我需要整理两者的区别,包括作用域、存储结构、开启方式、失效机制和应用场景。比如,一级缓存的哈希表结构存储SQL语句和结果,而二级缓存是双向链表。同时,一级缓存在同一SqlSession内有效,而二级缓存跨SqlSession。用户可能关心它们的性能影响和适用情况,比如高并发或分布式环境下如何选择。 还要注意引用中的细节,比如一级缓存在Spring Boot中的问题,需要事务支持,否则可能因为每次使用不同的SqlSession而失效。此外,二级缓存的脏读问题在分布式系统中更明显,需要第三方缓存解决方案,如Redis。这些点都需要在回答中明确说明。 最后,生成相关问题时要围绕用户可能进一步询问的方向,比如性能调优、脏读处理、配置步骤、与第三方整合以及适用场景对比等。确保问题能够引导用户深入理解MyBatis缓存机制。</think>### MyBatis 一级缓存与二级缓存的对比分析 #### 一、核心区别 | **特性** | **一级缓存** | **二级缓存** | |------------------|--------------------------------------------|--------------------------------------------| | **作用域** | SqlSession 级别(默认开启) | Mapper/Namespace 级别(需手动配置) | | **存储结构** | 哈希表(键为 SQL 语句,值为结果)[^3] | 双向链表(节点包含 SQL 语句和结果) | | **开启方式** | 默认开启(可通过配置关闭) | 需在映射文件中添加 `<cache/>` 标签开启 | | **失效机制** | 执行更新操作或关闭 SqlSession 时失效 | 执行更新操作或配置超时时间后失效 | | **适用场景** | 单次请求内重复查询优化 | 跨请求的共享查询优化 | --- #### 二、使用场景与注意事项 1. **一级缓存** - **适用场景** 适用于短时间内同一 SqlSession 内重复执行相同 SQL 的查询,例如事务中的多次相同查询。 - **典型问题** - 在 Spring Boot 中默认无效(需开启事务才能生效)[^4] - 易产生脏读(无法感知其他 SqlSession 的更新)[^4] - 建议设置为 `STATEMENT` 级别关闭,避免数据不一致 2. **二级缓存** - **适用场景** 适用于跨 SqlSession 的频繁查询且数据更新较少的场景(如配置表、静态数据)。 - **典型问题** - 分布式部署时本地缓存导致脏读(需用 Redis 等第三方缓存替换)[^2] - 多表关联查询需配置 `<cache-ref>` 实现缓存共享 - 需注意缓存淘汰策略(如 LRU、FIFO)和序列化问题 --- #### 三、生产环境建议 1. **单机应用** - 关闭一级缓存(设为 `STATEMENT`) - 选择性开启二级缓存(仅对低频更新数据生效) 2. **分布式应用** - 禁用默认二级缓存,改用 Redis 等分布式缓存 - 结合注解 `@CacheNamespace` 实现细粒度控制 --- #### 四、配置示例(二级缓存 + Redis) ```xml <!-- Mapper.xml 中启用 Redis 缓存 --> <cache type="org.mybatis.caches.redis.RedisCache"/> ``` ```properties # redis.properties host=localhost port=6379 ``` 需引入依赖: ```xml <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-redis</artifactId> <version>1.0.0-beta2</version> </dependency> ``` --- §§ 1. MyBatis 如何通过 Redis 实现分布式二级缓存? 2. 为什么 Spring Boot 中一级缓存默认无效?如何修复? 3. 多表联查时如何配置二级缓存避免脏数据? 4. MyBatis 缓存与 Spring Cache 整合的优缺点是什么? 5. 如何监控 MyBatis 缓存命中率以进行性能调优?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值