当前位置: 首页 > news >正文

利用分布式锁在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.RedisSDK,当然大家可以自行选择合适的库来实现,主要是演示起来方便,因为其他库需要用脚本自行实现可过期的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 thenredis.call("EXPIRE", KEYS[1], ARGV[2])return 1
elsereturn 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,需要注意的是实现的时候尽量使用桶锁,避免使用全局锁

防抖过滤器的实现

接下来我们使用上面定义的IDistributedLockFilter来实现防抖过滤器,我们创建一个基于 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获取具体的分布式锁实现。
  • 使用 controllerNameactionName 以及用户标识构(或其他唯一标识)建锁的键,确保锁的唯一性。
  • 如果获取锁失败,则直接返回错误响应,避免后续操作的执行。
  • 在操作执行完毕后,无论是否成功,都释放锁。

为了更灵活地在不同的锁实现之间进行切换,我们定义了一个枚举 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中实现防抖

前言 在 Web 应用开发过程中&#xff0c;防抖&#xff08;Debounce&#xff09; 是确保同一操作在短时间内不会被重复触发的一种有效手段。常见的场景包括防止用户在短时间内重复提交表单&#xff0c;或者避免多次点击按钮导致后台服务执行多次相同的操作。无论在单机环境中&a…...

Django+Vue3前后端分离学习(二)(重写User类)

一、重写User类&#xff1a; 1、首先导入User类&#xff1a; from django.contrib.auth.models import User 2、然后点在User上&#xff0c;按住ctrl 点进去&#xff0c;发现 User类继承AbstractUser Ctrl点进去AbstractUser&#xff0c;然后将此方法全部复制到自己APP的mo…...

兔英语语法体系——观后笔记

目录 一、视频链接 二、视频前言 三、简单句(Simple Sentences) 1. 可独立完成的动作 2. 有1个动作的承受者 3. 有两个动作承受者 4. 只有一个动作承受者(但需补充) 5. 非 “动作” 6. 总结 四、五大基本句型 五、句子成分 6. 定语 7. 状语 8. 同位语 9. 总结 …...

哈希表如何避免冲突

系列文章&#xff1a; 1. 先导片--Map&Set之二叉搜索树 2. Map&Set之相关概念 3. 哈希表如何避免冲突 目录 1.概念 2. 冲突-概念 3. 冲突-避免 3.1 冲突-避免-哈希函数设计 3.2 冲突-避免-负载因子调节 4. 冲突-解决 4.1 冲突-解决-闭散列 4.1.1 线性探…...

内核模块驱动开发

内核模块开始学习前&#xff0c;一定是最先接触到内核模块三要素(面试)&#xff0c;驱动入口、驱动出口和协议的遵循。 1.内核模块三要素(面试)//修饰模块化驱动的入口函数module_init(demo_init);//修饰模块化驱动的出口函数module_eixt(demo_exit);//遵循GPL开源协议MODULE_…...

Linux 下 alsa 库录音并保存为 WAV 格式

麦克风列表&#xff1a; [jnjn build]$ arecord -l **** List of CAPTURE Hardware Devices **** card 0: AudioPCI [Ensoniq AudioPCI], device 0: ES1371/1 [ES1371 DAC2/ADC]Subdevices: 1/1Subdevice #0: subdevice #0 card 1: Camera [2K USB Camera], device 0: USB Aud…...

使用stripe进行在线支付、退款、订阅、取消订阅功能(uniapp+h5)

stripe官网:Stripe 登录 | 登录 Stripe 管理平台 然后在首页当中打开测试模式,使用测试的公钥跟私钥进行开发 测试卡号 4242 4242 4242 4242 1234 567 在线支付 stripe的在线支付有两种,第一种就是无代码,第二中就是使用api进行自定义,一般来说推荐第二种进行开发 无…...

深度学习中常见的损失函数

关注B站可以观看更多实战教学视频&#xff1a;hallo128的个人空间 深度学习中常见的损失函数 损失函数的作用 损失函数是衡量神经网络输出与真实标签之间差距的指标。在训练过程中&#xff0c;神经网络的目标是最小化损失函数的值。常见的损失函数包括均方误差&#xff08;MS…...

认识Linux及Linux的环境搭建

目录 1、什么是Linux2、Linux环境搭建2.1 下载安装 Xshell2.2 下载安装 VMware Workstation Pro2.3 选择适合自己系统 1、什么是Linux Linux&#xff0c;一般指GNU/Linux&#xff08;单独的Linux内核并不可直接使用&#xff0c;一般搭配GNU套件&#xff0c;故得此称呼&#xff…...

Java之线程篇三

​​​​​​​ 目录 线程状态 观察线程的所有状态 线程状态及其描述 线程状态转换 代码示例1 代码示例2 线程安全 概念 线程不安全的代码示例 线程不安全的原因 线程安全的代码示例-加锁 synchronized关键字 synchronized的特性 小结 形成死锁的四个必要条件 …...

Bootstrap动态设置表格title项

页面searchType <form id"formId"><div class"select-list"><ul><li><select name"searchType" id"searchType"><option value"1">按各节点统计</option><option value"…...

Arrays.sort()方法在Java中的使用:理论与实践

目录 一.概述 二.实现方式 三.具体介绍 1.基本数据类型数组 2.对象数组 1)使对象实现Comparable接口 2)为对象再专门实现一个比较器类 四.进阶技巧 1.基础类型数组实现自定义比较 2.如何进行逆序排序 3.lambda表达式实现比较器类 4.List的排序方法Collection.sort()…...

用AI写论文,千万不要这样用ChatGPT生成参考文献References!!

ChatGPT作为一种先进的语言大模型&#xff0c;被广泛用于生成文本&#xff0c;虽然用ChatGPT辅助论文写作已是大势所趋&#xff0c;但是&#xff0c;用于生成参考文献References的部分还是要谨慎对待。 在学术写作中&#xff0c;参考文献References扮演着至关重要的角色&#…...

Debian 12如何关闭防火墙

在Debian 12中&#xff0c;默认的防火墙管理工具是ufw&#xff08;Uncomplicated Firewall&#xff09;。您可以使用以下命令来关闭防火墙&#xff1a; 关闭防火墙&#xff1a; sudo ufw disable查看防火墙状态&#xff1a; sudo ufw status如果需要重新开启防火墙&#xff1a;…...

windows C++-并行编程-PPL任务并行(二)

延续任务 在异步编程中&#xff0c;一个异步操作在完成时调用另一个操作并将数据传递到其中的情况非常常见。 传统上&#xff0c;这使用回调方法来完成。 在并发运行时中&#xff0c;延续任务提供了同样的功能。 延续任务(也简称为“延续”)是一个异步任务&#xff0c;由另一个…...

快速了解 servlet(SpringMVC 的底层)

Servlet 是 Java EE&#xff08;现 Jakarta EE&#xff09;中用于处理 Web 请求的核心组件。它在 Web 应用程序的服务器端运行&#xff0c;负责接收和处理客户端&#xff08;如浏览器&#xff09;的请求&#xff0c;并生成响应。 尽管现代Web开发更多采用SpringMVC等框架&…...

QT中tr的作用是什么

在Qt框架中&#xff0c;tr() 函数是一个非常重要的宏&#xff0c;它用于国际化和本地化&#xff08;i18n和l10n&#xff09;支持。tr() 函数使得Qt应用程序能够根据不同的语言环境&#xff08;locale&#xff09;显示相应的翻译文本&#xff0c;从而支持多种语言。 具体来说&a…...

OpenCV结构分析与形状描述符(7)计算轮廓的面积的函数contourArea()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 计算轮廓的面积。 该函数计算轮廓的面积。与 moments 类似&#xff0c;面积是使用格林公式计算的。因此&#xff0c;返回的面积与你使用 drawCo…...

内网环境使用Docker部署Qwen2模型-vLLM篇

在此之前&#xff0c;我们已成功利用Docker与Ollama框架&#xff0c;在内网环境中部署了Qwen2模型。下面我们再来看一下使用Docker与vLLM框架部署Qwen2模型。 准备vLLM镜像 在一台具备网络环境的机器上执行以下命令&#xff0c;拉取vLLM的镜像&#xff1a; # 官方镜像 docke…...

Rust的常数、作用域与所有权

【图书介绍】《Rust编程与项目实战》-CSDN博客 《Rust编程与项目实战》(朱文伟&#xff0c;李建英)【摘要 书评 试读】- 京东图书 (jd.com) Rust到底值不值得学&#xff0c;之一 -CSDN博客 Rust到底值不值得学&#xff0c;之二-CSDN博客 Rust的数据类型-CSDN博客 3.7 常…...

synchronized 学习

学习源&#xff1a; https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖&#xff0c;也要考虑性能问题&#xff08;场景&#xff09; 2.常见面试问题&#xff1a; sync出…...

多场景 OkHttpClient 管理器 - Android 网络通信解决方案

下面是一个完整的 Android 实现&#xff0c;展示如何创建和管理多个 OkHttpClient 实例&#xff0c;分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...

关于nvm与node.js

1 安装nvm 安装过程中手动修改 nvm的安装路径&#xff0c; 以及修改 通过nvm安装node后正在使用的node的存放目录【这句话可能难以理解&#xff0c;但接着往下看你就了然了】 2 修改nvm中settings.txt文件配置 nvm安装成功后&#xff0c;通常在该文件中会出现以下配置&…...

今日学习:Spring线程池|并发修改异常|链路丢失|登录续期|VIP过期策略|数值类缓存

文章目录 优雅版线程池ThreadPoolTaskExecutor和ThreadPoolTaskExecutor的装饰器并发修改异常并发修改异常简介实现机制设计原因及意义 使用线程池造成的链路丢失问题线程池导致的链路丢失问题发生原因 常见解决方法更好的解决方法设计精妙之处 登录续期登录续期常见实现方式特…...

管理学院权限管理系统开发总结

文章目录 &#x1f393; 管理学院权限管理系统开发总结 - 现代化Web应用实践之路&#x1f4dd; 项目概述&#x1f3d7;️ 技术架构设计后端技术栈前端技术栈 &#x1f4a1; 核心功能特性1. 用户管理模块2. 权限管理系统3. 统计报表功能4. 用户体验优化 &#x1f5c4;️ 数据库设…...

PostgreSQL——环境搭建

一、Linux # 安装 PostgreSQL 15 仓库 sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-$(rpm -E %{rhel})-x86_64/pgdg-redhat-repo-latest.noarch.rpm# 安装之前先确认是否已经存在PostgreSQL rpm -qa | grep postgres# 如果存在&#xff0…...

五子棋测试用例

一.项目背景 1.1 项目简介 传统棋类文化的推广 五子棋是一种古老的棋类游戏&#xff0c;有着深厚的文化底蕴。通过将五子棋制作成网页游戏&#xff0c;可以让更多的人了解和接触到这一传统棋类文化。无论是国内还是国外的玩家&#xff0c;都可以通过网页五子棋感受到东方棋类…...

二维FDTD算法仿真

二维FDTD算法仿真&#xff0c;并带完全匹配层&#xff0c;输入波形为高斯波、平面波 FDTD_二维/FDTD.zip , 6075 FDTD_二维/FDTD_31.m , 1029 FDTD_二维/FDTD_32.m , 2806 FDTD_二维/FDTD_33.m , 3782 FDTD_二维/FDTD_34.m , 4182 FDTD_二维/FDTD_35.m , 4793...

Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解

文章目录 一、开启慢查询日志&#xff0c;定位耗时SQL1.1 查看慢查询日志是否开启1.2 临时开启慢查询日志1.3 永久开启慢查询日志1.4 分析慢查询日志 二、使用EXPLAIN分析SQL执行计划2.1 EXPLAIN的基本使用2.2 EXPLAIN分析案例2.3 根据EXPLAIN结果优化SQL 三、使用SHOW PROFILE…...

相关类相关的可视化图像总结

目录 一、散点图 二、气泡图 三、相关图 四、热力图 五、二维密度图 六、多模态二维密度图 七、雷达图 八、桑基图 九、总结 一、散点图 特点 通过点的位置展示两个连续变量之间的关系&#xff0c;可直观判断线性相关、非线性相关或无相关关系&#xff0c;点的分布密…...