一、为什么需要Redis分布式锁?
上周我们团队遇到了一个诡异的BUG:用户抢购优惠券时,出现了同一张券被重复领取的情况。检查代码发现,虽然用了数据库事务,但在高并发场景下多个请求同时通过验证。这时候老张掏出手机:"用Redis做个分布式锁吧!"
典型应用场景
- 电商秒杀系统中库存扣减
- 文件上传时的重复提交拦截
- 定时任务调度防多节点重复执行
- 支付系统的订单状态变更
就像医院候诊室的叫号系统,分布式锁就是那个确保"每次只进一人"的电子显示屏,防止多个服务实例同时操作关键资源。
二、技术选型:为什么选择StackExchange.Redis?
在.NET生态中,常见的Redis客户端有:
客户端库 | 维护状态 | 性能 | 功能完整性 |
---|---|---|---|
StackExchange | 活跃 | 高 | 完整 |
ServiceStack | 停滞 | 中 | 部分缺失 |
CSRedis | 活跃 | 高 | 轻量 |
我们选择StackExchange.Redis的三大理由:
- 官方推荐,微软Azure认证
- 支持异步编程模型
- 完善的连接池管理和重试机制
三、手把手实现Redis锁
示例1:基础锁实现
// 创建Redis连接(单例模式最佳)
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost");
IDatabase db = redis.GetDatabase();
// 获取锁
public async Task<bool> AcquireLock(string lockKey, string clientId, TimeSpan expiry)
{
// 使用SET命令的NX和EX参数保证原子性操作
return await db.StringSetAsync(
lockKey,
clientId,
expiry,
When.NotExists,
CommandFlags.DemandMaster);
}
// 释放锁
public async Task ReleaseLock(string lockKey, string clientId)
{
// Lua脚本保证原子性验证和删除
string script =
@"if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end";
await db.ScriptEvaluateAsync(
script,
new RedisKey[] { lockKey },
new RedisValue[] { clientId });
}
代码要点解析:
When.NotExists
确保只有不存在时才设置值- 客户端ID使用Guid保证全局唯一
- Lua脚本解决"先判断后删除"的原子性问题
示例2:带自动续期的锁
public class AutoRenewalLock : IDisposable
{
private readonly IDatabase _db;
private readonly string _lockKey;
private readonly string _clientId;
private readonly TimeSpan _expiry;
private Timer _renewTimer;
public AutoRenewalLock(IDatabase db, string lockKey, TimeSpan expiry)
{
_db = db;
_lockKey = lockKey;
_clientId = Guid.NewGuid().ToString();
_expiry = expiry;
// 启动续期定时器
_renewTimer = new Timer(
callback: async _ => await RenewLock(),
state: null,
dueTime: (int)_expiry.TotalMilliseconds / 2,
period: (int)_expiry.TotalMilliseconds / 2);
}
private async Task RenewLock()
{
await _db.StringSetAsync(
_lockKey,
_clientId,
_expiry,
When.Exists);
}
public void Dispose()
{
_renewTimer?.Dispose();
ReleaseLock().Wait();
}
private async Task ReleaseLock()
{
var script = "..."; // 同基础锁的Lua脚本
await _db.ScriptEvaluateAsync(script, new[] { _lockKey }, new[] { _clientId });
}
}
四、避坑指南:常见问题与解决方案
1. 锁超时难题
某电商平台曾因锁超时设置不当导致库存超卖:
- ❌ 错误做法:固定30秒超时
- ✅ 正确方案:动态计算(业务耗时*3 + 网络缓冲)
推荐公式:
超时时间 = 平均业务耗时 × 3 + 网络延迟缓冲(建议200ms~500ms)
2. 锁误删防护
通过三层防护确保安全:
- 客户端唯一标识(UUID)
- Lua脚本原子验证
- 锁版本号机制(类似CAS)
五、技术方案对比分析
Redis锁 vs 数据库锁
维度 | Redis锁 | 数据库锁 |
---|---|---|
性能 | 10万+ QPS | 1000 QPS |
实现复杂度 | 中等 | 简单 |
可靠性 | 依赖Redis可用性 | 依赖数据库事务 |
适用场景 | 高频短操作 | 低频长事务 |
RedLock算法进阶
当需要更高可靠性时,可以采用RedLock算法:
- 向5个Redis实例顺序获取锁
- 半数以上成功视为获取成功
- 总耗时需小于锁有效期
// RedLock.NET示例
var redlockFactory = RedLockFactory.Create(
new List<RedLockMultiplexer> { connection });
using var redLock = await redlockFactory.CreateLockAsync(
"resourceKey",
TimeSpan.FromSeconds(30));
六、最佳实践路线
实施步骤建议
- 评估业务场景的并发强度
- 确定可接受的异常边界
- 选择基础锁或RedLock方案
- 建立监控指标(锁等待时间、冲突率)
- 设计降级方案(如本地锁+Redis锁组合)
性能优化技巧
- 连接池大小设置:建议最大连接数=并发线程数×1.5
- 序列化优化:优先使用MessagePack代替JSON
- 批量操作:使用IBatch接口提升吞吐量
七、总结与展望
经过多个项目的实战检验,合理使用Redis分布式锁可以解决90%的并发问题。但需要注意:
- 锁粒度要尽可能小(如按用户ID分段)
- 必须设置合理的超时时间
- 配合熔断机制防止Redis故障扩散
未来趋势方面,随着.NET 6的普及,可以尝试将锁逻辑与Channel结合,实现更高效的事件驱动架构。但核心思想不会变——就像交通信号灯,分布式锁的本质是资源访问的协调者。