多实例部署下如何避免Redis值被重复处理?
解决.NET多实例后台服务重复处理Redis哈希值的问题
方案一:全局分布式锁(粗粒度控制)
直接给整个Redis哈希键加分布式锁,同一时间只有一个实例能拿到锁并处理所有值,其他实例跳过本次执行。核心是利用Redis的原子性命令实现锁的获取与释放,同时设置过期时间避免死锁。
伪代码实现
// CutBackgroundService 中的处理逻辑 var lockKey = $"lock:{hashKey}"; var redisDb = GetRedisDatabase(); // 原子性获取锁,设置1秒过期(根据实际处理时长调整,确保覆盖单次全量处理时间) bool acquiredLock = await redisDb.StringSetAsync(lockKey, "processing", TimeSpan.FromSeconds(1), When.NotExists); if (!acquiredLock) { // 没抢到锁,直接跳过本次循环 return; } try { var redisValues = GetRedisValues(hashKey); foreach (var value in redisValues) { var processedValue = ProcessValue(value); UpdateValueInRedis(processedValue); } } finally { // 用Lua脚本原子释放锁,防止误删其他实例的锁(如果锁已自动过期则不操作) var releaseScript = @"if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end"; await redisDb.ScriptEvaluateAsync(releaseScript, new RedisKey[] { lockKey }, new RedisValue[] { "processing" }); }
优缺点
- 优点:实现简单,全局控制逻辑清晰,适合所有值需要统一处理的场景
- 缺点:锁竞争可能导致部分实例频繁空跑;如果处理时长超过锁过期时间,仍会出现重复处理风险,需要合理设置过期时间或添加锁续约逻辑
方案二:单条目锁标记(细粒度控制)
不给整个哈希键加锁,而是给每个待处理的条目单独添加Locked标记,只有获取到未锁定的条目并原子性标记为锁定的实例,才能处理该条目,多个实例可同时处理不同条目,提升并发效率。
伪代码实现(基于Lua脚本保证原子性)
var redisDb = GetRedisDatabase(); // Lua脚本:批量获取未锁定的条目,并原子性标记为锁定 var acquireScript = @" local result = {} local entries = redis.call('HGETALL', KEYS[1]) for i=1, #entries, 2 do local entryKey = entries[i] local entryValue = entries[i+1] local obj = cjson.decode(entryValue) -- 同时检查锁定状态和超时(防止实例崩溃导致永久锁定) if not obj.Locked or obj.LockedTime < (redis.call('TIME')[1] - 1) then obj.Locked = true obj.LockedTime = redis.call('TIME')[1] redis.call('HSET', KEYS[1], entryKey, cjson.encode(obj)) table.insert(result, {entryKey, entryValue}) end end return result "; // 获取可处理的条目 var processableEntries = await redisDb.ScriptEvaluateAsync(acquireScript, new RedisKey[] { hashKey }); // 处理条目 foreach (var entry in processableEntries) { var entryKey = entry[0].ToString(); var originalValue = entry[1].ToString(); var processedValue = ProcessValue(originalValue); // 处理完成后原子性取消锁定并更新值 var updateScript = @" local obj = cjson.decode(redis.call('HGET', KEYS[1], KEYS[2])) obj.Locked = false obj.ProcessedResult = ARGV[1] redis.call('HSET', KEYS[1], KEYS[2], cjson.encode(obj)) "; await redisDb.ScriptEvaluateAsync(updateScript, new RedisKey[] { hashKey, entryKey }, new RedisValue[] { processedValue }); }
优缺点
- 优点:并发效率高,多个实例可并行处理不同条目;通过超时机制避免永久锁定
- 缺点:实现相对复杂,需要处理条目的序列化/反序列化;需额外维护锁定超时逻辑
额外注意事项
- 无论哪种方案,都必须设置锁的过期/超时时间,防止实例崩溃导致锁永久持有
- 如果处理逻辑耗时不稳定,可添加锁续约逻辑:在处理过程中每隔一段时间更新锁的过期时间
- 细粒度锁方案中,也可以把锁定状态单独存到一个独立的Redis哈希键(如
locked_status:{hashKey}),避免修改原数据结构,提升灵活性
内容的提问来源于stack exchange,提问作者Jetonii
相关产品推荐
相关产品推荐

