缓存脏数据处理
MyBatis 二级缓存在多表关联场景下存在脏数据风险。当一个表被更新后,只有该表对应的 Mapper 缓存被清空,而关联表的缓存仍然保留旧数据,导致查询结果不一致。
脏数据的产生
场景重现
假设有两个表:user(用户表)和 order(订单表),通过 user_id 关联。
SQL
-- 用户表
CREATE TABLE user (
id BIGINT PRIMARY KEY,
username VARCHAR(50)
);
-- 订单表
CREATE TABLE `order` (
id BIGINT PRIMARY KEY,
user_id BIGINT,
product VARCHAR(100),
FOREIGN KEY (user_id) REFERENCES user(id)
);
对应的 Mapper 都开启了二级缓存:
XML
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<cache/>
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
<!-- OrderMapper.xml -->
<mapper namespace="com.example.mapper.OrderMapper">
<cache/>
<select id="selectByUserId" resultType="Order">
SELECT * FROM `order` WHERE user_id = #{userId}
</select>
</mapper>
脏数据流程
Java
// 1. 查询用户信息(写入 UserMapper 缓存)
SqlSession session1 = factory.openSession();
UserMapper userMapper = session1.getMapper(UserMapper.class);
User user = userMapper.selectById(1);
session1.close();
// 2. 查询用户订单(写入 OrderMapper 缓存)
SqlSession session2 = factory.openSession();
OrderMapper orderMapper = session2.getMapper(OrderMapper.class);
List<Order> orders = orderMapper.selectByUserId(1);
session2.close();
// 3. 更新用户信息
SqlSession session3 = factory.openSession();
UserMapper userMapper3 = session3.getMapper(UserMapper.class);
user.setUsername("newName");
userMapper3.updateUser(user);
session3.commit(); // 清空 UserMapper 缓存,但 OrderMapper 缓存未清空
session3.close();
// 4. 再次查询用户订单
SqlSession session4 = factory.openSession();
OrderMapper orderMapper4 = session4.getMapper(OrderMapper.class);
// 假设 OrderMapper 的查询关联了 user 表
List<Order> orders2 = orderMapper4.selectByUserId(1);
// 此时订单数据中可能包含旧的用户名 "oldName"(脏数据)
问题根因
| 问题 | 说明 |
|---|---|
| 缓存隔离 | 每个 Mapper 的缓存相互隔离 |
| 更新传播 | 更新 user 表只清空 UserMapper 缓存 |
| 关联缓存 | OrderMapper 缓存中仍有包含旧 user 数据的查询结果 |
| 数据不一致 | 查询 order 时读到过期的 user 数据 |
解决方案一:flushCache 强制刷新
在执行关联查询时强制刷新缓存:
XML
<!-- OrderMapper.xml -->
<mapper namespace="com.example.mapper.OrderMapper">
<cache/>
<!-- 每次查询都刷新缓存 -->
<select id="selectByUserId" resultType="Order" flushCache="true">
SELECT o.*, u.username
FROM `order` o
LEFT JOIN user u ON o.user_id = u.id
WHERE o.user_id = #{userId}
</select>
</mapper>
| 方案 | 优点 | 缺点 |
|---|---|---|
| flushCache=true | 简单,保证数据最新 | 每次查询都清空缓存,命中率低 |
此方案相当于放弃了二级缓存,适用于数据实时性要求高的场景。
解决方案二:cache-ref 共享缓存
通过 <cache-ref> 让多个 Mapper 共享同一个缓存空间:
XML
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<cache/>
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
<!-- OrderMapper.xml -->
<mapper namespace="com.example.mapper.OrderMapper">
<!-- 引用 UserMapper 的缓存 -->
<cache-ref namespace="com.example.mapper.UserMapper"/>
<select id="selectByUserId" resultType="Order">
SELECT o.*, u.username
FROM `order` o
LEFT JOIN user u ON o.user_id = u.id
WHERE o.user_id = #{userId}
</select>
</mapper>
cache-ref 工作原理
XML
UserMapper Cache ← 两个 Mapper 共享
├── user:1 = {id:1, username:"newName"}
└── orders:1 = [{id:101, product:"A", username:"newName"}]
更新 user 时:
→ 清空 UserMapper Cache(两个 Mapper 的缓存都被清空)
cache-ref 配置规则
| 配置 | 说明 |
|---|---|
<cache-ref namespace="..."/> | 引用指定 namespace 的缓存 |
| 只能引用一个 | 一个 Mapper 只能引用一个缓存 |
被引用的 Mapper 必须有自己的 <cache/> | 否则报错 |
注意事项
cache-ref 只能清空被引用的 Mapper 缓存,不能清空引用者自身的缓存。如果 OrderMapper 有独立的查询也需要缓存,需要反向也做 cache-ref。
解决方案三:统一 Mapper 管理
将关联表的查询合并到一个 Mapper 中,统一管理缓存:
XML
<!-- UnifiedMapper.xml -->
<mapper namespace="com.example.mapper.UnifiedMapper">
<cache/>
<!-- 用户和订单统一查询 -->
<select id="selectUserWithOrders" resultMap="userOrdersResult">
SELECT u.*, o.id as order_id, o.product
FROM user u
LEFT JOIN `order` o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
<resultMap id="userOrdersResult" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="product" column="product"/>
</collection>
</resultMap>
</mapper>
| 方案 | 优点 | 缺点 |
|---|---|---|
| 统一 Mapper | 单一缓存空间,无脏数据 | 耦合度高,查询逻辑集中 |
解决方案四:禁用二级缓存
最直接的方案是禁用二级缓存,完全依赖数据库实时数据:
XML
<!-- 方式一:不开启 <cache/> -->
<mapper namespace="com.example.mapper.OrderMapper">
<!-- 不配置 cache,二级缓存不生效 -->
</mapper>
<!-- 方式二:全局禁用 -->
<!-- mybatis-config.xml -->
<settings>
<setting name="cacheEnabled" value="false"/>
</settings>
| 方案 | 优点 | 缺点 |
|---|---|---|
| 禁用二级缓存 | 无脏数据风险 | 性能差,每次查询访问数据库 |
脏数据场景汇总
| 场景 | 脏数据风险 | 推荐方案 |
|---|---|---|
| 单表查询 | 低 | 无需处理 |
| 一对多关联查询 | 高 | cache-ref 或 flushCache |
| 多对多关联查询 | 高 | 统一 Mapper 或禁用缓存 |
| 多表更新频繁 | 高 | cache-ref 或禁用缓存 |
| 读多写少 | 低 | 正常使用二级缓存 |
flushCache 与 useCache 组合使用
精细控制单条 SQL 的缓存行为:
text
<!-- 查询时使用缓存,但不刷新缓存 -->
<select id="selectRealTime" resultType="User"
useCache="true" flushCache="false">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 查询时不使用缓存,也不写入缓存 -->
<select id="selectNoCache" resultType="User"
useCache="false" flushCache="false">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 查询后强制刷新缓存 -->
<select id="selectAndRefresh" resultType="User"
useCache="true" flushCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
| useCache | flushCache | 行为 |
|---|---|---|
| true | false | 读缓存,不刷新(默认查询) |
| true | true | 读缓存,执行后清空 |
| false | false | 不读缓存,不写入 |
| false | true | 不读缓存,清空缓存 |
方案对比
| 方案 | 实现难度 | 缓存命中率 | 数据一致性 | 适用场景 |
|---|---|---|---|---|
| flushCache=true | 低 | 低 | 高 | 实时性要求高 |
| cache-ref | 中 | 中 | 高 | 关联表较少 |
| 统一 Mapper | 高 | 高 | 高 | 关联表固定 |
| 禁用二级缓存 | 低 | 无 | 最高 | 数据一致性要求极严 |
注意事项
- 关联更新是脏数据根因:只要有多表关联,更新一个表就要考虑清空关联表的缓存
- cache-ref 是单向的:A 引用 B 的缓存,B 更新时 A 的缓存会被清空,但 A 更新时 B 不受影响
- flushCache 慎用:每条 SQL 都 flushCache 相当于放弃二级缓存
- 分布式缓存同样有脏数据:Redis 等分布式缓存也无法自动感知表关联关系
- 推荐策略:读多写少场景用二级缓存,写多读少场景禁用缓存或使用 flushCache
要点总结
- 多表关联更新时,被更新的表缓存被清空,但关联表的缓存未清空,导致脏数据
- flushCache=true 强制每次查询刷新缓存,简单但命中率低
- cache-ref 让多个 Mapper 共享缓存空间,更新时同步清空
- 统一 Mapper 将关联查询集中管理,避免跨 Mapper 缓存不一致
- 禁用二级缓存是最安全的方案,适用于数据一致性要求极严的场景
- useCache 和 flushCache 可组合使用,精细控制单条 SQL 的缓存行为
- 关联更新频繁场景推荐 cache-ref 或禁用缓存,读多写少场景正常使用二级缓存
存放路径:D:\git2\jwdev\articles\MYBATIS\进阶\缓存机制\缓存脏数据处理.md
📝 发现内容有误?点击此处直接编辑