一、为什么需要分布式锁?
当我们的系统从单机架构升级到微服务架构时,多个服务实例同时访问共享资源的情况越来越常见。比如电商平台的库存扣减场景,如果有100个用户同时抢购最后一件商品,如何保证库存不会被错误地扣减成负数?这时候就需要分布式锁来协调多个节点的操作。
二、Redis实现分布式锁的核心原理
2.1 基础版实现方案
最基础的实现方式是使用Redis的SETNX命令:
SETNX lock_key unique_value
如果返回1表示获取锁成功,0则表示已被占用。但这样存在严重问题——如果客户端崩溃,锁永远不会释放。于是我们加上过期时间:
SETNX lock_key unique_value
EXPIRE lock_key 30
但这两个命令不是原子操作,可能在执行EXPIRE前发生崩溃。于是Redis 2.6.12+版本提供了更好的解决方案:
SET lock_key unique_value NX EX 30
这个原子命令同时完成设置值和过期时间,确保要么都成功,要么都失败。
2.2 解锁的正确姿势
直接使用DEL命令删除锁会导致严重问题:
DEL lock_key # 危险操作!
假设客户端A获取锁后处理时间过长,锁自动过期后被客户端B获取。此时A完成任务后执行DEL,就会误删B的锁。正确做法是通过Lua脚本验证值:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
这个脚本保证只有锁的持有者才能删除锁。
三、C#实现示例
using StackExchange.Redis;
public class RedisDistributedLock
{
private readonly IDatabase _redis;
private readonly string _lockKey;
private string _lockValue;
private readonly TimeSpan _expiry;
public RedisDistributedLock(IDatabase redis, string lockKey, TimeSpan expiry)
{
_redis = redis;
_lockKey = lockKey;
_expiry = expiry;
}
public bool AcquireLock()
{
// 生成唯一标识,建议使用Guid+线程ID
_lockValue = $"{Guid.NewGuid()}:{Environment.CurrentManagedThreadId}";
// 尝试获取锁
return _redis.LockTake(_lockKey, _lockValue, _expiry);
}
public void ReleaseLock()
{
var luaScript = @"if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end";
_redis.ScriptEvaluate(luaScript,
new RedisKey[] { _lockKey },
new RedisValue[] { _lockValue });
}
}
// 使用示例
var redis = ConnectionMultiplexer.Connect("localhost").GetDatabase();
var distributedLock = new RedisDistributedLock(redis, "order_lock", TimeSpan.FromSeconds(30));
try
{
if (distributedLock.AcquireLock())
{
// 执行业务逻辑
Console.WriteLine("成功获取分布式锁");
Thread.Sleep(10000); // 模拟业务处理
}
}
finally
{
distributedLock.ReleaseLock();
}
四、典型应用场景
- 库存扣减:防止超卖现象,确保库存操作的原子性
- 定时任务调度:避免多个节点重复执行定时任务
- 分布式事务协调:在跨服务操作中保持状态一致性
- 全局配置更新:保证配置变更的串行化执行
- 支付订单处理:防止重复支付、重复退款等资金风险
五、技术优缺点分析
5.1 核心优势
- 实现简单:相比ZooKeeper等方案更轻量
- 性能出色:Redis单节点可支持10万+ QPS
- 功能丰富:结合Lua脚本可实现复杂逻辑
- 高可用:通过Redis Cluster保证服务可用性
5.2 潜在风险
- 时钟漂移问题:各节点时间不同步可能影响锁有效期
- 脑裂风险:主从切换时可能产生多个客户端持有锁
- 锁续期难题:业务处理时间超过锁有效期时需要特殊处理
- 客户端阻塞:获取不到锁的客户端需要合理的等待策略
六、生产环境注意事项
- 锁标识唯一性:必须使用全局唯一值(推荐UUID+客户端标识)
- 过期时间设置:建议设置为平均业务处理时间的2-3倍
- 重试策略:采用指数退避算法,避免雪崩效应
- 监控报警:对锁等待时间、获取失败率设置监控指标
- 降级方案:在Redis不可用时要有备用策略(如本地锁+告警)
- 网络分区处理:配合token机制防止网络恢复后的误操作
七、进阶优化方案
对于更高要求的场景,可以考虑以下方案:
- Redlock算法:通过多个独立Redis节点实现更可靠的锁
- 自动续期机制:通过后台线程定期延长锁有效期
- 可重入锁实现:支持同一线程多次获取锁
- 公平锁实现:使用Redis队列维护等待顺序
- 联锁机制:同时获取多个关联资源锁时采用两阶段策略
八、总结与建议
Redis分布式锁就像交通信号灯,协调着分布式系统中的各个"车辆"有序通行。虽然实现简单,但要真正用好需要注意诸多细节。建议在以下场景优先考虑:
- 对性能要求较高的短期锁需求
- 已存在Redis基础设施的环境
- 可以容忍极小概率锁失效的业务场景
对于金融交易等强一致性要求的场景,建议采用Redlock方案或改用ZooKeeper等CP系统。无论选择哪种方案,都要记住:分布式锁不是银弹,合理设计业务逻辑才是根本。