第一章:MyBatis缓存机制概述
MyBatis 作为一款优秀的持久层框架,提供了强大的 SQL 映射与对象关系映射能力,其中缓存机制是提升数据库操作性能的关键特性之一。通过合理利用缓存,可以有效减少对数据库的重复查询,降低系统负载,提高响应速度。
缓存的基本分类
MyBatis 的缓存分为两种类型:
- 一级缓存(本地缓存):默认开启,作用范围为 SqlSession 级别。在同一个 SqlSession 中,执行相同 SQL 查询时,会从缓存中直接获取结果。
- 二级缓存(全局缓存):默认关闭,作用范围为 Mapper 级别,可在多个 SqlSession 之间共享缓存数据,需手动启用并配置。
缓存的工作流程
当执行一次查询时,MyBatis 按照以下顺序访问数据:
- 首先检查二级缓存是否存在对应数据(若启用);
- 若未命中,则检查一级缓存;
- 若两级缓存均未命中,则访问数据库,并将结果写入一级缓存;
- 提交事务后,一级缓存被清空,同时将数据写入二级缓存(如配置)。
二级缓存配置示例
要在 MyBatis 中启用二级缓存,需在 Mapper XML 文件中添加
<cache/> 标签:
<!-- 在 Mapper XML 中启用二级缓存 -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启当前命名空间的二级缓存 -->
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="false"/>
<select id="selectUserById" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>
上述配置说明:
eviction="LRU":使用最近最少使用算法回收缓存;flushInterval="60000":每隔 60 秒清空缓存;size="512":最多缓存 512 个查询结果;readOnly="false":表示缓存对象可读写,支持序列化副本。
缓存策略对比
| 特性 | 一级缓存 | 二级缓存 |
|---|
| 作用范围 | SqlSession | Mapper Namespace |
| 默认状态 | 开启 | 关闭 |
| 跨会话共享 | 否 | 是 |
| 清空时机 | SqlSession 关闭或提交 | 配置刷新间隔或手动刷新 |
第二章:一级缓存深入剖析——SqlSession级别的数据共享
2.1 一级缓存的基本原理与生命周期
一级缓存是MyBatis默认开启的会话级别缓存,其生命周期与SqlSession绑定。在同一个SqlSession中,执行相同的SQL查询时,后续请求将直接从缓存中获取结果,避免重复访问数据库。
缓存的存储结构
一级缓存底层基于HashMap实现,键为MappedStatement的ID、SQL语句、参数值、分页信息等组合,值为查询结果对象。
缓存的失效机制
当发生以下操作时,一级缓存会被清空:
- 执行任何增删改操作(INSERT/UPDATE/DELETE)
- 手动调用SqlSession的clearCache()方法
- SqlSession关闭或提交后重新开启新会话
// 示例:同一SqlSession中的缓存命中
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
User user1 = mapper.selectById(1); // 查询走数据库
User user2 = mapper.selectById(1); // 命中一级缓存
session.close(); // 缓存随之销毁
上述代码展示了缓存的典型使用场景:两次相同查询在同一个会话中,第二次无需访问数据库。一旦会话关闭,缓存数据即被清除。
2.2 源码解析:SqlSession如何管理缓存映射
一级缓存的存储结构
SqlSession 内部通过 `PerpetualCache` 实现一级缓存,底层基于 HashMap 存储查询结果。每个 SqlSession 拥有独立的缓存实例,确保会话间隔离。
// org.apache.ibatis.session.defaults.DefaultSqlSession
private final Executor executor;
public <T> T selectOne(String statement, Object parameter) {
return this.<T>selectList(statement, parameter).get(0);
}
上述调用最终交由 Executor 执行查询,BaseExecutor 中维护了 localCache 实例,键值为 MappedStatement 的 ID 与参数的组合。
缓存映射的生命周期
- 缓存创建于 SqlSession 初始化时
- 执行 commit/rollback 时清空缓存
- 关闭 Session 时释放所有映射
关键缓存键生成机制
| 组成部分 | 说明 |
|---|
| MappedStatement.id | 唯一标识 SQL 映射语句 |
| 环境参数 | 如分页、超时设置等上下文信息 |
2.3 实践演示:相同SqlSession下的查询命中验证
在 MyBatis 中,一级缓存默认开启,作用域为同一个 `SqlSession`。当在相同会话中执行相同的 SQL 查询时,第二次查询将直接从缓存中获取结果,而不会访问数据库。
验证流程
- 获取一个 SqlSession 并执行第一次查询
- 执行相同的查询语句
- 观察日志是否输出 SQL 执行记录
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
// 第一次查询:触发数据库访问
User user1 = mapper.selectById(1);
System.out.println("第一次查询: " + user1);
// 第二次查询:命中一级缓存,不访问数据库
User user2 = mapper.selectById(1);
System.out.println("第二次查询: " + user2);
session.close(); // 清空缓存
上述代码中,只有第一次调用 `selectById(1)` 时会打印 SQL 日志,第二次直接返回缓存结果,证明了同一 SqlSession 下的查询命中机制。
2.4 一级缓存失效的典型场景分析
在 MyBatis 中,一级缓存默认开启,作用域为 SqlSession。当某些操作打破其一致性时,缓存将自动失效。
执行更新操作
任何 insert、update 或 delete 操作都会清空当前 SqlSession 的一级缓存,防止脏读:
<update id="updateUser">
UPDATE users SET name = #{name} WHERE id = #{id}
</update>
该语句执行后,MyBatis 会清空缓存,确保后续查询获取最新数据。
手动清空缓存
通过调用
sqlSession.clearCache() 可主动清除缓存:
sqlSession.selectOne("findUserById", 1);
sqlSession.clearCache(); // 缓存立即失效
适用于数据敏感场景,保障查询结果实时性。
SqlSession 关闭或提交
当调用
sqlSession.commit() 或
close() 时,缓存生命周期结束。不同 SqlSession 之间缓存不共享,天然隔离。
| 触发场景 | 是否清空缓存 |
|---|
| 执行更新语句 | 是 |
| 调用 clearCache() | 是 |
| SqlSession 提交或关闭 | 是 |
2.5 生产环境中一级缓存的使用建议与风险规避
合理启用与作用域控制
一级缓存默认在同一个 SqlSession 中生效,适用于读多写少的场景。但在高并发环境下,若未及时刷新缓存,可能导致数据不一致。
- 避免跨业务长时间持有 SqlSession
- 在更新频繁的操作后显式调用
clearCache() - 禁用不必要的自动缓存机制
代码示例与分析
// 查询用户信息(会命中一级缓存)
User user1 = sqlSession.selectOne("getUser", 1);
User user2 = sqlSession.selectOne("getUser", 1); // 直接从缓存返回
sqlSession.clearCache(); // 清除缓存,后续查询将访问数据库
上述代码中,第二次查询不会触发 SQL 执行,数据来自内存。若在此期间数据库被外部修改,则应用层无法感知变更,存在脏读风险。
风险规避策略
| 风险类型 | 应对措施 |
|---|
| 数据过期 | 设置短生命周期操作自动刷新 |
| 内存泄漏 | 确保 SqlSession 及时关闭 |
第三章:二级缓存架构详解——跨SqlSession的数据共享
3.1 二级缓存的工作机制与启用条件
工作机制概述
二级缓存是MyBatis中跨SqlSession共享的缓存机制,位于Mapper命名空间级别。当多个SqlSession执行相同查询时,首次结果将存储在二级缓存中,后续请求直接从缓存读取,减少数据库访问。
启用条件
要启用二级缓存,需满足以下条件:
- 在Mapper XML中添加
<cache/>标签 - SqlSession必须提交或关闭后,数据才会写入缓存
- 查询所涉及的POJO类必须实现
Serializable接口
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
上述配置表示:使用LRU淘汰策略,每60秒清空一次缓存,最多缓存512个对象,且返回只读对象以提升性能。参数
flushInterval控制刷新频率,
size限制内存占用,合理配置可平衡性能与一致性。
3.2 配置实战:Mapper级别缓存的声明与优化
启用Mapper级缓存
在MyBatis中,Mapper级别的二级缓存默认关闭,需在映射文件中显式开启。通过
<cache/>标签即可声明缓存策略:
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
上述配置表示:采用LRU(最近最少使用)回收策略,每60秒刷新一次缓存,最多缓存512个对象,且返回只读实例以提升性能。
缓存优化建议
- 避免在频繁写操作的Mapper中启用缓存,防止数据不一致;
- 结合
flushCache="true"控制特定语句执行后刷新缓存; - 对于关联查询,可使用
<cache-ref namespace=""/>共享缓存实例。
3.3 对象序列化与缓存存储的实现细节
在分布式系统中,对象需通过序列化转换为可存储或传输的格式。常见的序列化方式包括 JSON、Protobuf 和 Gob。以 Go 语言为例,使用 JSON 序列化结构体:
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
data, _ := json.Marshal(user)
该代码将 User 对象编码为字节流,便于写入缓存。反序列化时需确保结构体字段匹配。
缓存存储策略
Redis 常用于缓存序列化后的对象。推荐采用以下流程:
- 序列化对象为 JSON 或二进制格式
- 设置 TTL(Time To Live)避免数据陈旧
- 使用命名空间隔离不同业务缓存键
| 格式 | 空间占用 | 性能 |
|---|
| JSON | 中等 | 较快 |
| Protobuf | 低 | 快 |
第四章:缓存交互与控制策略
4.1 清空缓存:insert、update、delete的默认行为影响
在MyBatis等持久层框架中,执行
INSERT、
UPDATE和
DELETE操作时,默认会清空当前
SqlSession的一级缓存和二级缓存,以保证数据一致性。
缓存清理机制
每次执行写操作后,为防止后续查询读取到过期数据,框架自动刷新缓存。这一行为由
flushCache属性控制,默认值为
true。
<insert id="insertUser" flushCache="true">
INSERT INTO users (id, name) VALUES (#{id}, #{name})
</insert>
上述配置表示插入后将清空关联的缓存。若手动设置
flushCache="false",可能导致脏读,仅适用于特殊场景。
操作类型与缓存行为对照表
| SQL 操作 | 默认 flushCache 值 | 说明 |
|---|
| INSERT | true | 插入后清空缓存,避免旧数据残留 |
| UPDATE | true | 更新后确保缓存同步 |
| DELETE | true | 删除后清除相关缓存项 |
4.2 useCache、flushCache属性的精细化控制
在MyBatis中,`useCache`与`flushCache`是Mapper语句级别的重要控制属性,用于精确管理二级缓存的行为。
缓存读写控制
`useCache="true"`表示该查询结果可被缓存,后续相同SQL将直接从缓存读取。默认对`
`生效:
<select id="selectUser" useCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
此配置适用于读多写少场景,显著提升查询性能。
缓存刷新机制
`flushCache`控制语句执行后是否清空本地及二级缓存。设置为`true`时强制刷新,常用于增删改操作:
<update id="updateUser" flushCache="true">
UPDATE user SET name = #{name} WHERE id = #{id}
</update>
避免脏数据,确保后续查询获取最新状态。
典型应用场景对比
| 语句类型 | useCache 默认值 | flushCache 默认值 |
|---|
| SELECT | true | false |
| INSERT/UPDATE/DELETE | false | true |
4.3 缓存作用域(SESSION vs STATEMENT)对比与选型
缓存作用域的基本概念
在MyBatis等持久层框架中,缓存作用域分为SESSION和STATEMENT两种级别。STATEMENT作用域仅对当前语句生效,每次执行都会查询数据库;而SESSION作用域则在同一个SqlSession内共享结果。
性能与一致性权衡
- STATEMENT:保证数据实时性,适用于高并发写多读少场景;
- SESSION:提升读取性能,适合读密集且数据变化不频繁的业务。
<select id="getUser" resultType="User" flushCache="false" useCache="true">
SELECT * FROM users WHERE id = #{id}
</select>
上述配置默认使用SESSION级缓存。若设useCache="false",则降级为STATEMENT行为。
选型建议
| 维度 | STATEMENT | SESSION |
|---|
| 性能 | 较低 | 较高 |
| 一致性 | 强 | 弱 |
| 适用场景 | 实时数据要求高 | 会话内重复查询 |
4.4 整合Redis实现分布式二级缓存实践
在高并发系统中,单一本地缓存难以应对节点间数据一致性问题。引入Redis作为分布式二级缓存,可有效提升数据共享能力与系统吞吐量。
缓存层级架构设计
采用“本地缓存(如Caffeine) + Redis”双层结构,本地缓存降低访问延迟,Redis保障跨实例数据一致性。读取时优先命中本地缓存,未命中则从Redis加载并回填。
数据同步机制
为避免缓存不一致,写操作需遵循“先更新数据库,再失效Redis缓存”的策略。通过发布/订阅模式通知其他节点清除本地缓存:
// 发布缓存失效消息
redisTemplate.convertAndSend("cache:invalidate", "user:123");
// 订阅端处理
@EventListener
public void handleInvalidate(CacheInvalidateEvent event) {
localCache.evict(event.getKey());
}
上述代码确保各节点本地缓存及时失效,Redis作为统一信道协调状态同步,保障最终一致性。
第五章:总结与最佳实践建议
构建高可用微服务架构的关键原则
在生产环境中保障系统稳定性,需遵循服务解耦、故障隔离和自动恢复三大原则。例如,使用熔断器模式可有效防止级联故障。以下为基于 Go 的熔断器实现片段:
circuitBreaker := gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "PaymentService",
Timeout: 60 * time.Second,
ReadyToCall: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures > 5
},
})
result, err := circuitBreaker.Execute(func() (interface{}, error) {
return callPaymentService()
})
日志与监控的最佳配置策略
统一日志格式并接入集中式监控平台是快速定位问题的前提。推荐采用结构化日志(如 JSON 格式),并通过 Grafana + Prometheus 实现可视化告警。
- 所有服务输出 JSON 日志,包含 trace_id、level、timestamp 字段
- 使用 Fluent Bit 收集日志并转发至 Elasticsearch
- 在 Prometheus 中配置服务健康检查抓取任务
- 设置响应延迟超过 500ms 触发企业微信告警
安全加固的实战清单
| 风险项 | 修复措施 | 验证方式 |
|---|
| 未授权访问 API | 集成 JWT 中间件校验 token | 使用 curl 测试无 token 请求被拒绝 |
| 敏感信息硬编码 | 迁移至 Hashicorp Vault 动态获取密钥 | 扫描镜像确认无 SECRET_KEY 字样 |