MyBatis 缓存体系解析

发布时间:2026/7/20 19:53:59
MyBatis 缓存体系解析 前言在 Java 后端开发领域MyBatis 作为占据统治地位的持久层框架其缓存机制是每一位开发者必须跨越的知识门槛。然而在实际的企业级生产环境中关于缓存的认知往往充斥着误区一级缓存的作用边界在哪里为什么大厂规范明令禁止使用原生二级缓存如何在 Spring Boot 中优雅地集成 Redis 并规避一致性陷阱一、SqlSession缓存体系的物理载体与生命周期基石在探讨任何缓存策略之前必须先彻底理解SqlSession。它不仅是 MyBatis 执行 SQL 的入口更是整个缓存体系的物理边界。不理解 SqlSession就无法真正理解 MyBatis 缓存的失效机制。1.1 核心本质与职责SqlSession 并非 JDBC Connection 的简单包装而是对数据库交互过程的高度抽象。它封装了 Connection、Statement、ResultSet 的处理逻辑并承担以下四大核心职责SQL 执行引擎提供selectOne,selectList,insert,update,delete等原生 API直接映射到 Mapper XML 或注解中的 SQL ID。Mapper 代理工厂通过getMapper(ClassT)方法生成接口的 JDK/CGLIB 动态代理对象这是现代 MyBatis 开发的唯一标准交互方式。事务控制单元在非 Spring 托管环境下提供手动的commit(),rollback(),close()事务管理能力。一级缓存容器SqlSession 内部持有一个Executor执行器而一级缓存就存储在 Executor 的LocalCache属性中。SqlSession 的生命周期严格等同于一级缓存的生命周期。1.2 线程安全性与作用域铁律SqlSession 是绝对线程不安全的。多线程并发访问同一个 SqlSession 实例会导致数据错乱、缓存污染、连接泄漏等严重问题。其正确的作用域取决于运行环境运行环境推荐作用域管理规范与说明原生 MyBatis方法级别 / 请求级别必须放在 try-with-resources 块中用完立即 close严禁跨方法复用Spring Boot方法/事务级别由SqlSessionTemplate通过 ThreadLocal 自动绑定开发者无需手动管理Web 应用HTTP 请求级别一个 HTTP 请求对应一个独立 Session请求结束即销毁⚠️ 生产红线警告永远不要将 SqlSession 声明为类的成员变量实例变量或静态变量。在 Spring 整合项目中你几乎不会直接接触 SqlSession 对象而是注入 Mapper 接口。Spring 框架会通过SqlSessionTemplate保证每个事务或请求绑定独立的 SqlSession并在事务结束时自动关闭。这种机制既保证了线程安全也定义了缓存的自然边界。1.3 内部组件协作拓扑理解 SqlSession 的内部结构有助于定位缓存行为SqlSessionFactory (全局单例工厂线程安全) │ openSession() ▼ SqlSession (线程私有会话非线程安全) │ ├── getMapper() → Mapper Proxy (动态代理对象) │ └── 内部持有 Executor (执行器) │ ├── StatementHandler (SQL构建、预编译、参数设置) ├── ParameterHandler (参数绑定处理) ├── ResultSetHandler (结果集映射与类型转换) └── LocalCache (一级缓存存储区PerpetualCache实现)二、MyBatis 一级缓存默认开启的会话级性能优化一级缓存是 MyBatis 默认开启的本地缓存其设计初衷是消除单次数据库会话内的重复查询开销。它是安全的、有益的但作用范围有限。2.1 工作原理与存储结构存储介质基于PerpetualCache实现本质是一个HashMapString, Object。缓存 Key 构成由MappedStatement ID SQL 文本 参数值 RowBounds(分页信息)组合生成的哈希值。这意味着只有完全相同的查询才能命中缓存。命中逻辑当同一个 SqlSession 中再次执行相同查询时直接从 HashMap 返回结果对象的引用跳过数据库访问和结果集映射过程。2.2 自动失效机制一级缓存是“脆弱”的以下任一操作都会导致当前 SqlSession 的一级缓存被立即清空SqlSession 调用close()或clearCache()方法。SqlSession 执行了commit()操作提交事务意味着数据可能已变更。该 SqlSession 执行了任何INSERT / UPDATE / DELETE语句。注意无论该写操作是否涉及同一张表只要执行了写操作整个 Session 的一级缓存都会被清空。这是 MyBatis 为保证数据一致性采取的保守策略。2.3 Spring Boot 环境下的特殊表现在 Spring Boot MyBatis 整合项目中由于事务管理器的介入每个 Service 方法通常对应一个独立的 SqlSession。方法执行完毕后 Session 即被关闭。因此一级缓存的实际作用范围被缩小到了单个 Service 方法内部。跨 Service 方法的重复查询无法命中一级缓存。这并非框架缺陷而是 Spring 事务隔离语义下的必然结果也是保证数据一致性的必要代价。三、MyBatis 二级缓存为何在企业级生产中被弃用二级缓存是 NamespaceMapper 接口级别的全局缓存设计目标是跨 SqlSession 共享数据以减少数据库压力。但在现代企业级开发中强烈建议禁用原生二级缓存原因如下3.1 致命缺陷一脏读问题最核心原因二级缓存以Namespace为单位隔离而非以数据库表为单位。场景复现假设UserMapper和OrderMapper都操作user表。当OrderMapper更新了 user 表数据并提交事务后MyBatis 只会清除OrderMapper的二级缓存不会清除UserMapper的二级缓存。后果此时通过UserMapper查询仍会读到旧的缓存数据产生脏读。在多表关联、多 Mapper 操作同表的复杂业务中这个问题几乎无法避免。3.2 致命缺陷二分布式环境失效原生二级缓存是 JVM 堆内存级别的单机缓存。在微服务或多实例部署架构中节点间缓存完全不互通。A 节点更新数据后B/C/D 节点的缓存永远不会被通知失效导致严重的数据不一致。这与现代云原生架构根本不相容。3.3 致命缺陷三序列化性能开销为保证多线程安全对象存入二级缓存时会进行深拷贝序列化/反序列化。对于复杂嵌套对象这个开销可能比直接查库还要慢违背了缓存提速的初衷。同时实体类必须实现Serializable接口增加了代码侵入性。3.4 致命缺陷四粒度不可控无法按业务维度如用户ID、租户ID、地区精细控制缓存的过期时间和淘汰策略只能整个 Namespace 一刀切地清除。这在热点数据与冷数据混合的场景下极其低效。四、替代方案Spring Boot Redis 缓存实战正确的做法是将缓存逻辑从“数据访问层Mapper”上移到“业务服务层Service”使用 Redis 作为分布式缓存基础设施。这实现了缓存与 ORM 的彻底解耦。4.1 第一步彻底禁用原生二级缓存防止团队成员误用在全局配置中强制关闭# application.ymlmybatis:configuration:cache-enabled:false# 全局禁用二级缓存同时全局搜索项目删除所有 Mapper XML 中的cache/标签及接口上的CacheNamespace注解。注意保留一级缓存默认开启它是无害且有益的会话级优化。4.2 第二步Redis 基础设施标准化配置引入核心依赖dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependency!-- 使用 Jackson 替代默认 JDK 序列化 --dependencygroupIdcom.fasterxml.jackson.core/groupIdartifactIdjackson-databind/artifactId/dependency配置标准化序列化器避免 JDK 序列化的乱码、不可读和跨语言兼容性问题ConfigurationpublicclassRedisConfig{BeanpublicRedisTemplateString,ObjectredisTemplate(RedisConnectionFactoryfactory){RedisTemplateString,ObjecttemplatenewRedisTemplate();template.setConnectionFactory(factory);StringRedisSerializerstringSerializernewStringRedisSerializer();GenericJackson2JsonRedisSerializerjsonSerializernewGenericJackson2JsonRedisSerializer();// Key 统一使用 String 序列化便于运维查看template.setKeySerializer(stringSerializer);template.setHashKeySerializer(stringSerializer);// Value 使用 Jackson 序列化支持多态和复杂对象template.setValueSerializer(jsonSerializer);template.setHashValueSerializer(jsonSerializer);template.afterPropertiesSet();returntemplate;}}4.3 第三步Service 层缓存实现模式模式 A手动编码 Cache Aside Pattern推荐复杂业务使用这是生产环境中最可靠、最灵活的模式遵循“旁路缓存”原则完全掌控缓存生命周期。ServiceRequiredArgsConstructorSlf4jpublicclassUserService{privatefinalUserMapperuserMapper;privatefinalRedisTemplateString,ObjectredisTemplate;privatestaticfinalStringCACHE_KEY_PREFIXmall:user:detail:;privatestaticfinalDurationNORMAL_TTLDuration.ofMinutes(30);privatestaticfinalDurationNULL_TTLDuration.ofMinutes(5);publicUsergetUserById(LonguserId){StringkeyCACHE_KEY_PREFIXuserId;// 1. 读缓存ObjectcachedredisTemplate.opsForValue().get(key);if(cached!null){// 处理空值缓存对象returncachedinstanceofNullObject?null:(User)cached;}// 2. 缓存未命中读数据库UseruseruserMapper.selectById(userId);// 3. 回填缓存含空值防穿透if(user!null){redisTemplate.opsForValue().set(key,user,NORMAL_TTL);}else{// 缓存空值防止恶意请求穿透到DBredisTemplate.opsForValue().set(key,NullObject.INSTANCE,NULL_TTL);}returnuser;}Transactional(rollbackForException.class)publicvoidupdateUser(Useruser){// ⚡ 核心原则先更新DB再删除缓存而非更新缓存userMapper.updateById(user);redisTemplate.delete(CACHE_KEY_PREFIXuser.getId());log.info(Updated user {} and evicted cache,user.getId());}}模式 BSpring Cache 注解模式适合简单 CRUD启动类添加EnableCaching配置spring.cache.typeredis。ServiceCacheConfig(cacheNamesmall:user)publicclassUserService{AutowiredprivateUserMapperuserMapper;Cacheable(key#p0,unless#result null)publicUsergetUserById(LonguserId){returnuserMapper.selectById(userId);}CacheEvict(key#p0.id)Transactional(rollbackForException.class)publicvoidupdateUser(Useruser){userMapper.updateById(user);}CacheEvict(allEntriestrue)publicvoidbatchSyncUsers(){// 批量同步时清空整个缓存区}}注注解模式虽然简洁但在复杂失效逻辑、多级缓存、防击穿锁、条件缓存等场景下灵活性不足建议仅在简单 CRUD 场景使用。五、缓存设计的五大黄金法则无论采用何种技术栈以下原则是保证缓存系统稳定可靠的底线违反任何一条都可能导致生产事故法则详细说明违规后果Cache Aside写操作时删除缓存而非更新缓存推荐“先更新DB再删缓存”并发写入导致缓存与DB永久不一致强制 TTL所有缓存键必须设置过期时间作为兜底机制异常情况下数据永久不一致内存泄漏防穿透缓存空值短TTL或使用布隆过滤器拦截非法请求恶意/异常请求直接打穿DB引发宕机防击穿热点Key重建时加互斥锁Redisson/synchronized热点Key过期瞬间海量并发压垮DB防雪崩TTL 添加随机偏移量如 ±10%大批Key同时过期流量瞬时涌入DBKey 命名规范标准采用分层命名法{应用名}:{业务模块}:{实体类型}:{业务标识}✅ 正确mall:user:detail:10086、mall:order:list:uid_10086:page_1❌ 错误user_10086无模块隔离易冲突难排查、cache1语义不明六、总结需求场景推荐方案核心理由单次请求内重复查询MyBatis 一级缓存默认零成本、安全、自动管理、无需干预单机热点只读字典数据Caffeine / Guava Cache纯内存、零网络开销、纳秒级响应分布式业务数据缓存Redis Service 层手动控制粒度精细、一致性可控、支持集群扩展简单 CRUD 快速开发Spring Cache Redis代码侵入性低开发效率高任何生产场景MyBatis 原生二级缓存不推荐脏读风险高与分布式架构不兼容MyBatis 应当被定位为纯粹的、高效的 SQL 执行引擎而缓存则是独立的、可插拔的业务基础设施。将两者解耦在 Service 层统一管控缓存策略才是符合现代云原生架构的正确实践。理解这一边界不仅能避免生产事故更是从“框架使用者”迈向“架构设计者”的关键一步。在实际工程中始终牢记缓存是加速手段不是数据源一致性优于性能显式控制优于隐式约定。