ToB企服应用市场:ToB评测及商务社交产业平台

标题: 利用分布式锁在ASP.NET Core中实现防抖 [打印本页]

作者: 水军大提督    时间: 2024-9-4 06:49
标题: 利用分布式锁在ASP.NET Core中实现防抖
媒介

在 Web 应用开辟过程中,防抖(Debounce) 是确保同一操纵在短时间内不会被重复触发的一种有效手段。常见的场景包罗防止用户在短时间内重复提交表单,或者避免多次点击按钮导致后台服务实行多次雷同的操纵。无论在单机环境中,照旧在分布式体系中都有一些场景必要利用它。本文将先容如何在ASP.NET Core中通过利用锁的方式来实现防抖,从而保证无论在单个或多实例部署的环境下都能有效避免重复操纵。
分布式锁接口定义

要实现分布式锁的第一步是定义一个通用的锁接口。通过 IDistributedLock 接口,应用程序可以在不同的场景中选择利用不同类型的锁来实现。
  1. public interface IDistributedLock
  2. {
  3.     /// <summary>
  4.     /// 尝试获取分布式锁。
  5.     /// </summary>
  6.     /// <param name="resourceKey">要锁定的资源标识。</param>
  7.     /// <param name="lockDuration">锁的持续时间。</param>
  8.     /// <returns>是否成功获取锁。</returns>
  9.     Task<bool> TryAcquireLockAsync(string resourceKey, TimeSpan? lockDuration = null);
  10.     /// <summary>
  11.     /// 释放分布式锁。
  12.     /// </summary>
  13.     /// <param name="resourceKey">要释放的资源标识。</param>
  14.     Task ReleaseLockAsync(string resourceKey);
  15. }
复制代码
这个接口定义了两个核心方法:
Redis 版本的分布式锁实现

在一样平常开辟的方案中,Redis 是一个常见的分布式锁实现方式。通过 Redis 的原子操纵配合SETNX指令,可以确保在多个实例环境中只有一个实例能够获取到锁。下面是 Redis 版本的分布式锁实现代码。
  1. public class RedisDistributedLock : IDistributedLock
  2. {
  3.     private readonly ConnectionMultiplexer _redisConnection;
  4.     private IDatabase _database;
  5.     public RedisDistributedLock(ConnectionMultiplexer redisConnection)
  6.     {
  7.         _redisConnection = redisConnection;
  8.         _database = _redisConnection.GetDatabase();
  9.     }
  10.     public Task<bool> TryAcquireLockAsync(string resourceKey, TimeSpan? lockDuration = null)
  11.     {
  12.         var isLockAcquired = _database.StringSetAsync(resourceKey, 1, lockDuration, When.NotExists);
  13.         return isLockAcquired;
  14.     }
  15.     public Task ReleaseLockAsync(string resourceKey)
  16.     {
  17.         return _database.KeyDeleteAsync(resourceKey);
  18.     }
  19. }
复制代码
在这个实现中利用的是StackExchange.Redis的SDK,当然大家可以自行选择合适的库来实现,主要是演示起来方便,因为其他库必要用脚本自行实现可过期的SETNX:
如果你选用别的Redis的SDK,一般必要写脚本来实现可以过期的SETNX,可以参考下面的LUA脚本
  1. -- 参数: KEYS[1] 表示键,ARGV[1] 表示值,ARGV[2] 表示过期时间(秒)
  2. if redis.call("SETNX", KEYS[1], ARGV[1]) == 1 then
  3.     redis.call("EXPIRE", KEYS[1], ARGV[2])
  4.     return 1
  5. else
  6.     return 0
  7. end
复制代码
当地锁的实现

在某些环境下,例如单机或单体应用中,利用当地锁可能会更为合适。这个时候利用基于内存的当地锁实现效果可能会更好。有的同学可能会担心哀求量的问题,导致内存占用过高的问题。其实换个角度思量,如果有很大哀求量或并发量,大多数我们可能不会直接利用单机。好了我们继续来看,这里我们为了方便,直接利用ConcurrentDictionary来实现。
  1. public class LocalLock : IDistributedLock
  2. {
  3.     private readonly ConcurrentDictionary<string, byte> lockCounts = new ConcurrentDictionary<string, byte>();
  4.     public Task<bool> TryAcquireLockAsync(string resourceKey, TimeSpan? lockDuration = null)
  5.     {
  6.         byte lockCount = 0;
  7.         if (lockCounts.TryAdd(resourceKey, lockCount))
  8.         {
  9.             lockCounts[resourceKey] = 1;
  10.             return Task.FromResult(true);
  11.         }
  12.         return Task.FromResult(false);
  13.     }
  14.     public Task ReleaseLockAsync(string resourceKey)
  15.     {
  16.         lockCounts.TryRemove(resourceKey, out _);
  17.         return Task.CompletedTask;
  18.     }
  19. }
复制代码
在这个实现中:
其实如果C#提供ConcurrentHashSet的话,用ConcurrentHashSet来实现会更好一点。毕竟ConcurrentDictionary是KV的方式来是实现,每个Value都会浪费一定的内存空间。当然你也可以选择自行实现一套ConcurrentHashSet,必要注意的是实现的时候尽量利用桶锁,避免利用全局锁。
防抖过滤器的实现

接下来我们利用上面定义的IDistributedLock和Filter来实现防抖过滤器,我们创建一个基于 IAsyncActionFilter 接口实现的过滤器,更方便我们在哀求实行前后获取和开释锁操纵。
  1. public class DistributedLockFilterAttribute : Attribute, IAsyncActionFilter
  2. {
  3.     private readonly string _lockPrefix;
  4.     private readonly LockType _lockType;
  5.     public DistributedLockFilterAttribute(string keyPrefix, LockType lockType = LockType.Local)
  6.     {
  7.         _lockPrefix = keyPrefix;
  8.         _lockType = lockType;
  9.     }
  10.     public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
  11.     {
  12.         IDistributedLock distributedLock = context.HttpContext.RequestServices.GetRequiredKeyedService<IDistributedLock>(_lockType.GetDescription());
  13.         string controllerName = context.RouteData.Values["controller"]?.ToString() ?? "";
  14.         string actionName = context.RouteData.Values["action"]?.ToString() ?? "";
  15.         //用户信息或其他唯一标识都可
  16.         var userKey = context.HttpContext.User!.Identity!.Name;
  17.         string lockKey = $"{_lockPrefix}:{userKey}:{controllerName}_{actionName}";
  18.         bool isLockAcquired = await distributedLock.TryAcquireLockAsync(lockKey);
  19.         
  20.         if (!isLockAcquired)
  21.         {
  22.             context.Result = new ObjectResult(new { code = 400, message = "请不要重复操作" });
  23.             return;
  24.         }
  25.         try
  26.         {
  27.             await next();
  28.         }
  29.         finally
  30.         {
  31.             await distributedLock.ReleaseLockAsync(lockKey);
  32.         }
  33.     }
  34. }
复制代码
在这个过滤器的操纵中:
为了更灵活地在不同的锁实现之间进行切换,我们定义了一个枚举 LockType,通过扩展方法 GetDescription 获取其描述,方便我们利用它的值。
  1. public enum LockType
  2. {
  3.     [Description("redis")]
  4.     Redis,
  5.     [Description("local")]
  6.     Local
  7. }
  8. public static class EnumExtensions
  9. {
  10.     public static string GetDescription(this Enum @enum)
  11.     {
  12.         Type type = @enum.GetType();
  13.         string name = Enum.GetName(type, @enum);
  14.         if (name == null)
  15.         {
  16.             return null;
  17.         }
  18.         FieldInfo field = type.GetField(name);
  19.         DescriptionAttribute attribute = System.Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) as DescriptionAttribute;
  20.         if (attribute == null)
  21.         {
  22.             return name;
  23.         }
  24.         return attribute?.Description;
  25.     }
  26. }
复制代码
这个扩展方法可以更方便地根据枚举的类型获取对应的枚举描述,从而在依赖注入中灵活的选择不同锁的实现,如果有更好的实现方式也可以,我们尽量利用更容易懂的方式。
注册和利用过滤器

在ASP.NET Core中,我们可以通过依赖注入的方式注册分布式锁相干的服务,并在控制器操纵中应用防抖过滤器的功能,以下是注册和利用分布式锁的示例代码。
  1. builder.Services.AddSingleton<ConnectionMultiplexer>(_ => ConnectionMultiplexer.Connect(builder.Configuration["Redis:ConnectionString"]!));
  2. //给IDistributedLock添加不同的实现
  3. builder.Services.AddKeyedSingleton<IDistributedLock, RedisDistributedLock>(LockType.Redis.GetDescription());
  4. builder.Services.AddKeyedSingleton<IDistributedLock, LocalLock>(LockType.Local.GetDescription());
复制代码
在这里,我们注册了 Redis 和当地两种分布式锁实现,并利用键(key)区分它们,以便在运行时根据必要选择具体的锁类型。
接下来,在控制器的操纵方法上应用我们定义的 DistributedLockFilter 过滤器,用来实现Action的防抖功能。
  1. [HttpGet("GetCurrentTime")]
  2. [DistributedLockFilter("GetCurrentTime", LockType.Redis)]
  3. public async Task<string> GetCurrentTime()
  4. {
  5.     await Task.Delay(10000); // 模拟长时间操作
  6.     return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
  7. }
复制代码
在这个简单的示例中:
如果是在10s之内一连多次哀求则会返回如下错误
  1. {
  2.   "code": 400,
  3.   "message": "请不要重复操作"
  4. }
复制代码
总结

本文详细先容了如何在 ASP.NET Core 中利用分布式锁实现防抖功能。通过定义通用的 IDistributedLock 接口,我们可以实现不同类型的锁机制,包罗 Redis 和当地内存锁。Redis 锁利用其原子操纵确保分布式环境中的唯一性,而当地锁则实用于单机环境。通过创建 DistributedLockFilter 过滤器,我们将锁机制集成到 ASP.NET Core 控制器中,防止对Action进行重复操纵。
这种方法不光提高了应用的稳定性,也增强了用户体验,避免了短时间内重复操纵的问题。希望本文对大家有所帮助。如果有任何问题或进一步讨论的需求,欢迎在批评区留言。


  
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4