第一章:MyBatis缓存机制概述
MyBatis 作为一款优秀的持久层框架,提供了强大的 SQL 映射与对象关系映射能力,其中缓存机制是提升数据库操作性能的关键组成部分。通过合理利用缓存,可以有效减少对数据库的重复查询,降低系统负载,提高响应速度。
缓存的基本分类
MyBatis 的缓存体系主要分为两种类型:
- 一级缓存(Local Cache):默认开启,作用范围为 SqlSession 级别。在同一个 SqlSession 中执行相同 SQL 查询时,会从缓存中直接返回结果。
- 二级缓存(Second Level Cache):跨 SqlSession 生效,通常作用于同一个 Mapper Namespace 下。需手动开启,并要求返回结果对象实现 Serializable 接口。
缓存的工作流程
当执行查询操作时,MyBatis 按照以下顺序获取数据:
- 首先检查二级缓存中是否存在对应数据(若启用);
- 若未命中,则查看当前 SqlSession 的一级缓存;
- 若两级缓存均未命中,则访问数据库并加载数据;
- 将查询结果写入一级缓存,若配置了二级缓存且符合条件,则同步到二级缓存。
二级缓存配置示例
要在 MyBatis 中启用二级缓存,需在 Mapper XML 文件中添加
<cache/> 标签:
<!-- 开启当前命名空间的二级缓存 -->
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
上述配置说明:
eviction="LRU":使用最近最少使用算法清理缓存;flushInterval="60000":每隔 60 秒刷新一次缓存;size="512":最多缓存 512 个查询结果;readOnly="true":表示返回的对象是只读的,可提升性能。
缓存策略对比
| 特性 | 一级缓存 | 二级缓存 |
|---|
| 作用范围 | SqlSession 级别 | Mapper Namespace 级别 |
| 默认状态 | 开启 | 关闭 |
| 数据共享 | 不跨 SqlSession | 多个 SqlSession 可共享 |
第二章:一级缓存的底层原理与应用实践
2.1 一级缓存的作用域与生命周期解析
作用域边界界定
MyBatis 的一级缓存默认开启,其作用域为
SqlSession 级别。同一个会话中执行相同 SQL 查询时,会直接从缓存获取结果,避免重复数据库访问。
生命周期同步机制
缓存的生命周期与
SqlSession 完全一致:在会话创建时诞生,关闭或清空时销毁。任何增删改操作都会清空缓存,确保数据一致性。
- 缓存基于
HashMap 实现,键为查询条件组合 - 事务提交或回滚将触发缓存清理
SqlSession session = sqlSessionFactory.openSession();
User user = session.selectOne("selectUser", 1); // 查询走缓存
session.update("updateUser", user); // 更新后缓存被清空
上述代码中,
update 操作自动清空缓存,后续相同查询将重新访问数据库。
2.2 SqlSession级别缓存的执行流程剖析
SqlSession级别的缓存是MyBatis的一级缓存,默认开启,作用域为同一个SqlSession内。当执行查询时,MyBatis会将查询结果存储在SqlSession的本地缓存中。
缓存命中流程
- 用户发起SQL查询请求
- MyBatis检查当前SqlSession缓存中是否存在相同语句和参数的缓存记录
- 若存在,则直接返回缓存结果;否则执行数据库查询并缓存结果
// 示例:SqlSession缓存行为
SqlSession session = sqlSessionFactory.openSession();
User user1 = session.selectOne("selectUserById", 1); // 查询数据库,写入缓存
User user2 = session.selectOne("selectUserById", 1); // 直接从缓存获取
上述代码中,第二次查询不会访问数据库,而是直接从SqlSession的HashMap结构中获取结果。缓存底层基于一个简单的
PerpetualCache实现,以MappedStatement的ID和查询参数作为缓存键。
缓存失效机制
任何增删改操作(INSERT、UPDATE、DELETE)都会清空一级缓存,确保数据一致性。调用
session.clearCache()也会手动清除缓存。
2.3 一级缓存命中条件与失效场景分析
缓存命中的核心条件
一级缓存(如MyBatis中SqlSession级别的缓存)命中需满足:相同的SqlSession、相同的SQL语句、相同的参数值、相同的执行环境。只有当这些条件全部匹配时,系统才会直接返回缓存结果。
常见失效场景
- 执行任何增删改操作后,缓存自动清空
- 手动调用
clearCache()方法 - SqlSession关闭或提交事务
- 查询条件发生变更,导致缓存键不一致
sqlSession.selectList("getUserById", 1);
// 此时触发数据库查询并写入缓存
sqlSession.update("updateUser", user);
// 执行更新操作,一级缓存被清空
上述代码中,执行
update操作后,所有已缓存的查询将失效,后续相同查询需重新访问数据库。
2.4 通过源码追踪一级缓存的实现机制
在 MyBatis 的执行流程中,一级缓存默认开启,其核心实现在
BaseExecutor 类中。缓存对象本质是一个
PerpetualCache 实例,以 HashMap 结构存储查询结果。
缓存的存储结构
protected PerpetualCache localCache;
public class PerpetualCache implements Cache {
private final Map<Object, Object> cache = new HashMap<>();
}
该缓存以
CacheKey 为键,查询结果为值。每次执行查询前,先调用
localCache.getObject(key) 尝试命中缓存。
关键触发时机
- 执行
query() 方法时检查缓存 - 执行
commit() 或 rollback() 时清空缓存 - 同一
SqlSession 内重复查询可命中缓存
一级缓存生命周期与
SqlSession 绑定,无法跨会话共享。
2.5 一级缓存使用中的常见问题与规避策略
缓存穿透
当查询不存在的数据时,缓存层无法命中,请求直接打到数据库,高并发下可能导致数据库压力激增。可通过布隆过滤器提前拦截无效请求。
// 使用布隆过滤器判断键是否存在
if !bloomFilter.MayContain(key) {
return ErrKeyNotFound
}
data, _ := cache.Get(key)
上述代码中,
bloomFilter.MayContain 可快速排除无效键,避免缓存与数据库的无谓查询。
缓存雪崩
大量缓存同时过期,导致瞬时请求涌向后端存储。应采用错峰过期策略:
- 设置随机 TTL,如基础时间 + 随机分钟
- 引入二级缓存或本地缓存作为兜底
第三章:二级缓存架构设计与核心组件
3.1 二级缓存的整体工作原理与启用条件
工作原理概述
MyBatis 的二级缓存是跨
SqlSession 的缓存机制,绑定在
Mapper 命名空间级别。当多个
SqlSession 执行相同命名空间下的查询时,结果会被缓存到
Cache 实例中,后续请求直接从缓存获取。
启用条件
- 全局配置中开启
cacheEnabled(默认开启) - 在 Mapper XML 中声明
<cache/> - 查询的 POJO 必须实现
Serializable 接口 - 执行的所有 SQL 操作必须在同一个命名空间下
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
上述配置定义了缓存策略:使用 LRU 算法回收,每 60 秒清空一次,最多缓存 512 个对象,且返回只读对象以提升性能。
数据同步机制
当执行
insert、
update 或
delete 操作时,MyBatis 会自动清空对应命名空间的缓存,确保数据一致性。
3.2 Cache接口与标准实现类的功能解析
Cache接口定义了缓存操作的核心契约,包括数据的存取、删除与过期策略。其标准实现类如`ConcurrentMapCache`和`EhCacheCache`分别基于内存结构与第三方库提供具体支持。
核心方法定义
接口中关键方法包括:
get(Object key):根据键获取缓存值put(Object key, Object value):写入缓存项evict(Object key):移除指定条目
典型实现分析
public class ConcurrentMapCache implements Cache {
private final ConcurrentMap<Object, Object> store = new ConcurrentHashMap<>();
@Override
public ValueWrapper get(Object key) {
return new SimpleValueWrapper(store.get(key));
}
}
上述代码展示基于
ConcurrentHashMap的线程安全缓存实现,适用于单机高并发场景,但不具备分布式能力。
3.3 缓存装饰器模式在二级缓存中的应用
缓存装饰器模式通过封装基础缓存操作,为二级缓存(如本地+分布式缓存)提供统一访问接口,提升代码可维护性与性能。
设计结构与职责分离
装饰器将本地缓存(如 Caffeine)作为一级缓存,Redis 作为二级缓存,请求优先命中本地,未命中则查询远程并回填。
代码实现示例
@Cacheable(name = "user", local = true, ttl = 60)
public User findUser(Long id) {
return userRepository.findById(id);
}
上述注解表示启用双层缓存机制:`local=true` 启用本地缓存,`ttl=60` 设置过期时间为60秒。方法执行前自动检查两级缓存,避免重复加载。
- 第一层:检查本地缓存是否存在,存在则直接返回
- 第二层:本地未命中,查询 Redis 缓存
- 第三层:均未命中,访问数据库并异步写入两级缓存
该模式显著降低数据库压力,同时减少网络延迟对响应时间的影响。
第四章:二级缓存的配置与最佳实践
4.1 开启二级缓存的XML配置与注解方式
在 MyBatis 中,二级缓存能显著提升查询性能。可通过 XML 配置或注解两种方式开启。
XML 配置方式
在映射文件中添加
<cache/> 标签即可启用二级缓存:
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
其中,
eviction 指定回收策略,
flushInterval 设置刷新间隔(毫秒),
size 限制缓存条目数,
readOnly 控制是否只读。
注解方式
使用
@CacheNamespace 注解在接口上开启缓存:
@CacheNamespace(readOnly = false, eviction = LruCache.class)
该方式适用于注解驱动的开发场景,灵活性更高,支持自定义缓存实现。
两种方式均需确保 SqlSession 提交后缓存才生效,且实体类实现
Serializable 接口。
4.2 自定义缓存实现与第三方缓存集成(如Redis)
在构建高性能应用时,自定义缓存可针对特定业务场景优化数据访问效率。通过实现简单的内存缓存结构,结合接口抽象,可灵活切换底层存储机制。
基础自定义缓存实现
type Cache interface {
Set(key string, value interface{})
Get(key string) (interface{}, bool)
}
type InMemoryCache struct {
data map[string]interface{}
}
func (c *InMemoryCache) Set(key string, value interface{}) {
c.data[key] = value
}
func (c *InMemoryCache) Get(key string) (interface{}, bool) {
val, exists := c.data[key]
return val, exists
}
上述代码定义了一个基于哈希表的内存缓存,支持基本的读写操作。Set 方法存储键值对,Get 返回值及存在状态,适用于低频更新场景。
集成 Redis 作为分布式缓存
使用
go-redis 驱动可无缝替换本地缓存:
- 统一 Cache 接口便于依赖注入
- Redis 提供持久化、过期策略和集群支持
- 提升系统横向扩展能力
4.3 并发访问下的缓存一致性保障策略
在高并发系统中,多个客户端或服务实例同时操作缓存与数据库时,极易引发数据不一致问题。为确保数据最终一致性,需引入合理的同步机制与更新策略。
写穿透与双写一致性
采用“先更新数据库,再失效缓存”(Write-Through + Cache Invalidation)模式可有效减少脏读。典型实现如下:
func UpdateUser(id int, name string) error {
// 1. 更新数据库
if err := db.Exec("UPDATE users SET name = ? WHERE id = ?", name, id); err != nil {
return err
}
// 2. 删除缓存,触发下次读取时重建
redis.Del(fmt.Sprintf("user:%d", id))
return nil
}
该逻辑确保数据源权威性,通过删除而非更新缓存,避免并发写导致的状态冲突。
并发控制与版本校验
引入Redis分布式锁与版本号机制,防止并发写覆盖:
- 使用SETNX加锁,保证更新临界区互斥
- 为缓存数据附加版本戳(如timestamp或自增ID)
- 读取时校验版本,过期则主动刷新缓存
此策略显著降低多实例场景下缓存状态分裂风险。
4.4 二级缓存性能优化与使用场景建议
在高并发系统中,合理利用二级缓存可显著降低数据库负载。通过引入分布式缓存如 Redis 或 Memcached,多个应用实例可共享缓存数据,避免重复查询。
缓存更新策略
推荐采用“写穿透 + 失效”模式:当数据更新时,先更新数据库,再使缓存失效,由下一次读请求重建缓存。
// 示例:缓存失效逻辑
public void updateUser(User user) {
userRepository.update(user);
redisCache.delete("user:" + user.getId()); // 删除旧缓存
}
该方式保证数据最终一致性,适用于读多写少场景。
适用场景对比
| 场景 | 是否推荐 | 说明 |
|---|
| 用户资料查询 | 推荐 | 读频繁,数据变更少 |
| 订单状态更新 | 谨慎使用 | 需强一致性,建议结合消息队列同步缓存 |
第五章:总结与缓存使用全景回顾
缓存策略的实战选择
在高并发系统中,合理选择缓存策略直接影响响应延迟与数据库负载。常见的策略包括 Cache-Aside、Read/Write Through 和 Write Behind。例如,在用户资料服务中采用 Cache-Aside 模式,可显著降低 MySQL 查询压力:
// Go 中实现 Cache-Aside 示例
func GetUser(id int) (*User, error) {
var user User
// 先查 Redis
if err := cache.Get(fmt.Sprintf("user:%d", id), &user); err == nil {
return &user, nil
}
// 回源查 DB
if err := db.QueryRow("SELECT name, email FROM users WHERE id = ?", id).Scan(&user.Name, &user.Email); err != nil {
return nil, err
}
// 异步写入缓存,设置 TTL 避免雪崩
go cache.Set(fmt.Sprintf("user:%d", id), user, 30*time.Minute)
return &user, nil
}
多级缓存架构的应用场景
大型系统常采用本地缓存 + 分布式缓存的多级结构。例如,电商商品详情页使用 Caffeine 作为一级缓存,Redis 作为二级,有效降低跨网络调用频率。
- 本地缓存适用于访问频繁且容忍短暂不一致的数据
- 分布式缓存保障多实例间数据共享与一致性
- 需配置合理的失效机制,避免脏数据累积
缓存异常问题的应对方案
面对缓存穿透、击穿与雪崩,应结合实际场景部署防御措施。如使用布隆过滤器拦截无效请求,或为关键 Key 设置逻辑过期:
| 问题类型 | 解决方案 | 案例 |
|---|
| 缓存穿透 | 布隆过滤器 + 空值缓存 | 防止恶意查询不存在的商品 ID |
| 缓存雪崩 | 随机 TTL + 高可用集群 | 大批 Key 同时过期导致 DB 崩溃 |