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. 开发者的生存法则
- 锁粒度控制:就像切蛋糕,太大吃不下,太小碎渣多
- 超时时间设置:建议设置为业务耗时3倍,像给快递预留合理配送时间
- 熔断机制:当错误率超过阈值,像电路跳闸一样暂停操作
- 监控预警:使用Redis的INFO命令监控内存、命中率等指标
6. 总结:在一致性与性能间走钢丝
通过实际项目案例我们发现,某电商平台采用Lua脚本+熔断策略后,秒杀系统的错误率从5%降至0.02%,同时QPS保持在2万以上。这就像优秀的杂技演员,在数据一致性和系统性能的钢丝上找到了完美平衡点。
记住没有银弹,就像不能拿菜刀切面包。需要根据业务特点选择组合方案,定期进行压力测试,就像给系统做健康体检。当你在深夜调试Redis时,不妨想想:这个选择是否像咖啡杯与茶杯的关系——看似相似,实则各有适用场景。