一、为什么需要分布式锁
想象一下这样的场景:你的电商系统正在搞秒杀活动,同一件商品在同一时刻可能有成百上千的用户点击购买。如果不加控制,很可能会导致超卖问题——库存显示还有10件,结果卖出去20件。这就是典型的并发问题,而分布式锁就是解决这类问题的利器。
在单机环境下,我们可以用简单的lock关键字或者Mutex来解决。但在分布式系统中,不同的请求可能被分发到不同的服务器上,这时候就需要一个所有服务器都能访问的中间件来协调锁的状态。
二、基于Redis的实现方案
Redis是最常用的分布式锁实现方案之一,主要因为它性能高、使用简单。我们来看一个完整的实现示例:
// 技术栈:.NET Core + StackExchange.Redis
public class RedisDistributedLock
{
private readonly IDatabase _redisDb;
public RedisDistributedLock(IConnectionMultiplexer connection)
{
_redisDb = connection.GetDatabase();
}
public async Task<bool> AcquireLockAsync(string lockKey, string lockValue, TimeSpan expiry)
{
// 使用SETNX命令尝试获取锁
return await _redisDb.StringSetAsync(
lockKey,
lockValue,
expiry,
When.NotExists,
CommandFlags.None);
}
public async Task ReleaseLockAsync(string lockKey, string lockValue)
{
// 使用Lua脚本确保只有锁的持有者才能释放锁
var script = @"if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end";
await _redisDb.ScriptEvaluateAsync(
script,
new RedisKey[] { lockKey },
new RedisValue[] { lockValue });
}
}
这个实现有几个关键点需要注意:
- 使用SETNX命令保证原子性操作
- 给锁设置过期时间,防止死锁
- 释放锁时使用Lua脚本保证操作的原子性
- 每个锁都有唯一标识,防止误删其他客户端的锁
优点:
- 性能极高,Redis本身单机可以支持10万+的QPS
- 实现相对简单
- 社区支持好,资料丰富
缺点:
- Redis主从切换时可能导致锁丢失(虽然概率低)
- 需要处理锁续期问题(长时间任务)
三、基于数据库的实现方案
如果你的系统已经使用了关系型数据库,又不想引入额外的中间件,可以考虑基于数据库的实现。下面是使用SQL Server的实现:
// 技术栈:.NET Core + Dapper
public class SqlServerDistributedLock
{
private readonly string _connectionString;
public SqlServerDistributedLock(string connectionString)
{
_connectionString = connectionString;
}
public async Task<bool> AcquireLockAsync(string lockName, int timeoutSeconds)
{
using var connection = new SqlConnection(_connectionString);
// 使用存储过程实现锁获取
var parameters = new DynamicParameters();
parameters.Add("@Resource", lockName);
parameters.Add("@LockTimeout", timeoutSeconds);
parameters.Add("@Result", dbType: DbType.Int32, direction: ParameterDirection.Output);
await connection.ExecuteAsync(
"sp_getapplock",
parameters,
commandType: CommandType.StoredProcedure);
return parameters.Get<int>("@Result") >= 0;
}
public async Task ReleaseLockAsync(string lockName)
{
using var connection = new SqlConnection(_connectionString);
var parameters = new DynamicParameters();
parameters.Add("@Resource", lockName);
parameters.Add("@Result", dbType: DbType.Int32, direction: ParameterDirection.Output);
await connection.ExecuteAsync(
"sp_releaseapplock",
parameters,
commandType: CommandType.StoredProcedure);
}
}
这里利用了SQL Server内置的应用程序锁机制,关键点包括:
- 使用系统存储过程sp_getapplock和sp_releaseapplock
- 支持锁超时设置
- 数据库事务会自动释放关联的锁
优点:
- 无需额外基础设施
- 与数据库事务天然集成
- 可靠性高
缺点:
- 性能较差,不适合高并发场景
- 数据库连接池可能成为瓶颈
- 不同数据库实现差异大
四、基于ZooKeeper的实现方案
对于需要高可靠性的场景,ZooKeeper是个不错的选择。虽然使用起来比Redis复杂,但提供了更强的保证:
// 技术栈:.NET Core + ZooKeeperNetEx
public class ZooKeeperDistributedLock
{
private readonly ZooKeeper _zk;
private string _lockPath;
public ZooKeeperDistributedLock(string connectionString)
{
_zk = new ZooKeeper(
connectionString,
TimeSpan.FromSeconds(10),
new NullWatcher());
}
public async Task<bool> AcquireLockAsync(string lockName, TimeSpan timeout)
{
var lockNode = $"/locks/{lockName}";
var ourPath = await _zk.CreateAsync(
$"{lockNode}/lock-",
Array.Empty<byte>(),
ZooKeeperNetEx.Ids.OPEN_ACL_UNSAFE,
CreateMode.EphemeralSequential);
// 获取所有子节点并排序
var children = await _zk.GetChildrenAsync(lockNode, false);
var sortedChildren = children.OrderBy(c => c).ToList();
// 检查我们是否是最小的节点
var ourIndex = sortedChildren.IndexOf(ourPath.Split('/').Last());
if (ourIndex == 0)
{
_lockPath = ourPath;
return true;
}
// 如果不是,监听前一个节点
var watch = new ManualResetEvent(false);
var watcher = new NodeDeletedWatcher(watch);
await _zk.ExistsAsync($"{lockNode}/{sortedChildren[ourIndex - 1]}", watcher);
// 等待超时或前一个节点被删除
return watch.WaitOne(timeout);
}
public async Task ReleaseLockAsync()
{
if (_lockPath != null)
{
await _zk.DeleteAsync(_lockPath, -1);
_lockPath = null;
}
}
}
ZooKeeper实现的关键特性:
- 使用临时顺序节点实现锁
- 通过监听机制实现锁等待
- 会话断开时自动清理临时节点
优点:
- 可靠性最高,不会出现Redis那样的锁丢失问题
- 提供公平锁实现
- 原生支持锁重入
缺点:
- 部署和维护成本高
- 性能比Redis差
- 使用复杂度高
五、方案对比与选型建议
现在我们来横向对比这三种方案:
- 性能方面:
- Redis > ZooKeeper > 数据库
- 如果QPS超过1万,基本只能选Redis
- 可靠性方面:
- ZooKeeper > 数据库 > Redis
- 金融级场景建议ZooKeeper
- 实现复杂度:
- Redis < 数据库 < ZooKeeper
- 快速开发首选Redis
- 适用场景:
- Redis:秒杀、缓存击穿防护
- 数据库:低频但需要强一致性的业务
- ZooKeeper:分布式协调、配置中心
六、实际应用中的注意事项
无论选择哪种方案,都需要注意以下几点:
- 锁的过期时间要设置合理:
- 太短会导致任务未完成锁就释放
- 太长会导致死锁时恢复慢
- 一定要实现锁的续期机制:
// Redis锁续期示例
public async Task RenewLockAsync(string lockKey, string lockValue, TimeSpan expiry)
{
if (await _redisDb.StringGetAsync(lockKey) == lockValue)
{
await _redisDb.KeyExpireAsync(lockKey, expiry);
}
}
- 考虑锁的可重入性:
- 同一个线程多次获取同一个锁应该成功
- 可以通过ThreadLocal存储锁计数实现
- 监控和报警:
- 记录锁等待时间
- 锁获取失败率超过阈值要报警
七、总结
分布式锁是分布式系统中的重要基础设施,没有放之四海而皆准的完美方案。Redis适合大多数高性能场景,数据库方案适合低频强一致性需求,ZooKeeper则适用于对可靠性要求极高的场景。
在实际应用中,除了考虑性能、可靠性外,还要结合团队的技术栈和运维能力。有时候,简单的方案反而是最好的选择。希望这篇文章能帮助你在项目中做出合理的技术选型。
评论