全部学科
NodeJS全栈
nodejs
Python全栈
python
小程序首页
📅 2026-05-20 8 分钟 ✍️ juanwangdev

缓存脏数据处理

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>
useCacheflushCache行为
truefalse读缓存,不刷新(默认查询)
truetrue读缓存,执行后清空
falsefalse不读缓存,不写入
falsetrue不读缓存,清空缓存

方案对比

方案实现难度缓存命中率数据一致性适用场景
flushCache=true实时性要求高
cache-ref关联表较少
统一 Mapper关联表固定
禁用二级缓存最高数据一致性要求极严

注意事项

  1. 关联更新是脏数据根因:只要有多表关联,更新一个表就要考虑清空关联表的缓存
  2. cache-ref 是单向的:A 引用 B 的缓存,B 更新时 A 的缓存会被清空,但 A 更新时 B 不受影响
  3. flushCache 慎用:每条 SQL 都 flushCache 相当于放弃二级缓存
  4. 分布式缓存同样有脏数据:Redis 等分布式缓存也无法自动感知表关联关系
  5. 推荐策略:读多写少场景用二级缓存,写多读少场景禁用缓存或使用 flushCache

要点总结

  • 多表关联更新时,被更新的表缓存被清空,但关联表的缓存未清空,导致脏数据
  • flushCache=true 强制每次查询刷新缓存,简单但命中率低
  • cache-ref 让多个 Mapper 共享缓存空间,更新时同步清空
  • 统一 Mapper 将关联查询集中管理,避免跨 Mapper 缓存不一致
  • 禁用二级缓存是最安全的方案,适用于数据一致性要求极严的场景
  • useCache 和 flushCache 可组合使用,精细控制单条 SQL 的缓存行为
  • 关联更新频繁场景推荐 cache-ref 或禁用缓存,读多写少场景正常使用二级缓存

存放路径:D:\git2\jwdev\articles\MYBATIS\进阶\缓存机制\缓存脏数据处理.md

📝 发现内容有误?点击此处直接编辑

← 上一篇 二级缓存配置
下一篇 → 缓存读写策略
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库