详细介绍一下MyBatis的缓存机制
MyBatis 缓存机制详解(一级缓存+二级缓存+自定义缓存)MyBatis 的缓存机制是其核心性能优化手段之一,目的是减少数据库查询次数,降低IO开销,提升查询效率。其设计遵循“分层缓存”理念,分为 一级缓存(SqlSession 级别) 和 二级缓存(Mapper 级别),同时支持集成第三方缓存(如 Redis)实现分布式场景下的缓存共享。
一、缓存核心设计理念缓存粒度:从“会话级”到“跨会话级”,逐步扩大缓存作用范围;失效策略:基于“数据一致性”优先,增删改操作会触发对应缓存失效;存储介质:默认基于内存(HashMap),支持自定义扩展(如磁盘、分布式缓存);命中规则:缓存 Key 由“SQL语句+参数+环境信息+Mapper 信息”组成,确保缓存精准命中。二、一级缓存(SqlSession 级别缓存)1. 基本定义一级缓存是 SqlSession 实例级别的缓存,即同一个 SqlSession 内执行相同的查询操作,只会第一次访问数据库,后续直接从内存中获取结果。
2. 核心特性默认开启:无需任何配置,MyBatis 自动启用,无法手动关闭;作用范围:仅当前 SqlSession 有效,不同 SqlSession 之间的缓存相互隔离;存储介质:内存中的 HashMap,键(Key)是缓存唯一标识,值(Value)是查询结果对象;线程不安全:SqlSession 是线程私有对象,不存在多线程并发访问缓存的问题。3. 缓存 Key 的构成(确保查询唯一性)一级缓存的 Key 由以下 4 部分组成,缺一不可:
代码语言:javascript复制Key = HashMap
"MappedStatementId": Mapper接口全类名+方法名(如com.example.mapper.UserMapper.selectById),
"SQL语句": 最终执行的SQL(含动态SQL拼接结果),
"参数": 查询参数(如id=1001),
"环境信息": 数据库连接环境(如数据源ID、事务状态)
}4. 缓存命中与失效场景(1)命中场景同一个 SqlSession 内,执行 相同的查询方法+相同参数+相同环境;两次查询之间没有执行该表的增删改操作(insert/update/delete);两次查询之间没有手动清除缓存(sqlSession.clearCache())。(2)失效场景(重点面试考点)执行 sqlSession.close():关闭 SqlSession 时,一级缓存会被销毁;执行增删改操作(insert()/update()/delete()):MyBatis 会自动清除当前 SqlSession 内的所有一级缓存(避免数据不一致);手动清除缓存:调用 sqlSession.clearCache() 方法,主动清空当前 SqlSession 的缓存;跨 SqlSession 查询:不同 SqlSession 之间的缓存相互独立,无法共享;查询参数/方法不同:即使是同一 Mapper 方法,参数不同或方法名不同,也会生成不同的缓存 Key;开启全局二级缓存:一级缓存依然生效,但查询结果会同步写入二级缓存(不影响一级缓存命中)。5. 一级缓存执行流程(示例)代码语言:javascript复制// 1. 获取 SqlSession(默认不自动提交事务)
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 2. 第一次查询:访问数据库,结果存入一级缓存
User user1 = userMapper.selectById(1001);
System.out.println(user1); // 数据库查询
// 3. 第二次查询:参数相同,直接从一级缓存获取
User user2 = userMapper.selectById(1001);
System.out.println(user2); // 缓存命中,无数据库查询
// 4. 执行更新操作:触发一级缓存清空
userMapper.updateName(1001, "新名字");
sqlSession.commit();
// 5. 第三次查询:缓存已失效,重新访问数据库
User user3 = userMapper.selectById(1001);
System.out.println(user3); // 数据库查询
// 6. 关闭 SqlSession:一级缓存销毁
sqlSession.close();三、二级缓存(Mapper 级别缓存)1. 基本定义二级缓存是 Mapper 接口级别的缓存,即同一个 Mapper 接口下的所有 SqlSession 共享缓存(跨 SqlSession 共享)。例如,UserMapper 的二级缓存可被多个 SqlSession 访问,适合查询频繁、修改少的场景。
2. 核心特性默认关闭:需手动配置开启;作用范围:同一个 Mapper 接口(namespace 相同),跨 SqlSession 共享;存储介质:默认是内存 HashMap,支持配置为磁盘存储或第三方缓存(如 Redis);线程安全:MyBatis 内部通过锁机制保证多线程并发访问缓存的安全性;序列化要求:缓存的实体类必须实现 Serializable 接口(默认缓存会序列化存储,避免对象引用问题)。3. 二级缓存的开启与配置(1)全局配置开启(可选,默认已开启)在 mybatis-config.xml 中配置 cacheEnabled(默认值为 true,可省略):
代码语言:javascript复制
(2)Mapper 级别开启(必须配置)在对应的 Mapper.xml 中添加
代码语言:javascript复制
eviction="LRU" flushInterval="60000" size="1024" readOnly="true" blocking="false" /> select * from user where id = #{id} update user set name = #{name} where id = #{id} (3)关键配置参数说明参数名 取值范围 作用说明 eviction LRU(默认)/FIFO/SOFT/WEAK 缓存回收策略:- LRU:最近最少使用(移除最长时间未被访问的缓存);- FIFO:先进先出(按缓存添加顺序移除);- SOFT:软引用(JVM内存不足时移除);- WEAK:弱引用(垃圾回收时直接移除)。 flushInterval 数值(毫秒) 缓存自动过期时间,默认无过期(一直有效)。 size 正整数 缓存最大存储条目数,超出后按回收策略移除旧缓存(避免内存溢出)。 readOnly true/false(默认) - true:缓存返回对象本身(性能高,但线程不安全,需确保对象不被修改);- false:缓存返回对象的拷贝(通过序列化,线程安全,性能略低)。 blocking true/false(默认) - true:缓存未命中时,其他线程需等待当前线程查询数据库并写入缓存(避免缓存穿透);- false:缓存未命中时,所有线程均访问数据库(可能导致并发查询)。 (4)实体类序列化要求二级缓存默认会对查询结果进行序列化存储(即使 readOnly="true"),因此实体类必须实现 Serializable 接口,否则会抛出 NotSerializableException 异常: 代码语言:javascript复制// 实体类实现 Serializable 接口 public class User implements Serializable { private Long id; private String name; // getter/setter... }4. 二级缓存命中与失效场景(1)命中场景同一个 Mapper 接口(namespace 相同);不同 SqlSession 执行相同的查询方法+相同参数;两次查询之间没有执行该 Mapper 下的增删改操作;缓存未过期且未达到最大条目数;开启了二级缓存(全局+Mapper 级别均开启)。(2)失效场景(重点面试考点)执行增删改操作:当前 Mapper 下的任何 insert()/update()/delete() 操作(无论是否修改目标数据),都会清空该 Mapper 的所有二级缓存;手动清空缓存:调用 sqlSession.clearCache()(仅清空当前 SqlSession 的一级缓存)或 sqlSessionFactory.getConfiguration().getCache("namespace").clear()(清空指定 Mapper 的二级缓存);未开启二级缓存:全局开关 cacheEnabled="false" 或 Mapper 未配置 SqlSession sqlSession1 = sqlSessionFactory.openSession(); UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class); User user1 = mapper1.selectById(1001); // 数据库查询,结果存入一级缓存+二级缓存 sqlSession1.close(); // 关闭 SqlSession 时,一级缓存销毁,二级缓存保留 // 2. 第二个 SqlSession:共享二级缓存 SqlSession sqlSession2 = sqlSessionFactory.openSession(); UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class); User user2 = mapper2.selectById(1001); // 二级缓存命中,无数据库查询 sqlSession2.close(); // 3. 第三个 SqlSession:执行更新操作,触发二级缓存清空 SqlSession sqlSession3 = sqlSessionFactory.openSession(); UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class); mapper3.updateName(1001, "新名字"); sqlSession3.commit(); // 提交事务,清空 UserMapper 的二级缓存 sqlSession3.close(); // 4. 第四个 SqlSession:缓存失效,重新查询 SqlSession sqlSession4 = sqlSessionFactory.openSession(); UserMapper mapper4 = sqlSession4.getMapper(UserMapper.class); User user4 = mapper4.selectById(1001); // 二级缓存已失效,数据库查询 sqlSession4.close();四、一级缓存与二级缓存的区别(面试高频)对比维度 一级缓存(SqlSession 级别) 二级缓存(Mapper 级别) 开启方式 默认开启,无需配置 默认关闭,需全局+Mapper 配置 作用范围 单个 SqlSession(线程私有) 同一个 Mapper 接口(跨 SqlSession) 存储介质 内存 HashMap(非序列化) 内存 HashMap/磁盘/第三方缓存(需序列化) 实体类要求 无序列化要求 必须实现 Serializable 接口 失效触发 关闭 SqlSession、增删改、手动清空 增删改操作、缓存过期、手动清空 数据一致性 高(会话内隔离,修改后缓存清空) 中(跨会话共享,需依赖失效机制) 适用场景 单次会话内重复查询(如表单校验) 多会话共享查询(如字典数据、静态数据) 线程安全 安全(线程私有) 安全(内部锁机制) 核心结论:一级缓存是“会话内优化”,二级缓存是“跨会话优化”,二者协同工作(查询时先查一级缓存→再查二级缓存→最后查数据库)。 五、自定义缓存(集成第三方缓存)MyBatis 的默认二级缓存是内存级缓存,存在以下局限性: 不支持分布式场景(多服务实例无法共享缓存);内存有限,无法存储大量数据;服务重启后缓存丢失。因此,实际开发中常集成 Redis、Ehcache 等第三方缓存替代默认二级缓存,MyBatis 提供了 Cache 接口,支持自定义缓存实现。 1. 集成 Redis 实现二级缓存(实战示例)(1)核心依赖(Maven)代码语言:javascript复制 (2)实现 MyBatis 的 Cache 接口代码语言:javascript复制import org.apache.ibatis.cache.Cache; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * 自定义 Redis 缓存实现 */ public class RedisCache implements Cache { // 缓存 ID(对应 Mapper 的 namespace) private final String id; // Redis 连接池 private final JedisPool jedisPool; // 读写锁(保证线程安全) private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); // 构造方法:MyBatis 会自动传入 Mapper 的 namespace 作为 id public RedisCache(String id) { if (id == null) { throw new IllegalArgumentException("Cache ID cannot be null"); } this.id = id; // 初始化 Redis 连接池(实际开发中应通过配置文件读取参数) this.jedisPool = new JedisPool("localhost", 6379); } @Override public String getId() { return id; // 缓存唯一标识(必须返回 Mapper 的 namespace) } // 存入缓存:key 是 MyBatis 生成的缓存 Key,value 是查询结果 @Override public void putObject(Object key, Object value) { try (Jedis jedis = jedisPool.getResource()) { // 序列化 key 和 value(使用 fastjson2) String redisKey = key.toString(); String redisValue = com.alibaba.fastjson2.JSON.toJSONString(value); // 存入 Redis,设置过期时间(30分钟) jedis.setex(redisKey, 1800, redisValue); } } // 获取缓存:根据 key 查询 Redis @Override public Object getObject(Object key) { try (Jedis jedis = jedisPool.getResource()) { String redisKey = key.toString(); String redisValue = jedis.get(redisKey); if (redisValue == null) { return null; } // 反序列化:根据实际返回类型转换(此处简化处理,实际需结合 TypeHandler) return com.alibaba.fastjson2.JSON.parseObject(redisValue, Object.class); } } // 移除缓存(MyBatis 内部很少调用,可空实现) @Override public Object removeObject(Object key) { try (Jedis jedis = jedisPool.getResource()) { return jedis.del(key.toString()); } } // 清空缓存(增删改操作时触发) @Override public void clear() { try (Jedis jedis = jedisPool.getResource()) { // 模糊删除当前 Mapper 的所有缓存(key 前缀为 id + ":") jedis.keys(id + ":*").forEach(jedis::del); } } // 获取缓存大小(可选实现) @Override public int getSize() { try (Jedis jedis = jedisPool.getResource()) { return Math.toIntExact(jedis.keys(id + ":*").size()); } } // 获取读写锁(MyBatis 用于并发控制) @Override public ReadWriteLock getReadWriteLock() { return readWriteLock; } }(3)在 Mapper.xml 中配置自定义缓存代码语言:javascript复制
select * from user where id = #{id}
2. 自定义缓存的核心注意事项缓存 Key 唯一性:必须基于 namespace + SQL + 参数 生成 Key,避免不同 Mapper 缓存冲突;序列化/反序列化:确保实体类可序列化,且反序列化时类型匹配(可结合 MyBatis 的 TypeHandler 优化);过期时间设置:避免缓存长期有效导致数据不一致,建议设置合理的过期时间(如 30 分钟~1 小时);分布式锁:分布式场景下,需通过 Redis 分布式锁(如 RedLock)优化并发问题(避免缓存穿透/击穿);连接池管理:Redis 连接池需合理配置(最大连接数、空闲时间),避免连接泄露。六、缓存机制的最佳实践(面试+实战)1. 适用场景一级缓存:默认启用,无需额外配置,适合单次会话内重复查询(如表单提交前的校验、批量操作中的重复查询);二级缓存:适合查询频繁、修改少、实时性要求低的数据(如字典表、地区表、静态配置数据);第三方缓存(Redis):分布式系统、微服务架构,需要跨服务共享缓存的场景。2. 避坑指南禁止在高频修改的数据上使用二级缓存(如用户表、订单表),否则会频繁触发缓存失效,反而降低性能;多表关联查询不建议使用二级缓存(如 select * from user u join order o on u.id = o.user_id),因为关联表的增删改操作无法触发当前 Mapper 的缓存清空,会导致数据不一致;实体类必须实现 Serializable 接口(二级缓存/第三方缓存均需序列化);分布式场景下,必须使用第三方缓存(如 Redis),默认二级缓存无法跨服务共享;避免缓存穿透:对查询结果为 null 的数据也存入缓存(设置短期过期),防止恶意查询不存在的 ID 击垮数据库;避免缓存击穿:热点数据设置永不过期,或通过互斥锁(如 Redis 的 setnx)控制并发查询。3. 性能优化建议合理设置二级缓存的过期时间(flushInterval):根据数据更新频率调整,如字典表可设置 1 小时,新闻列表可设置 5 分钟;限制缓存条目数(size):避免缓存过大导致内存溢出,建议设置为 1024~4096;开启缓存只读模式(readOnly="true"):如果查询结果无需修改,开启只读模式可提升性能(避免对象拷贝);结合 MyBatis 分页插件(如 PageHelper):分页查询的缓存 Key 会包含分页参数,避免缓存混淆;监控缓存命中率:通过自定义缓存统计命中次数,优化缓存策略(如命中率低于 30%,建议关闭二级缓存)。七、面试核心考点总结缓存分层:一级缓存(SqlSession 级,默认开启)和二级缓存(Mapper 级,默认关闭)的区别;失效机制:增删改操作会触发缓存清空,跨 SqlSession 仅二级缓存可共享;二级缓存配置:全局开关 cacheEnabled + Mapper 标签