详细介绍一下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​​ 中添加 ​​​​ 标签,开启当前 Mapper 的二级缓存:

代码语言:javascript复制

eviction="LRU"

flushInterval="60000"

size="1024"

readOnly="true"

blocking="false"

/>

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 未配置 ​​​​ 标签;查询方法禁用二级缓存:​​select​​ 标签设置 ​​useCache="false"​​(如实时性要求高的查询);增删改方法未触发缓存清空:​​insert​​/​​update​​/​​delete​​ 标签设置 ​​flushCache="false"​​(不推荐,会导致数据不一致);实体类未实现 ​​Serializable​​ 接口:缓存写入失败,无法命中;跨 Mapper 查询:不同 Mapper 接口的缓存相互独立,无法共享(如 UserMapper 和 OrderMapper 的缓存不互通)。5. 二级缓存执行流程(示例)代码语言:javascript复制// 1. 第一个 SqlSession:查询并写入二级缓存

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复制

org.mybatis

mybatis

3.5.13

redis.clients

jedis

4.4.6

com.alibaba

fastjson2

2.0.41

(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复制

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 标签 ​​​​ + 实体类 ​​Serializable​​;自定义缓存:实现 ​​Cache​​ 接口,集成 Redis 等第三方缓存的步骤;最佳实践:哪些场景适合用二级缓存,哪些场景禁用,如何避免数据不一致。MyBatis 缓存机制的核心是“平衡性能与数据一致性”,实际开发中需根据业务场景选择合适的缓存策略,避免盲目开启二级缓存导致的问题。

Copyright © 2022 ZGC网游最新活动_热门游戏资讯_玩家互动社区 All Rights Reserved.