避免 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
共享缓存:若多表存在强关联(如user
和user_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 {@Autowiredprivate UserMapper userMapper;/*** 正确的事务管理:更新后提交事务,触发缓存清空*/@Transactionalpublic 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 等分布式缓存,确保所有节点共享同一缓存源:
- 引入 MyBatis-Redis 依赖:
<dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-redis</artifactId><version>1.0.0-beta2</version>
</dependency>
- 配置 Redis 缓存(
redis.properties
):
properties
redis.host=127.0.0.1
redis.port=6379
redis.timeout=2000
redis.default.expiration=300000 # 5分钟过期
- 在 Mapper 中指定 Redis 缓存:
<mapper namespace="com.example.mapper.UserMapper"><cache type="org.mybatis.caches.redis.RedisCache"/><!-- SQL语句 -->
</mapper>
优势:
- 分布式环境下缓存全局一致,避免节点间数据差异。
- 支持缓存过期、集群同步等高级特性,进一步减少脏读风险。
8. 手动管理缓存(极端场景)
对于复杂业务(如跨服务更新),可通过 MyBatis 的Cache
接口手动操作缓存:
@Slf4j
@Service
public class CacheManagerService {@Autowiredprivate 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);}}
}
适用场景:
- 跨微服务更新数据后,手动触发缓存清空。
- 定时任务刷新缓存(如凌晨批量更新后全量清空)。
三、总结:避免脏读的核心原则
- 优先依赖自动机制:信任 MyBatis 的
flushCache
默认行为,不随意修改配置。 - 事务是基础:确保更新操作在事务中执行并正常提交。
- 粒度要匹配:缓存范围(Mapper)与数据更新范围保持一致。
- 按需禁用:强一致性数据直接禁用二级缓存,不冒风险。
- 分布式必用集中缓存:单机缓存无法满足分布式环境的一致性要求。
通过以上措施,可从根本上避免二级缓存的脏读问题,在性能优化与数据一致性之间找到平衡。