闽公网安备 35020302035485号
public interface IDistributedLock
{
/// <summary>
/// 尝试获取分布式锁。
/// </summary>
/// <param name="resourceKey">要锁定的资源标识。</param>
/// <param name="lockDuration">锁的持续时间。</param>
/// <returns>是否成功获取锁。</returns>
Task<bool> TryAcquireLockAsync(string resourceKey, TimeSpan? lockDuration = null);
/// <summary>
/// 释放分布式锁。
/// </summary>
/// <param name="resourceKey">要释放的资源标识。</param>
Task ReleaseLockAsync(string resourceKey);
}
这个接口定义了两个核心方法:ReleaseLockAsync:释放已获取的锁,允许其他操作进入临界区。
public class RedisDistributedLock : IDistributedLock
{
private readonly ConnectionMultiplexer _redisConnection;
private IDatabase _database;
public RedisDistributedLock(ConnectionMultiplexer redisConnection)
{
_redisConnection = redisConnection;
_database = _redisConnection.GetDatabase();
}
public Task<bool> TryAcquireLockAsync(string resourceKey, TimeSpan? lockDuration = null)
{
var isLockAcquired = _database.StringSetAsync(resourceKey, 1, lockDuration, When.NotExists);
return isLockAcquired;
}
public Task ReleaseLockAsync(string resourceKey)
{
return _database.KeyDeleteAsync(resourceKey);
}
}
在这个实现中使用的是StackExchange.Redis的SDK,当然大家可以自行选择合适的库来实现,主要是演示起来方便,因为其他库需要用脚本自行实现可过期的SETNX:-- 参数: KEYS[1] 表示键,ARGV[1] 表示值,ARGV[2] 表示过期时间(秒)
if redis.call("SETNX", KEYS[1], ARGV[1]) == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[2])
return 1
else
return 0
end
使用 SETNX 尝试设置键 KEYS[1] 的值为 ARGV[1]。如果键不存在,则返回 1 并成功设置键;如果键已存在,则返回 0。最终脚本返回 1 表示成功设置了键值对并设置了过期时间,返回 0 表示键已经存在,操作未成功。
public class LocalLock : IDistributedLock
{
private readonly ConcurrentDictionary<string, byte> lockCounts = new ConcurrentDictionary<string, byte>();
// 堆代码 duidaima.com
public Task<bool> TryAcquireLockAsync(string resourceKey, TimeSpan? lockDuration = null)
{
byte lockCount = 0;
if (lockCounts.TryAdd(resourceKey, lockCount))
{
lockCounts[resourceKey] = 1;
return Task.FromResult(true);
}
return Task.FromResult(false);
}
public Task ReleaseLockAsync(string resourceKey)
{
lockCounts.TryRemove(resourceKey, out _);
return Task.CompletedTask;
}
}
在这个实现中:public class DistributedLockFilterAttribute : Attribute, IAsyncActionFilter
{
private readonly string _lockPrefix;
private readonly LockType _lockType;
public DistributedLockFilterAttribute(string keyPrefix, LockType lockType = LockType.Local)
{
_lockPrefix = keyPrefix;
_lockType = lockType;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
IDistributedLock distributedLock = context.HttpContext.RequestServices.GetRequiredKeyedService<IDistributedLock>(_lockType.GetDescription());
string controllerName = context.RouteData.Values["controller"]?.ToString() ?? "";
string actionName = context.RouteData.Values["action"]?.ToString() ?? "";
//用户信息或其他唯一标识都可
var userKey = context.HttpContext.User!.Identity!.Name;
string lockKey = $"{_lockPrefix}:{userKey}:{controllerName}_{actionName}";
bool isLockAcquired = await distributedLock.TryAcquireLockAsync(lockKey);
if (!isLockAcquired)
{
context.Result = new ObjectResult(new { code = 400, message = "请不要重复操作" });
return;
}
try
{
await next();
}
finally
{
await distributedLock.ReleaseLockAsync(lockKey);
}
}
}
在这个过滤器的操作中:public enum LockType
{
[Description("redis")]
Redis,
[Description("local")]
Local
}
public static class EnumExtensions
{
public static string GetDescription(this Enum @enum)
{
Type type = @enum.GetType();
string name = Enum.GetName(type, @enum);
if (name == null)
{
return null;
}
FieldInfo field = type.GetField(name);
DescriptionAttribute attribute = System.Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) as DescriptionAttribute;
if (attribute == null)
{
return name;
}
return attribute?.Description;
}
}
这个扩展方法可以更方便地根据枚举的类型获取对应的枚举描述,从而在依赖注入中灵活的选择不同锁的实现,如果有更好的实现方式也可以,我们尽量使用更容易懂的方式。builder.Services.AddSingleton<ConnectionMultiplexer>(_ => ConnectionMultiplexer.Connect(builder.Configuration["Redis:ConnectionString"]!)); //给IDistributedLock添加不同的实现 builder.Services.AddKeyedSingleton<IDistributedLock, RedisDistributedLock>(LockType.Redis.GetDescription()); builder.Services.AddKeyedSingleton<IDistributedLock, LocalLock>(LockType.Local.GetDescription());在这里,我们注册了 Redis 和本地两种分布式锁实现,并使用键(key)区分它们,以便在运行时根据需要选择具体的锁类型。接下来,在控制器的操作方法上应用我们定义的 DistributedLockFilter 过滤器,用来实现Action的防抖功能。
[HttpGet("GetCurrentTime")]
[DistributedLockFilter("GetCurrentTime", LockType.Redis)]
public async Task<string> GetCurrentTime()
{
await Task.Delay(10000); // 模拟长时间操作
return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
在这个简单的示例中:{
"code": 400,
"message": "请不要重复操作"
}
总结