Java基础 | MyBatis的缓存机制(一级、二级)
- 前言
- 一、 一级缓存(SqlSession 会话级缓存)
- 1. 核心定义(默认生效,无需配置)
- 2. 实战核心特性(踩坑重点)
- 3. 实战代码示例(验证缓存生效)
- 二、 二级缓存(Mapper 命名空间级缓存)
- 1. 核心定义(非永久,需手动配置)
- 2. 生效前提(缺一不可,项目必看)
- 3. 三种实现方式(按场景选型,经验导向)
- 方式 1:XML 配置本地二级缓存(单应用首选)
- 方式 2:注解配置本地二级缓存(纯注解开发首选)
- 方式 3:整合 Redis 实现分布式二级缓存(分布式场景唯一推荐)
- 4. 三种实现方式对比与选型建议(经验总结)
- 5. 二级缓存实战避坑指南(必看)
- 三、 一级缓存 vs 二级缓存 核心区别(清晰对比)
- 四、 总结
前言
MyBatis 内置的一级缓存+二级缓存机制,是项目中优化数据库查询性能的核心手段。二者在作用域、生命周期、配置方式上差异显著,结合实际业务场景选择合适的缓存策略,能大幅减少数据库 IO 压力。
一、 一级缓存(SqlSession 会话级缓存)
1. 核心定义(默认生效,无需配置)
一级缓存是SqlSession 作用域的本地缓存,MyBatis 启动即默认开启,无法手动关闭。
- 同一
SqlSession内,执行完全相同的 SQL 查询(SQL 语句、参数、环境一致)时,首次查询会从数据库获取数据并写入一级缓存;后续查询直接读取缓存,无需走数据库。 - 生命周期与
SqlSession强绑定:SqlSession关闭/提交/回滚后,一级缓存会被立即清空,数据不会持久化。
2. 实战核心特性(踩坑重点)
| 特性 | 详情 | 项目经验总结 |
|---|---|---|
| 作用域 | 单个 SqlSession 隔离 | 不同 SqlSession 完全不共享缓存,避免多会话数据干扰 |
| 存储介质 | JVM 堆内存 | 读写速度极快,但受 JVM 内存限制,不能缓存大量数据 |
| 失效场景 | 1. 执行update/delete/insert操作(自动清空缓存,保证数据一致性)2. SqlSession 关闭/提交/回滚 3. 手动调用 sqlSession.clearCache()4. 查询条件/参数不同 | 开发中不要依赖一级缓存做跨会话数据复用,否则会导致数据不一致 |
3. 实战代码示例(验证缓存生效)
// 1. 获取 SqlSession(自动提交事务)SqlSessionsqlSession=MyBatisUtil.getSqlSession(true);UserMapperuserMapper=sqlSession.getMapper(UserMapper.class);// 2. 首次查询:无缓存 → 查数据库 → 写入一级缓存Useruser1=userMapper.selectById(1);System.out.println("首次查询:"+user1);// 3. 二次查询:同 SqlSession + 同 SQL → 读一级缓存Useruser2=userMapper.selectById(1);System.out.println("是否同一对象:"+(user1==user2));// true(内存直接复用)// 4. 执行更新操作 → 触发一级缓存失效userMapper.updateById(newUser(1,"新名字"));// 5. 三次查询:缓存失效 → 重新查数据库Useruser3=userMapper.selectById(1);System.out.println("更新后是否同一对象:"+(user1==user3));// false// 6. 关闭 SqlSession → 一级缓存彻底销毁sqlSession.close();二、 二级缓存(Mapper 命名空间级缓存)
1. 核心定义(非永久,需手动配置)
二级缓存是Mapper 命名空间作用域的缓存,作用于同一 Mapper 下的所有 SqlSession,是跨会话共享的缓存。
- 缓存优先级:查询时遵循
一级缓存 → 二级缓存 → 数据库的顺序,一级缓存未命中才会查二级缓存。 - 数据同步时机:只有当
SqlSession关闭/提交时,才会将一级缓存中的数据同步到二级缓存。 - 关键结论:二级缓存不是永久有效!默认按 LRU 策略淘汰冷数据,还可手动配置过期时间,达到时间后自动清空。
2. 生效前提(缺一不可,项目必看)
- 全局配置开启二级缓存开关(
mybatis-config.xml中cacheEnabled=true,默认开启)。 - 对应 Mapper 显式配置开启二级缓存(XML 或注解方式)。
- 缓存的实体类必须实现
Serializable接口(缓存序列化/反序列化需要,否则直接报错)。
3. 三种实现方式(按场景选型,经验导向)
方式 1:XML 配置本地二级缓存(单应用首选)
适用场景:单体应用、查询多修改少的表(如字典表、配置表),无需引入额外依赖,配置灵活。
步骤 1:全局配置(可省略,默认开启)
<configuration><settings><!-- 全局开关,true 开启,false 全局禁用二级缓存 --><settingname="cacheEnabled"value="true"/></settings></configuration>步骤 2:Mapper 配置(核心,支持过期时间)
<!-- UserMapper.xml --><mappernamespace="com.example.mapper.UserMapper"><!-- 二级缓存核心配置:非永久缓存,带淘汰策略+过期时间 --><cache eviction="LRU"<!-- 淘汰策略:LRU(最近最少使用,推荐),可选 FIFO/SOFT/WEAK -->flushInterval="60000"<!-- 过期时间:60秒自动清空缓存,避免数据长期过期 -->size="1024"<!-- 缓存上限:最多存1024个对象,防止内存溢出 -->readOnly="false"<!-- 读写模式:false(默认,支持更新缓存),true(只读,性能更高) -->/><!-- 查询语句:默认启用二级缓存,可通过 useCache=false 禁用 --><selectid="selectById"resultType="com.example.entity.User">select * from user where id = #{id}</select><!-- 增删改语句:默认 flushCache=true,执行后清空当前 Mapper 二级缓存(必须!) --><updateid="updateById"parameterType="com.example.entity.User">update user set name = #{name} where id = #{id}</update></mapper>步骤 3:实体类序列化(必做)
publicclassUserimplementsSerializable{privatestaticfinallongserialVersionUID=1L;// 显式指定序列化版本号,避免反序列化异常privateIntegerid;privateStringname;// getter/setter/toString}实战验证:跨 SqlSession 共享缓存
// SqlSession1:查询 → 关闭 → 同步数据到二级缓存SqlSessionsqlSession1=MyBatisUtil.getSqlSession(true);Useruser1=sqlSession1.getMapper(UserMapper.class).selectById(1);sqlSession1.close();// 关键:关闭会话才会同步到二级缓存// SqlSession2:同 Mapper 查询 → 直接读二级缓存SqlSessionsqlSession2=MyBatisUtil.getSqlSession(true);Useruser2=sqlSession2.getMapper(UserMapper.class).selectById(1);System.out.println("跨会话是否同一对象:"+(user1==user2));// false(序列化后新对象)sqlSession2.close();// 等待 60 秒(超过 flushInterval)→ 二级缓存自动过期Thread.sleep(60000);// SqlSession3:缓存过期 → 重新查数据库SqlSessionsqlSession3=MyBatisUtil.getSqlSession(true);Useruser3=sqlSession3.getMapper(UserMapper.class).selectById(1);System.out.println("过期后查询:"+user3);sqlSession3.close();方式 2:注解配置本地二级缓存(纯注解开发首选)
适用场景:Spring Boot 纯注解项目,无需 XML 文件,配置简洁。
importorg.apache.ibatis.annotations.CacheNamespace;importorg.apache.ibatis.annotations.Select;importorg.apache.ibatis.cache.decorators.LruCache;importorg.apache.ibatis.cache.impl.PerpetualCache;// 注解替代 XML 的 <cache> 标签,参数完全对应@CacheNamespace(implementation=PerpetualCache.class,// 缓存基础实现类eviction=LruCache.class,// LRU 淘汰策略flushInterval=60000,// 60秒过期size=1024,// 缓存上限readWrite=true// 读写模式)publicinterfaceUserMapper{@Select("select * from user where id = #{id}")UserselectById(Integerid);}经验总结:注解配置和 XML 配置功能完全一致,纯注解项目选这个,混合项目推荐 XML(可读性更高)。
方式 3:整合 Redis 实现分布式二级缓存(分布式场景唯一推荐)
核心痛点:本地二级缓存(JVM 内存)在微服务/集群部署时,会出现缓存一致性问题(比如服务 A 修改数据清空缓存,服务 B 缓存还是旧数据)。
解决方案:用 Redis 替换本地缓存,实现跨服务缓存共享,这是工业级分布式项目的标准方案。
步骤 1:引入依赖(Maven)
<!-- MyBatis Redis 缓存适配器 --><dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-redis</artifactId><version>1.0.0-beta2</version></dependency><!-- Redis 客户端 --><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>3.7.0</version></dependency>步骤 2:配置 Redis 连接(redis.properties)
redis.host=127.0.0.1 redis.port=6379 redis.password= # 无密码留空 redis.database=0 redis.timeout=2000步骤 3:Mapper 配置 Redis 缓存
<!-- UserMapper.xml --><mappernamespace="com.example.mapper.UserMapper"><!-- 替换默认缓存为 Redis 缓存 --><cachetype="org.mybatis.caches.redis.RedisCache"><!-- Redis 缓存过期时间,单位秒 --><propertyname="expiration"value="60"/></cache><selectid="selectById"resultType="com.example.entity.User">select * from user where id = #{id}</select></mapper>经验总结:分布式场景下,本地二级缓存等于“坑”,必须用 Redis 实现缓存共享;Redis 缓存支持持久化,应用重启后缓存不会丢失。
4. 三种实现方式对比与选型建议(经验总结)
| 实现方式 | 适用场景 | 核心优势 | 核心劣势 | 选型优先级 |
|---|---|---|---|---|
| XML 配置本地缓存 | 单体应用、查询多修改少的表 | 配置灵活、无额外依赖、支持过期时间 | 不支持分布式,多服务缓存不一致 | 单体应用 →首选 |
| 注解配置本地缓存 | 纯注解开发的单体项目 | 无需 XML,开发效率高 | 复杂配置可读性差、不支持分布式 | 纯注解单体 → 次选 |
| Redis 分布式缓存 | 微服务/集群部署、跨服务共享缓存 | 缓存一致性强、支持持久化、无单点问题 | 需要部署 Redis、引入额外依赖 | 分布式场景 →唯一推荐 |
5. 二级缓存实战避坑指南(必看)
- 不适合开启二级缓存的表:频繁更新的表(订单表、库存表),会频繁清空缓存,反而降低性能;数据一致性要求极高的表(金融交易表),缓存存在短暂延迟。
- 适合开启的表:查询多、修改少的静态表(字典表、地区表、配置表)。
- 参数配置经验值:
flushInterval建议设为 30-60 秒,size建议根据业务设为 512-2048,避免内存溢出。 - 不要忽略序列化:实体类未实现
Serializable会直接抛出NotSerializableException,这是新手最常踩的坑。
三、 一级缓存 vs 二级缓存 核心区别(清晰对比)
| 对比维度 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用域 | SqlSession 会话级 | Mapper 命名空间级 |
| 共享性 | 不同 SqlSession 不共享 | 同 Mapper 的所有 SqlSession 共享 |
| 开启方式 | 默认开启,无法关闭 | 需全局+Mapper 手动配置 |
| 存储介质 | 仅 JVM 堆内存 | 默认 JVM 内存,可扩展为 Redis 等 |
| 数据同步时机 | 查询后立即写入 | SqlSession 关闭/提交时从一级缓存同步 |
| 生命周期 | 随 SqlSession 销毁而销毁 | 非永久,可配置过期时间,随应用启停 |
| 对象一致性 | 同会话查询返回同一个对象(== true) | 跨会话返回序列化新对象(== false) |
| 适用场景 | 单会话内重复查询 | 跨会话重复查询、分布式共享数据 |
四、 总结
MyBatis 缓存的核心是“一级缓存解决单会话重复查询,二级缓存解决跨会话重复查询”。
- 单体应用优先用XML 配置的本地二级缓存,简单高效;
- 分布式应用必须用Redis 分布式二级缓存,解决一致性问题;
- 无论哪种缓存,都要结合业务场景,避免盲目开启,否则会适得其反。