如何防止Redis缓存失效与跨系统读取时的竞态条件?
解决分布式系统中Redis缓存失效与读取的竞态问题
你遇到的是典型的分布式缓存竞态场景——两个独立系统分别执行缓存失效和读取操作,因为缺乏协同机制,可能出现读取操作先拿到旧值、随后缓存才被删除的情况,导致脏数据。以下是几种实用的解决方案,从降低概率到彻底规避不等,按需选择:
方案一:缓存失效标记+二次校验(彻底规避脏数据)
核心思路是不直接删除缓存,而是先标记缓存失效,让读取操作感知到“缓存即将失效”,优先从数据库获取最新值。同时通过二次校验,避免读取过程中缓存被标记的情况。
修改后的代码:
System1(失效缓存):
void InValidateCache(string key) { // 1. 设置失效标记,过期时间设为业务能接受的最大脏数据容忍时长(比如10秒) _cache.Set($"{key}:invalid", "true", TimeSpan.FromSeconds(10)); // 2. 延迟删除原缓存,给读取系统足够的处理时间(比如1秒后过期) _cache.Expire(key, TimeSpan.FromSeconds(1)); }
System2(读取缓存):
string GetCacheKey(string key) { string invalidFlagKey = $"{key}:invalid"; // 第一步:检查是否存在失效标记 if (_cache.Exists(invalidFlagKey)) { // 直接读DB并更新缓存,同时清除失效标记 string latestValue = ReadFromDatabase(key); _cache.Set(key, latestValue, TimeSpan.FromMinutes(5)); _cache.Remove(invalidFlagKey); return latestValue; } // 第二步:正常读取缓存 string cacheValue = _cache.Get(key); if (cacheValue == null) { cacheValue = ReadFromDatabase(key); _cache.Set(key, cacheValue, TimeSpan.FromMinutes(5)); return cacheValue; } // 第三步:二次校验失效标记,防止读取缓存的瞬间System1设置了标记 if (_cache.Exists(invalidFlagKey)) { string latestValue = ReadFromDatabase(key); _cache.Set(key, latestValue, TimeSpan.FromMinutes(5)); _cache.Remove(invalidFlagKey); return latestValue; } return cacheValue; }
适用场景:对脏数据零容忍的核心业务场景,比如订单、支付数据。
方案二:主动更新缓存而非删除(简化逻辑)
如果System1有权限直接读取数据库,可以将“删除缓存”改为“主动更新缓存为最新值”,这样即使读取操作和更新操作并发,最终拿到的也是最新值(利用Redis的单线程原子性保证)。
修改后的代码:
System1(失效/更新缓存):
void InValidateCache(string key) { // 从DB获取最新值 string latestValue = ReadFromDatabase(key); // 原子设置缓存(SETNX保证只有一个系统能更新,避免并发覆盖) bool isSet = _cache.SetNX(key, latestValue, TimeSpan.FromMinutes(5)); // 如果设置失败,说明已有其他系统完成更新,无需重复操作 }
System2(读取缓存):
string GetCacheKey(string key) { string cacheValue = _cache.Get(key); if (cacheValue != null) { return cacheValue; } // 缓存不存在时,原子更新缓存 string dbValue = ReadFromDatabase(key); _cache.SetNX(key, dbValue, TimeSpan.FromMinutes(5)); return dbValue; }
适用场景:数据更新频率不高,且System1能直接访问数据库的场景,实现简单,可靠性高。
方案三:读取重试(快速降低概率)
如果不想大幅修改现有逻辑,可以在读取缓存后增加一次短延迟重试,对比两次读取结果。如果结果不一致,说明中间发生了缓存失效,直接读DB。
修改后的代码:
System2(读取缓存):
string GetCacheKey(string key) { string firstRead = _cache.Get(key); if (firstRead == null) { return ReadAndUpdateCache(key); } // 短延迟后重试(比如10ms,根据Redis网络延迟调整) Thread.Sleep(10); string secondRead = _cache.Get(key); if (firstRead != secondRead) { // 缓存被修改,直接读DB return ReadAndUpdateCache(key); } return firstRead; } // 抽离读DB并更新缓存的逻辑 private string ReadAndUpdateCache(string key) { string dbValue = ReadFromDatabase(key); _cache.Set(key, dbValue, TimeSpan.FromMinutes(5)); return dbValue; }
适用场景:对脏数据容忍度较高,且希望快速迭代的场景,只能降低概率,无法完全避免竞态。
总结
由于两个系统完全独立、无法感知彼此操作,没法做到100%消除竞态,但通过上述方案可以极大降低脏数据出现的概率,甚至在核心场景下彻底规避。优先推荐方案一(标记+校验)或方案二(主动更新),根据业务权限和复杂度选择。
内容的提问来源于stack exchange,提问作者ach5
相关产品推荐
相关产品推荐

