.net core RedLockNet 分布式锁
github源码地址
samcook/RedLock.net: An implementation of the Redlock algorithm in C# (github.com)
源码结构
主要代码在 RedLockFactory ,RedLock 两个类中
RedLock 创建 lock 和 释放 lock
主要看 RedLock 代码
下面 是主要只读字段
private readonly ICollection<RedisConnection> redisCaches; //这是redis节点集合 private readonly int quorum;//法定数量 private readonly int quorumRetryCount; private readonly int quorumRetryDelayMs;
private readonly TimeSpan expiryTime; //过期时间
private readonly TimeSpan? waitTime;// 等待时间,相同的 resource 如果当前的锁被其他线程占用,最多等待时间
private readonly TimeSpan? retryTime;// retryTime 等待时间内,多久尝试获取一次
quorum = redisCaches.Count / 2 + 1; // 这个算了节点数除2加1
quorumRetryCount = retryConfiguration?.RetryCount ?? DefaultQuorumRetryCount;
quorumRetryDelayMs = retryConfiguration?.RetryDelayMs ?? DefaultQuorumRetryDelayMs;
创建锁 主要 是实例化 RedLock 和 调用 redisLock.Start();
internal static RedLock Create( ILogger<RedLock> logger, ICollection<RedisConnection> redisCaches, string resource, TimeSpan expiryTime, TimeSpan? waitTime = null, TimeSpan? retryTime = null, RedLockRetryConfiguration retryConfiguration = null, CancellationToken? cancellationToken = null) { var redisLock = new RedLock( logger, redisCaches, resource, expiryTime, waitTime, retryTime, retryConfiguration, cancellationToken); redisLock.Start(); return redisLock; }
下面 主要是给参数赋值
private RedLock( ILogger<RedLock> logger, ICollection<RedisConnection> redisCaches, string resource, TimeSpan expiryTime, TimeSpan? waitTime = null, TimeSpan? retryTime = null, RedLockRetryConfiguration retryConfiguration = null, CancellationToken? cancellationToken = null) { this.logger = logger; if (expiryTime < MinimumExpiryTime) { logger.LogWarning($"Expiry time {expiryTime.TotalMilliseconds}ms too low, setting to {MinimumExpiryTime.TotalMilliseconds}ms"); expiryTime = MinimumExpiryTime; } if (retryTime != null && retryTime.Value < MinimumRetryTime) { logger.LogWarning($"Retry time {retryTime.Value.TotalMilliseconds}ms too low, setting to {MinimumRetryTime.TotalMilliseconds}ms"); retryTime = MinimumRetryTime; } this.redisCaches = redisCaches; quorum = redisCaches.Count / 2 + 1; quorumRetryCount = retryConfiguration?.RetryCount ?? DefaultQuorumRetryCount; quorumRetryDelayMs = retryConfiguration?.RetryDelayMs ?? DefaultQuorumRetryDelayMs; Resource = resource; LockId = Guid.NewGuid().ToString(); this.expiryTime = expiryTime; this.waitTime = waitTime; this.retryTime = retryTime; this.cancellationToken = cancellationToken ?? CancellationToken.None; }
然后 是 主要代码 Start 主要逻辑就是 判断时间 然后调用 Acquire(); 方法
private void Start() { if (waitTime.HasValue && retryTime.HasValue && waitTime.Value.TotalMilliseconds > 0 && retryTime.Value.TotalMilliseconds > 0) { var stopwatch = Stopwatch.StartNew(); // ReSharper disable PossibleInvalidOperationException while (!IsAcquired && stopwatch.Elapsed <= waitTime.Value) { (Status, InstanceSummary) = Acquire(); if (!IsAcquired) { TaskUtils.Delay(retryTime.Value, cancellationToken).Wait(cancellationToken); } } // ReSharper restore PossibleInvalidOperationException } else { (Status, InstanceSummary) = Acquire(); } logger.LogInformation($"Lock status: {Status} ({InstanceSummary}), {Resource} ({LockId})"); if (IsAcquired) { StartAutoExtendTimer(); } }
private (RedLockStatus, RedLockInstanceSummary) Acquire() { var lockSummary = new RedLockInstanceSummary(); for (var i = 0; i < quorumRetryCount; i++) { cancellationToken.ThrowIfCancellationRequested(); var iteration = i + 1; logger.LogDebug($"Lock attempt {iteration}/{quorumRetryCount}: {Resource} ({LockId}), expiry: {expiryTime}"); var stopwatch = Stopwatch.StartNew(); lockSummary = Lock(); var validityTicks = GetRemainingValidityTicks(stopwatch); logger.LogDebug($"Acquired locks for {Resource} ({LockId}) in {lockSummary.Acquired}/{redisCaches.Count} instances, quorum: {quorum}, validityTicks: {validityTicks}"); if (lockSummary.Acquired >= quorum && validityTicks > 0) { return (RedLockStatus.Acquired, lockSummary); } // we failed to get enough locks for a quorum, unlock everything and try again Unlock(); // only sleep if we have more retries left if (i < quorumRetryCount - 1) { var sleepMs = ThreadSafeRandom.Next(quorumRetryDelayMs); logger.LogDebug($"Sleeping {sleepMs}ms"); TaskUtils.Delay(sleepMs, cancellationToken).Wait(cancellationToken); } } var status = GetFailedRedLockStatus(lockSummary); // give up logger.LogDebug($"Could not acquire quorum after {quorumRetryCount} attempts, giving up: {Resource} ({LockId}). {lockSummary}."); return (status, lockSummary); }
加锁的方法 主要 是调用 LockInstance
private RedLockInstanceSummary Lock()
{
var lockResults = new ConcurrentBag<RedLockInstanceResult>();
Parallel.ForEach(redisCaches, cache =>
{
lockResults.Add(LockInstance(cache));
});
return PopulateRedLockResult(lockResults);
}
LockInstance 方法怎么加锁的 注意 这个方法 是并行循环调用的 也就是每个redis节点 都加了 一个带过期时间的key
主要代码 原来就是 加了 key 过期时间
cache.ConnectionMultiplexer .GetDatabase(cache.RedisDatabase) .StringSet(redisKey, LockId, expiryTime, When.NotExists, CommandFlags.DemandMaster);
然后 给状态 赋值 result = redisResult ? RedLockInstanceResult.Success : RedLockInstanceResult.Conflicted;
private RedLockInstanceResult LockInstance(RedisConnection cache) { var redisKey = GetRedisKey(cache.RedisKeyFormat, Resource); var host = GetHost(cache.ConnectionMultiplexer); RedLockInstanceResult result; try { logger.LogTrace($"LockInstance enter {host}: {redisKey}, {LockId}, {expiryTime}"); var redisResult = cache.ConnectionMultiplexer .GetDatabase(cache.RedisDatabase) .StringSet(redisKey, LockId, expiryTime, When.NotExists, CommandFlags.DemandMaster); result = redisResult ? RedLockInstanceResult.Success : RedLockInstanceResult.Conflicted; } catch (Exception ex) { logger.LogDebug($"Error locking lock instance {host}: {ex.Message}"); result = RedLockInstanceResult.Error; } logger.LogTrace($"LockInstance exit {host}: {redisKey}, {LockId}, {result}"); return result; }
解锁 主要 调用 UnlockInstance
private void Unlock() { // ReSharper disable once MethodSupportsCancellation extendUnlockSemaphore.Wait(); try { Parallel.ForEach(redisCaches, UnlockInstance); } finally { extendUnlockSemaphore.Release(); } }
UnlockInstance 这里主要代码 原来是 并行循环 调用了 lua 脚本
private void UnlockInstance(RedisConnection cache)
{
var redisKey = GetRedisKey(cache.RedisKeyFormat, Resource);
var host = GetHost(cache.ConnectionMultiplexer);
var result = false;
try
{
logger.LogTrace($"UnlockInstance enter {host}: {redisKey}, {LockId}");
result = (bool) cache.ConnectionMultiplexer
.GetDatabase(cache.RedisDatabase)
.ScriptEvaluate(UnlockScript, new RedisKey[] {redisKey}, new RedisValue[] {LockId}, CommandFlags.DemandMaster);
}
catch (Exception ex)
{
logger.LogDebug($"Error unlocking lock instance {host}: {ex.Message}");
}
logger.LogTrace($"UnlockInstance exit {host}: {redisKey}, {LockId}, {result}");
}
解锁脚本
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
public enum RedLockStatus { /// <summary> /// The lock has not yet been successfully acquired, or has been released. /// </summary> Unlocked, /// <summary> /// The lock was acquired successfully. /// </summary> Acquired, /// <summary> /// The lock was not acquired because there was no quorum available. /// </summary> NoQuorum, /// <summary> /// The lock was not acquired because it is currently locked with a different LockId. /// </summary> Conflicted, /// <summary> /// The lock expiry time passed before lock acquisition could be completed. /// </summary> Expired }
最后 总结 加锁
首先 实例化 RedLock 然后掉用 实例方法 Start ,Start 实例 方法 有判断 当前锁的 状态 RedLockStatus 为 Key 添加 过期时间 ,同时为每个redis节点 都添加 ,解锁是调用 解锁脚本 也是执行 每个节点解锁
原来原理这么简单