第一章:MyBatis缓存机制概述
MyBatis 作为一款优秀的持久层框架,提供了强大的 SQL 映射与对象关系映射功能。在性能优化方面,缓存机制是其核心特性之一。通过合理利用缓存,可以有效减少数据库访问频率,提升系统响应速度。
缓存的基本分类
MyBatis 的缓存体系分为一级缓存和二级缓存:
- 一级缓存:默认开启,作用范围为 SqlSession 级别。同一个 SqlSession 中执行相同的 SQL 查询时,会从缓存中直接获取结果。
- 二级缓存:基于 Mapper 接口级别,多个 SqlSession 可共享缓存数据。需手动开启,并要求返回结果实现 Serializable 接口。
缓存工作流程
当发起查询请求时,MyBatis 按照以下顺序查找数据:
- 首先检查二级缓存(如果启用);
- 再检查一级缓存;
- 若均未命中,则访问数据库并更新缓存。
启用二级缓存配置示例
要在 MyBatis 中启用二级缓存,需在 Mapper XML 文件中添加
<cache/> 标签:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启二级缓存 -->
<cache />
<select id="selectUserById" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>
上述配置中,
<cache/> 使用默认配置,也可自定义清除策略、缓存大小等参数。
缓存刷新时机
| 操作类型 | 对一级缓存的影响 | 对二级缓存的影响 |
|---|
| INSERT/UPDATE/DELETE | 清空当前 SqlSession 缓存 | 清空对应 Mapper 的二级缓存 |
| SqlSession 关闭 | 缓存失效 | 无直接影响 |
第二章:一级缓存深入解析与实践
2.1 一级缓存的基本原理与生命周期
一级缓存是数据库会话级别中的本地缓存,用于临时存储当前会话中查询出的对象实例。它由会话(Session)自动管理,无需开发者手动干预。
工作原理
当执行相同SQL查询时,MyBatis 首先检查一级缓存中是否存在对应结果。若存在,则直接返回缓存数据,避免重复访问数据库。
生命周期
- 创建:在打开 SqlSession 时自动创建
- 使用:每次查询先命中缓存再查数据库
- 清空:执行增删改操作或调用 clearCache() 时清空
- 销毁:SqlSession 关闭后缓存失效
SqlSession session = sqlSessionFactory.openSession();
User user = session.selectOne("selectUser", 1); // 查询并存入缓存
User cachedUser = session.selectOne("selectUser", 1); // 直接从缓存获取
上述代码中,第二次查询不会触发数据库访问,而是从一级缓存中直接返回已存在的 User 对象,提升性能。
2.2 SqlSession级别缓存的触发条件
SqlSession级别的缓存是MyBatis默认开启的一级缓存,其作用域为同一个SqlSession实例。当在相同会话中执行相同的SQL查询时,MyBatis会优先从缓存中获取结果。
缓存生效的前提条件
- 必须是同一个SqlSession对象发起的查询
- SQL语句、参数值、分页信息完全一致
- 中间未执行过session.clearCache()或增删改操作
典型代码示例
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.selectUserById(1); // 第一次查询,访问数据库
mapper.selectUserById(1); // 第二次查询,命中缓存
session.close(); // 会话结束,缓存清空
上述代码中,两次调用selectUserById(1)使用的是同一会话且参数相同,第二次直接从缓存返回结果,避免了数据库访问。
2.3 一级缓存失效场景分析与规避
在 MyBatis 中,一级缓存默认开启,作用域为 SqlSession。当同一个 SqlSession 内执行相同 SQL 时,会从缓存中直接获取结果。然而,在某些场景下缓存将失效。
常见失效场景
- SqlSession 执行了 insert、update 或 delete 操作后,一级缓存会被清空
- 手动调用
clearCache() 方法 - SqlSession 关闭或提交后,缓存生命周期结束
代码示例
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.selectUserById(1); // 查询走缓存
session.commit(); // 提交事务,清空缓存
mapper.selectUserById(1); // 再次查询,缓存已失效,重新访问数据库
该行为确保数据一致性,但在高频读写混合场景中可能导致性能下降。可通过合理控制事务边界、避免不必要的 DML 操作来规避缓存频繁失效问题。
2.4 结合实际代码演示缓存命中过程
在现代应用中,缓存命中是提升性能的关键环节。通过具体代码可清晰观察其执行流程。
模拟缓存查询逻辑
func getData(key string, cache map[string]string) (string, bool) {
value, found := cache[key]
return value, found // 返回值与命中状态
}
上述函数尝试从 map 中获取数据,map 在 Go 中常用于实现内存缓存。found 为布尔值,表示是否命中。
调用示例与结果分析
| 输入 key | 缓存状态 | 返回值 | 命中? |
|---|
| "user:1" | 存在 | "Alice", true | 是 |
| "user:99" | 不存在 | "", false | 否 |
当请求的数据存在于缓存中时,直接返回结果,避免数据库访问,显著降低响应延迟。
2.5 一级缓存的线程安全性与使用建议
线程安全机制分析
一级缓存通常以线程私有方式实现,例如在 MyBatis 中,
SqlSession 级别的缓存仅在当前会话中有效。由于每个线程持有独立的
SqlSession 实例,天然避免了并发访问冲突。
SqlSession session = sqlSessionFactory.openSession();
User user = session.selectOne("selectUser", 1); // 数据存入一级缓存
session.close(); // 缓存随之销毁
上述代码中,缓存生命周期与会话绑定,不同线程使用各自的会话,因此无需额外同步控制。
使用建议
- 避免跨线程共享 SqlSession,防止潜在内存泄漏和状态混乱
- 在事务结束后及时关闭会话,确保缓存资源释放
- 不建议在长生命周期对象中持有 SqlSession 引用
第三章:二级缓存架构设计与应用
3.1 二级缓存的工作机制与启用方式
工作机制解析
二级缓存是MyBatis中跨SqlSession级别的缓存机制,多个会话共享同一份缓存数据,有效减少数据库访问频次。当开启后,查询结果将被序列化存储于缓存中,后续相同语句优先从缓存读取。
启用配置方式
需在Mapper映射文件中添加
<cache/>标签:
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
其中,
eviction指定回收策略(LRU/FIFO/SOFT/WEAK),
flushInterval设置刷新周期(毫秒),
size限制缓存条目数,
readOnly控制是否返回只读副本。
关键注意事项
- 实体类必须实现
Serializable接口以支持序列化 - 更新操作会清空对应命名空间的缓存
- 建议在读多写少场景中启用,避免脏读
3.2 配置Cache实现类与序列化要求
在构建高性能缓存系统时,选择合适的缓存实现类并规范序列化机制至关重要。Spring 提供了多种 CacheManager 实现,如 `ConcurrentMapCacheManager`、`RedisCacheManager` 等,需根据实际场景进行配置。
常见缓存实现类配置
ConcurrentMapCacheManager:适用于单机环境,线程安全但无持久化能力;RedisCacheManager:支持分布式部署,依赖 Redis 存储,具备高可用与持久化特性。
序列化规范要求
缓存对象必须实现
Serializable 接口,推荐使用 JSON 序列化以提升可读性与跨平台兼容性。以下为 Redis 配置示例:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(connectionFactory).cacheDefaults(config).build();
}
}
上述配置中,键采用字符串序列化,值使用 Jackson JSON 序列化,确保复杂对象能正确存储与还原。
3.3 跨SqlSession数据共享的实战验证
在MyBatis中,不同SqlSession之间的数据默认是隔离的。为验证跨SqlSession的数据共享行为,需明确一级缓存与事务边界的影响。
测试场景设计
- 开启两个独立SqlSession,分别查询同一记录
- 在第一个Session中更新数据并提交事务
- 第二个Session重新查询,观察数据一致性
关键代码实现
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
User user1 = session1.selectOne("selectUser", 1);
session1.update("updateUser", user1);
User user2 = session2.selectOne("selectUser", 1); // 验证是否读取最新数据
System.out.println(user1 == user2); // 输出 false,说明跨会话无引用共享
上述代码表明:即使数据内容可能一致,不同SqlSession加载的对象实例始终独立。只有提交事务后,其他会话才能通过重新查询获取最新数据,体现数据库的ACID特性与MyBatis会话隔离机制的协同作用。
第四章:缓存策略优化与高级配置
4.1 使用EhCache/Redis集成第三方缓存
在现代Java应用中,集成EhCache或Redis作为第三方缓存可显著提升系统性能。EhCache适用于本地缓存场景,配置简单且低延迟;Redis则适合分布式环境,支持数据持久化与高可用架构。
缓存选型对比
| 特性 | EhCache | Redis |
|---|
| 部署模式 | 本地内存 | 独立服务 |
| 数据共享 | 单机 | 跨节点 |
| 持久化 | 否 | 支持 |
Spring Boot集成Redis示例
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisConnectionFactory connectionFactory() {
return new LettuceConnectionFactory(new RedisStandaloneConfiguration("localhost", 6379));
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)); // 设置缓存过期时间
return RedisCacheManager.builder(factory).cacheDefaults(config).build();
}
}
上述代码通过
@EnableCaching启用缓存功能,配置Lettuce连接工厂并定义缓存管理器,设置默认TTL为10分钟,适用于读多写少的业务场景。
4.2 缓存刷新策略与flushInterval配置
缓存刷新策略直接影响数据一致性与系统性能。合理配置 `flushInterval` 可在性能与实时性之间取得平衡。
刷新机制类型
常见的缓存刷新方式包括:
- 定时刷新:按固定周期清空缓存,适用于数据更新规律的场景;
- 事件驱动刷新:依赖数据变更通知触发,实时性强但实现复杂。
配置示例与说明
<cache type="PERPETUAL">
<property name="flushInterval" value="60000"/>
</cache>
上述配置表示每 60,000 毫秒(即 1 分钟)自动清空缓存一次。`flushInterval` 的值需根据业务容忍延迟设定:过短会增加数据库压力,过长则导致数据陈旧。
策略对比
4.3 Cache属性详解:size、readOnly、blocking
缓存配置中的核心属性直接影响系统性能与数据一致性。合理设置 `size`、`readOnly` 和 `blocking` 能有效优化读写吞吐与资源占用。
容量控制:size
`size` 定义缓存最多可存储的元素数量,超出时触发淘汰策略(如LRU)。限制大小防止内存溢出。
<cache name="userCache" maxElementsInMemory="1000" />
上述配置限定缓存最多存放1000个对象,适用于高频访问但总量可控的场景。
写模式控制:readOnly
- true:只读缓存,禁止修改,适合静态数据;
- false:可读写,需配合锁机制保证并发安全。
并发访问:blocking
启用后,多个线程请求未命中时仅放行一个去加载源数据,其余阻塞等待,避免“缓存击穿”。
| 属性 | 作用 |
|---|
| blocking=true | 启用阻塞同步,保护后端负载 |
| blocking=false | 并发加载,可能造成重复计算 |
4.4 高并发场景下的缓存一致性解决方案
在高并发系统中,缓存与数据库的双写一致性是核心挑战。为保障数据准确,常用策略包括写穿透、失效缓存与分布式锁协同控制。
数据同步机制
采用“先更新数据库,再删除缓存”模式(Cache-Aside),可有效降低脏读风险。若更新后缓存未及时失效,可引入消息队列异步补偿:
// 伪代码:更新数据库并发送失效消息
func UpdateData(id int, data string) {
db.Exec("UPDATE table SET value = ? WHERE id = ?", data, id)
redis.Del("cache:key:" + strconv.Itoa(id)) // 主动删除
mq.Publish("cache.invalidate", "key:"+strconv.Itoa(id))
}
该逻辑确保主库更新成功后,缓存立即失效,并通过消息广播通知其他节点同步状态。
一致性方案对比
| 方案 | 一致性强度 | 性能开销 |
|---|
| 强一致性(分布式锁) | 高 | 高 |
| 最终一致性(消息队列) | 中 | 低 |
| 读写串行化 | 极高 | 较高 |
第五章:缓存机制性能总结与最佳实践
缓存命中率优化策略
提升缓存命中率是系统性能调优的核心。常见手段包括使用 LRU(最近最少使用)淘汰策略、预加载热点数据以及合理设置 TTL(Time To Live)。例如,在 Go 服务中可采用
groupcache 库实现分布式缓存:
// 初始化本地缓存实例
cache := groupcache.NewGroup("hotData", 64<<20, groupcache.GetterFunc(
func(ctx context.Context, key string, dest groupcache.Sink) error {
// 模拟从数据库加载
data := fetchFromDB(key)
return dest.SetString(data)
}))
多级缓存架构设计
典型的多级缓存结构包含本地缓存(如 Caffeine)、分布式缓存(如 Redis)和数据库持久层。该模式有效降低后端压力。
- 请求优先访问 JVM 内存缓存(L1)
- L1 未命中则查询 Redis 集群(L2)
- 两级均未命中,回源至数据库并异步写入缓存
为防止缓存雪崩,建议对不同 Key 设置随机过期时间:
ttl := time.Duration(30+rand.Intn(10)) * time.Minute
redisClient.Set(ctx, key, value, ttl)
缓存穿透与击穿防护
针对恶意查询不存在的 Key,应启用布隆过滤器前置拦截。Redisson 提供了内置支持:
| 问题类型 | 解决方案 | 工具示例 |
|---|
| 缓存穿透 | 布隆过滤器 + 空值缓存 | Redisson Bloom Filter |
| 缓存击穿 | 互斥锁更新缓存 | Redis SETNX / RedLock |
流程图:缓存读取逻辑
请求到达 → 检查布隆过滤器 → (通过)→ 查询 L1 缓存 → (命中?返回)→ 查 L2 → (命中?返回)→ 回源 DB 并刷新两级缓存