如何避免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. 分布式必用集中缓存:单机缓存无法满足分布式环境的一致性要求。

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

相关推荐
刃神太酷啦7 小时前
力扣校招算法通关:双指针技巧全场景拆解 —— 从数组操作到环检测的高效解题范式
java·c语言·数据结构·c++·算法·leetcode·职场和发展
Mos_x7 小时前
计算机组成原理核心知识点梳理
java·后端
墨寒博客栈7 小时前
Linux基础常用命令
java·linux·运维·服务器·前端
回忆是昨天里的海7 小时前
k8s-部署springboot容器化应用
java·容器·kubernetes
INFINI Labs7 小时前
使用 Docker Compose 轻松实现 INFINI Console 离线部署与持久化管理
java·docker·eureka·devops·docker compose·console·easyserach
Cosolar7 小时前
国产麒麟系统 aarch64 架构 PostgreSQL 15 源码编译安装完整教程
java·后端
GalaxyPokemon7 小时前
PlayerFeedback 插件开发日志
java·服务器·前端
天天摸鱼的java工程师8 小时前
别再写那些重复代码了!8年Java老兵教你用 Hutool 提升开发效率
java·后端
喝杯绿茶8 小时前
springboot中的事务
java·spring boot·后端