首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

利用分布式锁在ASP.NET Core中实现防抖

编程知识
2024年09月04日 07:31

前言

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 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,需要注意的是实现的时候尽量使用桶锁,避免使用全局锁

防抖过滤器的实现

接下来我们使用上面定义的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进行重复操作。

这种方法不仅提高了应用的稳定性,也增强了用户体验,避免了短时间内重复操作的问题。希望本文对大家有所帮助。如果有任何问题或进一步讨论的需求,欢迎在评论区留言。

👇欢迎扫码关注我的公众号👇
From:https://www.cnblogs.com/wucy/p/18394437/aspnetcore-distributed-lock-debounce
本文地址: http://www.shuzixingkong.net/article/1714
0评论
提交 加载更多评论
其他文章 【题目全解】ACGO挑战赛#8
前言:本次挑战赛的难度相较于前面几期有所提升,主要还是因为集训的关系,出题组的成员们没有充裕的时间想原创题目(so,只能原模原样搬运某一场 ABC 的考试了。)Anyway,AK 了就行。 备注:由于 Python 的常数过大,本题解暂不同步更新 Python 版本的题解。 第一题 - Inters
论文速读纪录 - 202408
特别鸣谢kimi,以下论文均在kimi辅助下阅读。 目录 RMIB: Representation Matching Information Bottleneck for Matching Text Representations AttentionRank: Unsupervised keyphr
论文速读纪录 - 202408 论文速读纪录 - 202408 论文速读纪录 - 202408
多线程、任务、异步的区别
Task和Thread的区别 这是一个高频,深刻的问题,无论去哪都逃不过被询问这个问题。Task是基于Thread的,这是众所周知的。但是Task和Thread的联系如此简单和纯粹确实我没想到的。甚至只需要几十行代码就能呈现其原理。一个简单的模拟实例说明Task及其调度问题,这真是一篇好文章。 任务
多线程、任务、异步的区别 多线程、任务、异步的区别
英语疑问句倒装特征以及从句的正常语序
上一篇介绍了英文中的五大基本句型,在进一步介绍句子如何在五大句型之上如何各种添枝加叶变得复杂之前,我们先来搞懂英语疑问句倒装特征以及从句的正常语序。 这是至关重要的一件事,但其实太多初学者都给忽略了,在懵圈中苦学了很久,结果在实际应用时产生了各种混乱,事倍而功半。 本文想通过举例的方式,来试图讲清这
英语疑问句倒装特征以及从句的正常语序
借助表格技术提升智能审计云平台应用体验
背景说明: 2009年,立信加入全球第五大国际会计网络——BDO国际。BDO 全球各地成员所均使用统一的审计方式,在完全遵守国际审计准则的原则同时,也会应不同地区要求提供附加指引,确保在全球提供一致的服务水平。如今,立信的审计过程及档案记录均已实现电子化。然而,审计工作并非一成不变,为了给客户提供最
借助表格技术提升智能审计云平台应用体验 借助表格技术提升智能审计云平台应用体验 借助表格技术提升智能审计云平台应用体验
图穷匕见-所有反DDD模式都是垃圾
本文书接上回《主观与客观,破除DDD凭经验魔咒》,关注公众号(老肖想当外语大佬)获取信息: 最新文章更新; DDD框架源码(.NET、Java双平台); 加群畅聊,建模分析、技术实现交流; 视频和直播在B站。 开个玩笑 “我不是针对这一个问题,我是说所有的反DDD模式都是垃圾”,作为教练,在团队中我
图穷匕见-所有反DDD模式都是垃圾 图穷匕见-所有反DDD模式都是垃圾 图穷匕见-所有反DDD模式都是垃圾
架构师备考的一些思考
前言 之前的python-pytorch的系列文章还没有写完,只是写到卷积神经网络。因为我报名成功了系统架构师的考试,所以决定先备考,等考完再继续写。 虽然架构师证书不能证明技术水平,但在现实生活中的某些情况下是有意义的。考试虽然无聊,但有些考题还是蛮有意思的。 思考 看了几套架构师的考题,发现个有
架构师备考的一些思考 架构师备考的一些思考
1p-frac:已开源,仅用单张分形图片即可媲美ImageNet的预训练效果 | ECCV 2024
分形几何是一个数学分支,主要应用于作图方面。一般来说,分形经过无数次递归迭代后的结果。比如取一条线段,抹去中间的三分之一,会得到长度是原三分之一长的两条线段,中间隔着相同长度的间隙。然后重复这个动作,直到所有的线段都被抹掉,就将会得到被以固定模式出现的间隙隔开的无限多的点,这就是康托尔集合。 目前有
1p-frac:已开源,仅用单张分形图片即可媲美ImageNet的预训练效果 | ECCV 2024 1p-frac:已开源,仅用单张分形图片即可媲美ImageNet的预训练效果 | ECCV 2024 1p-frac:已开源,仅用单张分形图片即可媲美ImageNet的预训练效果 | ECCV 2024