如何避免MyBatis二级缓存中的脏读

避免 MyBatis 二级缓存中的脏读问题(即缓存数据与数据库实际数据不一致),需要从缓存更新机制、配置策略、业务设计等多维度入手。以下是经过实践验证的解决方案,结合底层原理和具体实现:

一、理解二级缓存脏读的根源

脏读的本质是缓存数据未及时同步数据库更新。二级缓存是 Mapper 级别的共享缓存,当某一 SqlSession 更新数据后,若其他 SqlSession 仍使用旧缓存,就会导致脏读。常见触发场景:

  • 更新操作未触发缓存清空
  • 事务未正常提交导致缓存未刷新
  • 跨 Mapper 操作导致缓存同步失效
  • 分布式环境下缓存未全局同步

二、解决方案详解

1. 依赖 MyBatis 自动缓存清空机制

MyBatis 默认在执行insert/update/delete操作时,会自动清空当前 Mapper 的二级缓存(通过flushCache="true"实现)。需确保该机制正常生效:

核心原理

更新操作会触发缓存清空,保证后续查询能从数据库获取最新数据。但需注意:只有事务提交后,缓存清空才会生效

实现示例

复制代码
<!-- Mapper.xml中默认配置(无需手动添加,但需确认) -->
<update id="updateUser" flushCache="true">
  UPDATE t_user SET username = #{username} WHERE id = #{id}
</update>

<insert id="insertUser" flushCache="true">
  INSERT INTO t_user (username, email) VALUES (#{username}, #{email})
</insert>

注意

  • 不要手动将flushCache设为false,这会禁用自动清空,直接导致脏读。
  • 若使用注解方式,需确保@Update/@Insert/@Delete注解的方法默认触发缓存清空(MyBatis 注解默认行为与 XML 一致)。
2. 控制查询语句的缓存刷新策略

对于实时性要求极高的查询(如库存、余额),可强制每次查询都刷新缓存,避免使用旧数据:

实现方式

select标签中设置flushCache="true",每次查询前清空缓存:

复制代码
<select id="selectUserById" resultType="User" flushCache="true">
  SELECT id, username, email FROM t_user WHERE id = #{id}
</select>

适用场景

  • 高频更新且实时性要求高的数据(如订单状态、库存数量)。
  • 避免:全局使用该配置,会导致缓存失效,失去性能优化意义。
3. 精细化控制缓存粒度

二级缓存默认以 Mapper 为单位(namespace 级别),粒度较粗。若同一 Mapper 中包含多表操作,可能导致无关更新触发缓存清空,或相关更新未触发清空。

优化方案

  • 拆分 Mapper :按表或业务模块拆分 Mapper,确保缓存粒度与数据更新范围匹配。

    例:UserMapper只处理t_user表,OrderMapper只处理t_order表,避免跨表操作导致缓存混乱。

  • 使用cache-ref共享缓存 :若多表存在强关联(如useruser_profile),可通过cache-ref让多个 Mapper 共享同一缓存,确保更新任一表时同步清空关联缓存:

    复制代码
    <!-- UserMapper.xml -->
    <cache eviction="LRU" flushInterval="30000"/>
    
    <!-- UserProfileMapper.xml 共享UserMapper的缓存 -->
    <cache-ref namespace="com.example.mapper.UserMapper"/>

    此时,更新user_profile表会清空UserMapper的缓存,避免关联数据脏读。

4. 严格控制事务边界

在 Spring+MyBatis 环境中,事务未提交会导致缓存更新延迟,是脏读的常见诱因。

原理

SqlSession 在事务提交前,更新操作的缓存清空不会生效(二级缓存写入 / 清空操作在事务提交后执行)。若事务未正常提交(如异常回滚),缓存不会更新,导致后续查询仍使用旧数据。

解决方案

  • 确保更新操作在事务中执行,并正常提交。
  • 避免长事务持有 SqlSession,减少缓存不一致窗口。

代码示例

复制代码
@Slf4j
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    /**
     * 正确的事务管理:更新后提交事务,触发缓存清空
     */
    @Transactional
    public void updateUser(Long id, String newUsername) {
        User user = userMapper.selectById(id);
        if (Objects.isNull(user)) {
            log.warn("用户不存在,id: {}", id);
            return;
        }
        user.setUsername(newUsername);
        userMapper.update(user);
        // 事务提交后,MyBatis会自动清空UserMapper的二级缓存
    }
}
5. 配置合理的缓存过期时间

即使缓存更新机制失效,合理的过期时间也能减少脏读影响。通过flushInterval设置自动刷新间隔:

复制代码
<cache 
  eviction="LRU" 
  flushInterval="60000"  <!-- 60秒自动刷新一次缓存 -->
  size="1024" 
  readOnly="false"/>

适用场景

  • 非核心数据(如商品分类、地区信息),允许短时间不一致。
  • 作为兜底机制,避免缓存永久脏数据。
6. 禁用敏感数据的二级缓存

对于强一致性要求的数据(如用户余额、订单状态),直接禁用二级缓存,优先保证数据准确性:

实现方式

  • 全局禁用:在mybatis-config.xml中关闭二级缓存(不推荐,会影响所有 Mapper):

    xml

    复制代码
    <settings>
        <setting name="cacheEnabled" value="false"/>
    </settings>
  • 局部禁用:在特定select标签中禁用:

    xml

    复制代码
    <select id="selectUserBalance" resultType="BigDecimal" useCache="false">
        SELECT balance FROM t_user_balance WHERE user_id = #{userId}
    </select>
7. 分布式环境下使用集中式缓存

单机环境下,二级缓存使用内存存储;分布式环境下,多节点的本地缓存无法同步,必然导致脏读。

解决方案

集成 Redis、Memcached 等分布式缓存,确保所有节点共享同一缓存源:

  1. 引入 MyBatis-Redis 依赖:

    <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-redis</artifactId> <version>1.0.0-beta2</version> </dependency>
  2. 配置 Redis 缓存(redis.properties):

properties

复制代码
redis.host=127.0.0.1
redis.port=6379
redis.timeout=2000
redis.default.expiration=300000  # 5分钟过期
  1. 在 Mapper 中指定 Redis 缓存:

    <mapper namespace="com.example.mapper.UserMapper"> <cache type="org.mybatis.caches.redis.RedisCache"/> </mapper>

优势

  • 分布式环境下缓存全局一致,避免节点间数据差异。
  • 支持缓存过期、集群同步等高级特性,进一步减少脏读风险。
8. 手动管理缓存(极端场景)

对于复杂业务(如跨服务更新),可通过 MyBatis 的Cache接口手动操作缓存:

复制代码
@Slf4j
@Service
public class CacheManagerService {
    @Autowired
    private SqlSessionFactory sqlSessionFactory;

    /**
     * 手动清空指定Mapper的二级缓存
     */
    public void clearMapperCache(String mapperNamespace) {
        Configuration configuration = sqlSessionFactory.getConfiguration();
        Cache cache = configuration.getCache(mapperNamespace);
        if (Objects.nonNull(cache)) {
            cache.clear();
            log.info("已手动清空缓存,namespace: {}", mapperNamespace);
        }
    }
}

适用场景

  • 跨微服务更新数据后,手动触发缓存清空。
  • 定时任务刷新缓存(如凌晨批量更新后全量清空)。

三、总结:避免脏读的核心原则

  1. 优先依赖自动机制 :信任 MyBatis 的flushCache默认行为,不随意修改配置。
  2. 事务是基础:确保更新操作在事务中执行并正常提交。
  3. 粒度要匹配:缓存范围(Mapper)与数据更新范围保持一致。
  4. 按需禁用:强一致性数据直接禁用二级缓存,不冒风险。
  5. 分布式必用集中缓存:单机缓存无法满足分布式环境的一致性要求。

通过以上措施,可从根本上避免二级缓存的脏读问题,在性能优化与数据一致性之间找到平衡。

相关推荐
祈祷苍天赐我java之术1 分钟前
Linux 进阶之性能调优,文件管理,网络安全
java·linux·运维
无处不在的海贼24 分钟前
小明的Java面试奇遇之发票系统相关深度实战挑战
java·经验分享·面试
武子康28 分钟前
Java-109 深入浅出 MySQL MHA主从故障切换机制详解 高可用终极方案
java·数据库·后端·mysql·性能优化·架构·系统架构
秋难降1 小时前
代码界的 “建筑师”:建造者模式,让复杂对象构建井然有序
java·后端·设计模式
BillKu1 小时前
Spring Boot 多环境配置
java·spring boot·后端
君不见,青丝成雪3 小时前
SpringBoot项目占用内存优化
java·spring boot·后端
ningqw3 小时前
Redis-分布式缓存
redis
Trust yourself2433 小时前
IDEA控制台乱码(Tomcat)解决方法
java·tomcat·intellij-idea
##学无止境##4 小时前
解锁Java分布式魔法:CAP与BASE的奇幻冒险
java·开发语言·分布式