利用分布式锁在ASP.NET Core中实现防抖
前言
在 Web
应用开发过程中,防抖(Debounce)
是确保同一操作在短时间内不会被重复触发的一种有效手段。常见的场景包括防止用户在短时间内重复提交表单,或者避免多次点击按钮导致后台服务执行多次相同的操作。无论在单机环境中,还是在分布式系统中都有一些场景需要使用它。本文将介绍如何在ASP.NET Core
中通过使用锁的方式来实现防抖,从而保证无论在单个或多实例部署的情况下都能有效避免重复操作。
分布式锁接口定义
要实现分布式锁的第一步是定义一个通用的锁接口。通过 IDistributedLock
接口,应用程序可以在不同的场景中选择使用不同类型的锁来实现。
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);
}
这个接口定义了两个核心方法:
TryAcquireLockAsync
:尝试获取分布式锁。如果锁获取成功,则返回true
,否则返回false
。ReleaseLockAsync
:释放已获取的锁,允许其他操作进入临界区。
Redis 版本的分布式锁实现
在日常开发的方案中,Redis
是一个常见的分布式锁实现方式。通过 Redis
的原子操作配合SETNX
指令,可以确保在多个实例环境中只有一个实例能够获取到锁。下面是 Redis
版本的分布式锁实现代码。
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
:
- 我们使用了
ConnectionMultiplexer
来管理与 Redis 的连接。 TryAcquireLockAsync
方法使用了StringSetAsync
方法,其中When.NotExists
参数确保只有在键不存在时才能成功设置值,从而实现锁的功能。ReleaseLockAsync
方法简单地删除了锁对应的键,从而释放锁。
如果你选用其它Redis的SDK,一般需要写脚本来实现可以过期的SETNX
,可以参考下面的LUA
脚本
-- 参数: 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。 - 如果
SETNX
返回 1,则为该键设置过期时间,过期时间为ARGV[2]
秒。 - 最终脚本返回 1 表示成功设置了键值对并设置了过期时间,返回 0 表示键已经存在,操作未成功。
本地锁的实现
在某些情况下,例如单机或单体应用中,使用本地锁可能会更为合适。这个时候使用基于内存的本地锁实现效果可能会更好。有的同学可能会担心请求量的问题,导致内存占用过高的问题。其实换个角度考虑,如果有很大请求量或并发量,大多数我们可能不会直接使用单机。好了我们继续来看,这里我们为了方便,直接使用ConcurrentDictionary
来实现。
public class LocalLock : IDistributedLock
{
private readonly ConcurrentDictionary<string, byte> lockCounts = new ConcurrentDictionary<string, byte>();
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;
}
}
在这个实现中:
- 我们使用
ConcurrentDictionary
来管理锁的状态,确保线程安全。 TryAcquireLockAsync
方法尝试在字典中添加一个键,如果成功则表示获取锁成功。ReleaseLockAsync
方法从字典中移除对应的键,从而释放锁。
其实如果
C#
提供ConcurrentHashSet
的话,用ConcurrentHashSet
来实现会更好一点。毕竟ConcurrentDictionary
是KV的方式来是实现,每个Value
都会浪费一定的内存空间。当然你也可以选择自行实现一套ConcurrentHashSet
,需要注意的是实现的时候尽量使用桶锁
,避免使用全局锁
。
防抖过滤器的实现
接下来我们使用上面定义的IDistributedLock
和Filter
来实现防抖过滤器,我们创建一个基于 IAsyncActionFilter
接口实现的过滤器,更方便我们在请求执行前后获取和释放锁操作。
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);
}
}
}
在这个过滤器的操作中:
- 我们通过容器和
LockType
获取具体的分布式锁实现。 - 使用
controllerName
和actionName
以及用户标识构(或其他唯一标识)建锁的键,确保锁的唯一性。 - 如果获取锁失败,则直接返回错误响应,避免后续操作的执行。
- 在操作执行完毕后,无论是否成功,都释放锁。
为了更灵活地在不同的锁实现之间进行切换,我们定义了一个枚举 LockType
,通过扩展方法 GetDescription
获取其描述,方便我们使用它的值。
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;
}
}
这个扩展方法可以更方便地根据枚举的类型获取对应的枚举描述,从而在依赖注入中灵活的选择不同锁的实现,如果有更好的实现方式也可以,我们尽量使用更容易懂的方式。
注册和使用过滤器
在ASP.NET Core
中,我们可以通过依赖注入的方式注册分布式锁相关的服务,并在控制器操作中应用防抖过滤器的功能,以下是注册和使用分布式锁的示例代码。
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");
}
在这个简单的示例中:
DistributedLockFilter
过滤器确保了当用户请求GetCurrentTime
操作时,不会在短时间内重复触发相同的操作。- 锁的类型被设置为
LockType.Redis
,因此在分布式环境下,多个实例之间也可以共享这个锁,当然这个类型是可选的。
如果是在10s之内连续多次请求则会返回如下错误
{
"code": 400,
"message": "请不要重复操作"
}
总结
本文详细介绍了如何在 ASP.NET Core 中使用分布式锁实现防抖功能。通过定义通用的 IDistributedLock
接口,我们可以实现不同类型的锁机制,包括 Redis 和本地内存锁。Redis 锁利用其原子操作确保分布式环境中的唯一性,而本地锁则适用于单机环境。通过创建 DistributedLockFilter
过滤器,我们将锁机制集成到 ASP.NET Core
控制器中,防止对Action
进行重复操作。
这种方法不仅提高了应用的稳定性,也增强了用户体验,避免了短时间内重复操作的问题。希望本文对大家有所帮助。如果有任何问题或进一步讨论的需求,欢迎在评论区留言。
利用分布式锁在ASP.NET Core中实现防抖的更多相关文章
- ASP.Net Core 中使用Zookeeper搭建分布式环境中的配置中心系列一:使用Zookeeper.Net组件演示基本的操作
前言:马上要过年了,祝大家新年快乐!在过年回家前分享一篇关于Zookeeper的文章,我们都知道现在微服务盛行,大数据.分布式系统中经常会使用到Zookeeper,它是微服务.分布式系统中必不可少的分 ...
- Asp.Net Core中利用Seq组件展示结构化日志功能
在一次.Net Core小项目的开发中,掌握的不够深入,对日志记录并没有好好利用,以至于一出现异常问题,都得跑动服务器上查看,那时一度怀疑自己肯定没学好,不然这一块日志不可能需要自己扒服务器日志来查看 ...
- Asp.Net Core 中利用QuartzHostedService 实现 Quartz 注入依赖 (DI)
QuartzHostedService 是一个用来在Asp.Net Core 中实现 Quartz 的任务注入依赖的nuget 包: 基本示例如下: using System; using Syst ...
- 用分布式缓存提升ASP.NET Core性能
得益于纯净.轻量化并且跨平台支持的特性,ASP.NET Core作为热门Web应用开发框架,其高性能传输和负载均衡的支持已广受青睐.实际上,10-20台Web服务器还是轻松驾驭的.有了多服务器负载的支 ...
- ASP.NET Core 中文文档 第三章 原理(13)管理应用程序状态
原文:Managing Application State 作者:Steve Smith 翻译:姚阿勇(Dr.Yao) 校对:高嵩 在 ASP.NET Core 中,有多种途径可以对应用程序的状态进行 ...
- ASP.NET Core中的缓存[1]:如何在一个ASP.NET Core应用中使用缓存
.NET Core针对缓存提供了很好的支持 ,我们不仅可以选择将数据缓存在应用进程自身的内存中,还可以采用分布式的形式将缓存数据存储在一个“中心数据库”中.对于分布式缓存,.NET Core提供了针对 ...
- 在 ASP.NET Core 中执行租户服务
在 ASP.NET Core 中执行租户服务 不定时更新翻译系列,此系列更新毫无时间规律,文笔菜翻译菜求各位看官老爷们轻喷,如觉得我翻译有问题请挪步原博客地址 本博文翻译自: http://gunna ...
- [翻译] 如何在 ASP.Net Core 中使用 Consul 来存储配置
[翻译] 如何在 ASP.Net Core 中使用 Consul 来存储配置 原文: USING CONSUL FOR STORING THE CONFIGURATION IN ASP.NET COR ...
- 在 ASP.NET CORE 中使用 SESSION (转载)
Session 是保存用户和 Web 应用的会话状态的一种方法,ASP.NET Core 提供了一个用于管理会话状态的中间件.在本文中我将会简单介绍一下 ASP.NET Core 中的 Session ...
- C# 嵌入dll 动软代码生成器基础使用 系统缓存全解析 .NET开发中的事务处理大比拼 C#之数据类型学习 【基于EF Core的Code First模式的DotNetCore快速开发框架】完成对DB First代码生成的支持 基于EF Core的Code First模式的DotNetCore快速开发框架 【懒人有道】在asp.net core中实现程序集注入
C# 嵌入dll 在很多时候我们在生成C#exe文件时,如果在工程里调用了dll文件时,那么如果不加以处理的话在生成的exe文件运行时需要连同这个dll一起转移,相比于一个单独干净的exe,这种形 ...
随机推荐
- 树莓派4B安装64位桌面版ubuntu20
[准备] 硬件: 电脑.树莓派4B.显示器(hdmi线Micro HDMI转标准HDMI).鼠标.键盘.读卡器.TF卡.网线 软件:ubuntu20(x64桌面版).官方烧录工具Raspberry P ...
- SpringBoot 整合 Sharding-JDBC 分库分表
导读 分库分表的技术有:数据库中间件Mycat(点我直达),当当网开源的Sharding-JDBC:我们公司用的也是sharding-jdbc,自己也搭建一个完整的项目,直接可以拿来用.下面附源码(C ...
- Spring Boot XML文件头
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-/ ...
- Oracle 日期减年数、两日期相减
-- 日期减年数 SELECT add_months(DEF_DATE,12*USEFUL_LIFE) FROM S_USER --两日期相减 SELECT round(sysdate-PEI.STA ...
- Docker自定义网段实现容器间的互访【开发环境中】
我们都知道docker容器之间是互相隔离的,不能互相访问,但如果有些依赖关系的服务要怎么办呢,所以自定义网段实现容器间的互访. Docker 安装好之后默认会创建三个虚拟网卡,可以使用 docker ...
- Serverless无服务应用架构纵横谈2:边缘计算激战正酣
Serverless无服务应用架构纵横谈2 前言 6年前,我写了一篇<Serverless无服务应用架构纵横谈>. 文中说到无论是公有云FaaS还是私有云FaaS,都不是云计算的未来. 因 ...
- python配置国内pypi镜像源操作步骤
使用pip config命令设置默认镜像源,使用国内的源,提高安装速度 操作步骤 临时方式pip install xxx -i https://pypi.tuna.tsinghua.edu.cn/si ...
- Vue 使用Use、prototype自定义全局插件
Vue 使用Use.prototype自定义全局插件 by:授客 QQ:1033553122 开发环境 Win 10 node-v10.15.3-x64.msi 下载地址: https ...
- ddddocr验证码图片识别YYDS
纯数字 数字+字母 python代码: import ddddocr def main(imgpath): # imgpath='E:\yam_0.png' ocr = ddddocr.DdddOcr ...
- 7、Git之Github操作
7.1.注册Github账号 7.1.1.访问官网 Github 官网:https://github.com/ 先访问GitHub的官网首页,点击 sign in (登录),跳转到登录页. 7.1.2 ...