You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何防止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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 09:46:05