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": "请不要重复操作" }总结