避免 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 {
@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 等分布式缓存,确保所有节点共享同一缓存源:
-
引入 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"/> </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);
}
}
}
适用场景:
- 跨微服务更新数据后,手动触发缓存清空。
- 定时任务刷新缓存(如凌晨批量更新后全量清空)。
三、总结:避免脏读的核心原则
- 优先依赖自动机制 :信任 MyBatis 的
flushCache
默认行为,不随意修改配置。 - 事务是基础:确保更新操作在事务中执行并正常提交。
- 粒度要匹配:缓存范围(Mapper)与数据更新范围保持一致。
- 按需禁用:强一致性数据直接禁用二级缓存,不冒风险。
- 分布式必用集中缓存:单机缓存无法满足分布式环境的一致性要求。
通过以上措施,可从根本上避免二级缓存的脏读问题,在性能优化与数据一致性之间找到平衡。