一、为什么需要分布式锁?

当我们的系统从单机架构升级到微服务架构时,多个服务实例同时访问共享资源的情况越来越常见。比如电商平台的库存扣减场景,如果有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();
}

四、典型应用场景

  1. 库存扣减:防止超卖现象,确保库存操作的原子性
  2. 定时任务调度:避免多个节点重复执行定时任务
  3. 分布式事务协调:在跨服务操作中保持状态一致性
  4. 全局配置更新:保证配置变更的串行化执行
  5. 支付订单处理:防止重复支付、重复退款等资金风险

五、技术优缺点分析

5.1 核心优势

  • 实现简单:相比ZooKeeper等方案更轻量
  • 性能出色:Redis单节点可支持10万+ QPS
  • 功能丰富:结合Lua脚本可实现复杂逻辑
  • 高可用:通过Redis Cluster保证服务可用性

5.2 潜在风险

  • 时钟漂移问题:各节点时间不同步可能影响锁有效期
  • 脑裂风险:主从切换时可能产生多个客户端持有锁
  • 锁续期难题:业务处理时间超过锁有效期时需要特殊处理
  • 客户端阻塞:获取不到锁的客户端需要合理的等待策略

六、生产环境注意事项

  1. 锁标识唯一性:必须使用全局唯一值(推荐UUID+客户端标识)
  2. 过期时间设置:建议设置为平均业务处理时间的2-3倍
  3. 重试策略:采用指数退避算法,避免雪崩效应
  4. 监控报警:对锁等待时间、获取失败率设置监控指标
  5. 降级方案:在Redis不可用时要有备用策略(如本地锁+告警)
  6. 网络分区处理:配合token机制防止网络恢复后的误操作

七、进阶优化方案

对于更高要求的场景,可以考虑以下方案:

  1. Redlock算法:通过多个独立Redis节点实现更可靠的锁
  2. 自动续期机制:通过后台线程定期延长锁有效期
  3. 可重入锁实现:支持同一线程多次获取锁
  4. 公平锁实现:使用Redis队列维护等待顺序
  5. 联锁机制:同时获取多个关联资源锁时采用两阶段策略

八、总结与建议

Redis分布式锁就像交通信号灯,协调着分布式系统中的各个"车辆"有序通行。虽然实现简单,但要真正用好需要注意诸多细节。建议在以下场景优先考虑:

  • 对性能要求较高的短期锁需求
  • 已存在Redis基础设施的环境
  • 可以容忍极小概率锁失效的业务场景

对于金融交易等强一致性要求的场景,建议采用Redlock方案或改用ZooKeeper等CP系统。无论选择哪种方案,都要记住:分布式锁不是银弹,合理设计业务逻辑才是根本。