分布式锁 - Redis实现
概述
分布式锁用于在多个服务实例之间协调对共享资源的访问。Redis 是实现分布式锁的最常用方案,因为它性能高、支持原子操作。
核心原理
客户端 A Redis 客户端 B
| | |
|-- SET lock_key value NX EX 30 ->| |
| |-- 设置成功(返回 OK) |
| | |-- SET lock_key value NX EX 30
| | |-- 设置失败(返回 nil)
| | |
|-- 执行业务逻辑... | |-- 等待或重试
| | |
|-- DEL lock_key ---------->| |
| |-- 删除成功 |
| | |-- SET lock_key value NX EX 30
| | |-- 设置成功,获取锁RedLock 算法
Redis 官方推荐的分布式锁算法(Martin Kleppmann 提出):
- 获取当前时间(毫秒)
- 依次尝试在 N 个 Redis 实例上获取锁
- 计算获取锁的总耗时
- 只有在大多数节点上都成功,且总耗时小于锁有效期,才认为成功
- 释放锁时,需要在所有节点上删除
C# 实现
1. 基础实现
csharp
using StackExchange.Redis;
public interface IDistributedLock
{
Task<bool> AcquireAsync(string key, TimeSpan expiry);
Task ReleaseAsync(string key);
Task<T> ExecuteWithLockAsync<T>(string key, Func<Task<T>> action, TimeSpan? expiry = null);
}
public class RedisDistributedLock : IDistributedLock
{
private readonly IDatabase _redis;
private readonly ILogger<RedisDistributedLock> _logger;
public RedisDistributedLock(
IConnectionMultiplexer redis,
ILogger<RedisDistributedLock> logger)
{
_redis = redis.GetDatabase();
_logger = logger;
}
public async Task<bool> AcquireAsync(string key, TimeSpan expiry)
{
var lockValue = Guid.NewGuid().ToString("N");
// SET key value NX EX seconds - 原子操作
var acquired = await _redis.StringSetAsync(
key,
lockValue,
expiry,
When.NotExists);
if (acquired)
{
_logger.LogDebug("Lock acquired: {Key}", key);
}
else
{
_logger.LogDebug("Lock not acquired: {Key}", key);
}
return acquired;
}
public async Task ReleaseAsync(string key)
{
// 直接删除(生产环境应该使用 Lua 脚本保证安全性)
await _redis.KeyDeleteAsync(key);
_logger.LogDebug("Lock released: {Key}", key);
}
public async Task<T> ExecuteWithLockAsync<T>(
string key,
Func<Task<T>> action,
TimeSpan? expiry = null)
{
var lockExpiry = expiry ?? TimeSpan.FromSeconds(30);
var lockAcquired = false;
try
{
// 尝试获取锁
lockAcquired = await AcquireAsync(key, lockExpiry);
if (!lockAcquired)
{
throw new TimeoutException($"Failed to acquire lock: {key}");
}
// 执行受保护的操作
return await action();
}
finally
{
// 确保释放锁
if (lockAcquired)
{
try
{
await ReleaseAsync(key);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to release lock: {Key}", key);
}
}
}
}
}2. 安全的锁释放(Lua 脚本)
csharp
public class SafeRedisDistributedLock : IDistributedLock
{
private readonly IDatabase _redis;
// Lua 脚本:只有持有者才能释放锁
private static readonly string ReleaseLockScript = @"
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end";
public async Task<bool> AcquireAsync(string key, TimeSpan expiry)
{
var lockValue = $"{Environment.MachineName}:{Guid.NewGuid():N}";
var acquired = await _redis.StringSetAsync(
key,
lockValue,
expiry,
When.NotExists);
return acquired;
}
public async Task ReleaseAsync(string key)
{
// 使用 Lua 脚本安全释放
var result = await _redis.ScriptEvaluateAsync(
ReleaseLockScript,
new RedisKey[] { key },
new RedisValue[] { GetLockValue(key) });
if ((int)result == 0)
{
_logger.LogWarning("Failed to release lock (may be held by another process): {Key}", key);
}
}
private string GetLockValue(string key)
{
// 实际生产中应该在获取锁时保存 lockValue
// 这里简化处理
return "";
}
}3. 带重试的锁获取
csharp
public class RetryableDistributedLock : IDistributedLock
{
private readonly IDatabase _redis;
private readonly int _maxRetries;
private readonly TimeSpan _retryDelay;
public RetryableDistributedLock(
IConnectionMultiplexer redis,
int maxRetries = 10,
TimeSpan? retryDelay = null)
{
_redis = redis.GetDatabase();
_maxRetries = maxRetries;
_retryDelay = retryDelay ?? TimeSpan.FromMilliseconds(100);
}
public async Task<bool> AcquireAsync(string key, TimeSpan expiry)
{
for (int i = 0; i < _maxRetries; i++)
{
var lockValue = Guid.NewGuid().ToString("N");
var acquired = await _redis.StringSetAsync(
key,
lockValue,
expiry,
When.NotExists);
if (acquired)
{
return true;
}
// 等待后重试
if (i < _maxRetries - 1)
{
await Task.Delay(_retryDelay);
}
}
return false;
}
public async Task ReleaseAsync(string key)
{
await _redis.KeyDeleteAsync(key);
}
}实际应用示例
1. 防止重复下单
csharp
public class OrderServiceWithLock
{
private readonly IDistributedLock _lock;
private readonly AppDbContext _dbContext;
public async Task<Order> CreateOrderAsync(Guid userId, CreateOrderRequest request)
{
var lockKey = $"order_lock:{userId}";
// 使用分布式锁保证同一用户不会并发创建订单
return await _lock.ExecuteWithLockAsync(lockKey, async () =>
{
// 检查是否有未支付的订单
var existingOrder = await _dbContext.Orders
.FirstOrDefaultAsync(o =>
o.UserId == userId &&
o.Status == "pending");
if (existingOrder != null)
{
throw new InvalidOperationException("You have a pending order");
}
// 创建新订单
var order = new Order
{
Id = Guid.NewGuid(),
UserId = userId,
TotalAmount = request.TotalAmount,
Status = "pending",
CreatedAt = DateTime.UtcNow
};
_dbContext.Orders.Add(order);
await _dbContext.SaveChangesAsync();
return order;
}, TimeSpan.FromSeconds(10));
}
}2. 库存扣减
csharp
public class InventoryServiceWithLock
{
private readonly IDistributedLock _lock;
private readonly AppDbContext _dbContext;
public async Task<Result<bool>> DeductStockAsync(
Guid productId,
int quantity)
{
var lockKey = $"inventory_lock:{productId}";
try
{
return await _lock.ExecuteWithLockAsync(lockKey, async () =>
{
var product = await _dbContext.Products.FindAsync(productId);
if (product == null)
{
return Result<bool>.Failure("Product not found");
}
if (product.Stock < quantity)
{
return Result<bool>.Failure("Insufficient stock");
}
product.Stock -= quantity;
await _dbContext.SaveChangesAsync();
return Result<bool>.Success(true);
}, TimeSpan.FromSeconds(5));
}
catch (TimeoutException)
{
return Result<bool>.Failure("System busy, please try again later");
}
}
}3. 定时任务防重复执行
csharp
public class ScheduledTaskService
{
private readonly IDistributedLock _lock;
private readonly ILogger<ScheduledTaskService> _logger;
public async Task ExecuteDailyReportAsync()
{
var lockKey = "scheduled_task:daily_report";
var lockExpiry = TimeSpan.FromMinutes(30); // 任务最长执行 30 分钟
var acquired = await _lock.AcquireAsync(lockKey, lockExpiry);
if (!acquired)
{
_logger.LogInformation("Daily report task is already running, skipping");
return;
}
try
{
_logger.LogInformation("Starting daily report generation");
// 执行报表生成逻辑
await GenerateDailyReportAsync();
_logger.LogInformation("Daily report generation completed");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to generate daily report");
throw;
}
finally
{
await _lock.ReleaseAsync(lockKey);
}
}
}RedLock.net 库(推荐)
1. 安装
bash
dotnet add package RedLock.net2. 使用
csharp
using RedLockNet;
using RedLockNet.SERedis;
using RedLockNet.SERedis.Configuration;
public class RedLockService
{
private readonly IRedLockFactory _redLockFactory;
public RedLockService(IConnectionMultiplexer redis)
{
var redLockEndpoints = new List<RedLockEndPoint>
{
new DnsEndPoint("redis1.example.com", 6379),
new DnsEndPoint("redis2.example.com", 6379),
new DnsEndPoint("redis3.example.com", 6379)
};
_redLockFactory = RedLockFactory.CreateFromMultiplexer(redis, redLockEndpoints);
}
public async Task<T> ExecuteWithRedLockAsync<T>(
string resource,
Func<Task<T>> action,
TimeSpan? expiry = null)
{
var lockExpiry = expiry ?? TimeSpan.FromSeconds(30);
await using (var redLock = await _redLockFactory.CreateLockAsync(
resource,
lockExpiry,
waitTime: TimeSpan.FromSeconds(10), // 等待获取锁的时间
retryTime: TimeSpan.FromMilliseconds(200))) // 重试间隔
{
if (redLock.IsAcquired)
{
// 成功获取锁,执行业务逻辑
return await action();
}
else
{
throw new TimeoutException($"Could not acquire lock for {resource}");
}
}
}
}
// 注册服务
builder.Services.AddSingleton<IRedLockFactory>(sp =>
{
var redis = sp.GetRequiredService<IConnectionMultiplexer>();
return RedLockFactory.CreateFromMultiplexer(redis, new List<RedLockEndPoint>
{
new DnsEndPoint("redis1", 6379),
new DnsEndPoint("redis2", 6379),
new DnsEndPoint("redis3", 6379)
});
});监控与告警
1. 指标收集
csharp
public class DistributedLockMetrics
{
private readonly Counter<long> _lockAttempts;
private readonly Counter<long> _lockSuccesses;
private readonly Counter<long> _lockFailures;
private readonly Histogram<double> _lockWaitTime;
private readonly Histogram<double> _lockHoldTime;
public void RecordLockAttempt(bool success, double waitTimeMs, double holdTimeMs)
{
_lockAttempts.Add(1);
if (success)
{
_lockSuccesses.Add(1);
}
else
{
_lockFailures.Add(1);
}
_lockWaitTime.Record(waitTimeMs);
_lockHoldTime.Record(holdTimeMs);
}
}2. Prometheus 告警
yaml
groups:
- name: distributed_lock_alerts
rules:
- alert: HighLockContention
expr: rate(distributed_lock_failures_total[5m]) / rate(distributed_lock_attempts_total[5m]) > 0.2
for: 5m
annotations:
summary: "High distributed lock contention"
description: "More than 20% of lock attempts are failing"
- alert: LongLockHoldTime
expr: histogram_quantile(0.99, distributed_lock_hold_time_seconds) > 30
for: 5m
annotations:
summary: "Long lock hold time"
description: "99th percentile lock hold time exceeds 30 seconds"最佳实践总结
✅ DO
- 设置合理的过期时间:避免死锁
- 使用唯一标识:确保只有持有者能释放锁
- 实现重试机制:提高成功率
- 监控锁竞争:及时发现性能问题
- 使用 RedLock:多节点部署时使用官方算法
❌ DON'T
- 不要长时间持有锁:尽快释放
- 不要忘记释放锁:使用 try-finally
- 不要用锁替代幂等性:两者结合使用
- 不要忽略超时:设置合理的等待时间
总结
Redis 分布式锁是处理分布式并发的有效工具:
✅ 优点:
- 高性能
- 实现简单
- 支持自动过期
⚠️ 注意事项:
- 需要处理锁超时
- 单点故障风险(使用 RedLock)
- 不能替代幂等性设计
在实际应用中,分布式锁应与幂等性机制结合使用,形成完整的并发控制体系。