避免 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)與數據更新范圍保持一致。
- 按需禁用:強一致性數據直接禁用二級緩存,不冒風險。
- 分布式必用集中緩存:單機緩存無法滿足分布式環境的一致性要求。
通過以上措施,可從根本上避免二級緩存的臟讀問題,在性能優化與數據一致性之間找到平衡。