1. 当多线程遇上Redis单线程模型

作为开发者最爱的内存数据库,Redis凭借单线程架构实现了高性能的并发处理。但当我们在C#等现代语言中开启多线程操作Redis时,就像在高速公路上突然出现多辆并行的卡车——看似效率提升,实则暗藏危机。我们常会遇到这样的情况:两个线程同时读取库存值后各自减1,结果实际库存只减少1次,这种"幽灵减库存"现象就是典型的多线程数据不一致问题。

2. 那些年我们踩过的坑

2.1 竞态条件(Race Condition)

// 错误示例:线程不安全的库存扣减
using StackExchange.Redis;
var db = ConnectionMultiplexer.Connect("localhost").GetDatabase();

// 线程A和线程B同时执行以下代码
int stock = (int)db.StringGet("product_stock");
if(stock > 0) {
    db.StringSet("product_stock", stock - 1);
}

这个经典案例中,当两个线程同时读取到库存为1时,都会执行扣减操作,最终导致超卖。就像超市最后一件商品被两个顾客同时放入购物车,收银台却只处理了一个交易。

2.2 过期时间覆盖

# 错误命令序列
SET key1 "value"
EXPIRE key1 60
# 此时另一个线程执行
SET key1 "new_value"

当第二个SET命令覆盖原有值时,过期时间仍然保持60秒。这就像给咖啡续杯时没重置计时器,导致新咖啡提前凉掉。

2.3 集合操作冲突

// 错误示例:非原子化的集合操作
var tran = db.CreateTransaction();
tran.SetAddAsync("user_tokens", "token123");
tran.KeyExpireAsync("user_tokens", TimeSpan.FromMinutes(30));
tran.Execute();

在事务执行间隙,其他线程可能修改集合数据。这就像在整理行李箱时,家人突然塞进来几件衣服,导致最终装箱结果不可预期。

3. 破解困局的六脉神剑

3.1 原子操作这把瑞士军刀

// 正确示例:使用原子命令
db.StringIncrement("product_stock", -1, flags: CommandFlags.FireAndForget);

Redis内置的INCR/DECR、HINCRBY等原子命令,就像精密的瑞士手表,每个齿轮的转动都精确同步。适用于计数器、库存扣减等场景,但功能相对基础。

优点:零锁开销,性能极致
缺点:功能受限,复杂逻辑无法实现

3.2 Lua脚本:数据库中的编程高手

-- 库存扣减脚本
local current = redis.call('GET', KEYS[1])
if tonumber(current) > 0 then
    return redis.call('DECR', KEYS[1])
else
    return -1
end

在C#中调用:

var script = LuaScript.Prepare(File.ReadAllText("deduct_stock.lua"));
var result = db.ScriptEvaluate(script, new RedisKey[] { "product_stock" });

Lua脚本就像数据库里的特种兵,能执行复杂的原子操作。适合需要多个命令原子执行的场景,比如带条件判断的库存扣减。

优点:原子性保证,逻辑灵活
缺点:调试困难,需要脚本维护

3.3 分布式锁:并发世界的红绿灯

// 使用RedLock算法实现分布式锁
var redlockFactory = RedLockFactory.Create(
    new List<RedLockMultiplexer> { ConnectionMultiplexer.Connect("redis1") });
using (var redLock = redlockFactory.CreateLock("resource_lock", TimeSpan.FromSeconds(10)))
{
    if (redLock.IsAcquired)
    {
        // 临界区操作
    }
}

分布式锁就像高速公路的收费站,让车辆有序通过。适用于抢购、秒杀等需要严格串行化的场景。

优点:强一致性保证
缺点:性能损耗,复杂度高

3.4 管道与事务:批量操作的快递小哥

var tran = db.CreateTransaction();
tran.AddCondition(Condition.StringEqual("version", 1));
tran.StringSetAsync("data", "new_value");
tran.StringSetAsync("version", 2);
bool committed = tran.Execute();

Redis事务就像可靠的快递员,把多个包裹打包运送。适用于批量更新操作,但要注意它不是传统数据库的ACID事务。

优点:减少网络开销
缺点:不支持回滚

4. 不同场景的兵器谱排行

  • 高并发计数器:原子操作 > Lua脚本
  • 电商秒杀系统:Lua脚本 + 分布式锁
  • 实时排行榜:ZADD原子命令
  • 分布式配置中心:WATCH/MULTI事务

就像选择交通工具:短途选自行车(原子命令),长途用汽车(Lua脚本),跨国运输需要飞机(分布式锁)。

5. 开发者的生存法则

  1. 锁粒度控制:就像切蛋糕,太大吃不下,太小碎渣多
  2. 超时时间设置:建议设置为业务耗时3倍,像给快递预留合理配送时间
  3. 熔断机制:当错误率超过阈值,像电路跳闸一样暂停操作
  4. 监控预警:使用Redis的INFO命令监控内存、命中率等指标

6. 总结:在一致性与性能间走钢丝

通过实际项目案例我们发现,某电商平台采用Lua脚本+熔断策略后,秒杀系统的错误率从5%降至0.02%,同时QPS保持在2万以上。这就像优秀的杂技演员,在数据一致性和系统性能的钢丝上找到了完美平衡点。

记住没有银弹,就像不能拿菜刀切面包。需要根据业务特点选择组合方案,定期进行压力测试,就像给系统做健康体检。当你在深夜调试Redis时,不妨想想:这个选择是否像咖啡杯与茶杯的关系——看似相似,实则各有适用场景。