67 lines
2.0 KiB
C#
67 lines
2.0 KiB
C#
using StackExchange.Redis;
|
|
using System.Collections.Concurrent;
|
|
using adas_core.Application.Services.Interfaces;
|
|
|
|
namespace adas_core.Application.Services.Caching
|
|
{
|
|
/// <summary>
|
|
/// Lock distribuido en Redis. Usa token por adquisición (owner)
|
|
/// y liberación segura con script Lua: borra la key solo si el valor coincide.
|
|
/// </summary>
|
|
public class RedisLockProvider(Func<IDatabase?> getDatabase) : ILockProvider
|
|
{
|
|
private readonly string _prefix = "lock:";
|
|
private readonly TimeSpan _ttl = TimeSpan.FromSeconds(5);
|
|
private readonly TimeSpan _retryDelay = TimeSpan.FromMilliseconds(50);
|
|
|
|
private static readonly string LuaReleaseScript = @"
|
|
if redis.call('get', KEYS[1]) == ARGV[1] then
|
|
return redis.call('del', KEYS[1])
|
|
else
|
|
return 0
|
|
end";
|
|
|
|
private readonly ConcurrentDictionary<string, string> _tokens =
|
|
new(StringComparer.Ordinal);
|
|
|
|
public async Task<bool> AcquireAsync(string key, TimeSpan timeout)
|
|
{
|
|
var redis = getDatabase();
|
|
if (redis is null) return false;
|
|
|
|
var redisKey = (RedisKey)(_prefix + key);
|
|
var token = Guid.NewGuid().ToString("N");
|
|
var end = DateTime.UtcNow.Add(timeout);
|
|
|
|
while (DateTime.UtcNow < end)
|
|
{
|
|
if (await redis.StringSetAsync(redisKey, token, _ttl, When.NotExists))
|
|
{
|
|
_tokens[key] = token;
|
|
return true;
|
|
}
|
|
|
|
await Task.Delay(_retryDelay);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public async Task ReleaseAsync(string key)
|
|
{
|
|
if (!_tokens.TryRemove(key, out var token))
|
|
return;
|
|
|
|
var redis = getDatabase();
|
|
if (redis is null) return;
|
|
|
|
var redisKey = (RedisKey)(_prefix + key);
|
|
|
|
await redis.ScriptEvaluateAsync(
|
|
LuaReleaseScript,
|
|
[redisKey],
|
|
[token]
|
|
);
|
|
}
|
|
}
|
|
} |