• ASP.NET限流器的简单实现


    一、滑动时间窗口

    我为RateLimiter定义了如下这个简单的IRateLimiter接口,唯一的无参方法TryAcquire利用返回的布尔值确定当前是否超出设定的速率限制。我只提供的两种基于时间窗口的实现,如下所示的基于“滑动时间窗口”的实现类型SliddingWindowRateLimiter,我们在构造的时候指定时间窗口和阈值。SliddingWindowRateLimiter采用一种“讨巧”的实现,它直接利用了BoundedChannel对象,我们将指定的阈值作为它的最大容量。

    1. public interface IRateLimiter
    2. {
    3. bool TryAcquire();
    4. }
    5. public sealed class SliddingWindowRateLimiter: IRateLimiter
    6. {
    7. private readonly TimeSpan _window;
    8. private readonly ChannelReader _reader;
    9. private readonly ChannelWriter _writer;
    10. public SliddingWindowRateLimiter(TimeSpan window, int permit)
    11. {
    12. _window = window;
    13. var options = new BoundedChannelOptions (permit)
    14. {
    15. FullMode = BoundedChannelFullMode.Wait,
    16. SingleReader = false,
    17. SingleWriter = true
    18. };
    19. var channel = Channel.CreateBounded(options);
    20. _reader = channel.Reader;
    21. _writer = channel.Writer;
    22. Task.Factory.StartNew(Trim,TaskCreationOptions.LongRunning);
    23. }
    24. public bool TryAcquire() => _writer.TryWrite(DateTimeOffset.UtcNow);
    25. private void Trim()
    26. {
    27. if (!_reader.TryPeek(out var timestamp))
    28. {
    29. Task.Delay(_window).Wait();
    30. Trim();
    31. }
    32. else
    33. {
    34. var delay = _window - (DateTimeOffset.UtcNow - timestamp);
    35. if (delay > TimeSpan.Zero)
    36. {
    37. Task.Delay(delay).Wait();
    38. Trim();
    39. }
    40. else
    41. {
    42. var valueTask = _reader.ReadAsync();
    43. if (!valueTask.IsCompleted) _ = valueTask.Result;
    44. Trim();
    45. }
    46. }
    47. }
    48. }

    在实现的TryAcquire方法中,我们试着将当前时间戳写入这个Channel,并将写入的结果(成功或者失败)作为返回值。为了让Channel中只包含指定时间窗口的时间戳,我们利用一个LongRuning的Task执行Trim方法对过期的时间戳进行“裁剪”。Trim会调用ChannelReader的TRyPeek方法,如果返回False,意味着Channel为空,此时会等待一段窗口时间再进行“裁剪”。如果提取出来时间戳在Now-Window与当前时间之间,意味着Channel里面的时间戳均在设定的窗口内,此时同样需要等待,等待时间为Window - (Now - Timestamp);只有在提取的时间超出窗口范围,我们才需要将其从Channel中移除。

    1. var limiter = new SliddingWindowRateLimiter(TimeSpan.FromSeconds(2),2);
    2. var index = 0;
    3. await Task.WhenAll( Enumerable.Range(1, 100).Select(_ => Task.Run(() => {
    4. while (true)
    5. {
    6. if (limiter.TryAcquire())
    7. {
    8. Console.WriteLine($"[{DateTimeOffset.Now}]{Interlocked.Increment(ref index)}");
    9. }
    10. }
    11. })));

    我们在上面的演示程序中使用这个SliddingWindowRateLimiter,设定的限速规则为 2/2s。我们创建了100个Task并发地调用这个SliddingWindowRateLimiter,并将它返回True时的时间戳显示出来,具体输出如下所示。

    image

    二、固定时间窗口

    如下这个FixedWindowRateLimiter类型是针对“固定窗口”的实现,字段_windowTicks和_permit同样表示时间窗口的时长(这里我们使用Int64类型的Ticks属性)和阈值。 _nextWindowStartTimeTicks表示下一次固定窗口的起始时间,这个需要动态调整,为了确保只有一个线程能够修改它,我们定义了_windowReseting这个“信号量”。_count是一个计数器,我们使用它确定是否“超速”。

    1. public sealed class FixedWindowRateLimiter : IRateLimiter
    2. {
    3. private readonly long _windowTicks;
    4. private readonly int _permit;
    5. private long _nextWindowStartTimeTicks;
    6. private volatile int _count = 0;
    7. public FixedWindowRateLimiter(TimeSpan window, int permit)
    8. {
    9. _windowTicks = window.Ticks;
    10. _permit = permit;
    11. _nextWindowStartTimeTicks = DateTimeOffset.UtcNow.Add(window).Ticks;
    12. }
    13. public bool TryAcquire()
    14. {
    15. // 超出时间窗口,重置计数器,并调整下一个时间窗口的开始时间
    16. var now = DateTimeOffset.UtcNow.Ticks;
    17. var nextWindowStartTimeTicks = nextWindowStartTimeTicks;
    18. if (now >= nextWindowStartTimeTicks && Interlocked.CompareExchange(ref _nextWindowStartTimeTicks, now + _windowTicks, nextWindowStartTimeTicks) == nextWindowStartTimeTicks)
    19. {
    20. Interlocked.Exchange(ref _count, 1);
    21. return true;
    22. }
    23. return _count < _permit && Interlocked.Increment(ref _count) <= _permit;
    24. }
    25. }

    在实现的TryAcquire方法中,我们先确定当前时间是否超过了设定的“下一个窗口开始时间”,如果是则调用Interlocked.CompareExchange方法修改__nextWindowStartTimeTicks字段。成功修改__nextWindowStartTimeTicks的线程会调整窗口开始时间,并重置计数器_count为1,并返回True。如果计数器大于等于设定阈值,方法返回False。否则我们让计数器+1,如果该值<=阈值,返回True,否则返回False。

    1. IRateLimiter limiter = new FixedWindowRateLimiter(window: TimeSpan.FromSeconds(2), permit: 2);
    2. var index = 0;
    3. await Task.WhenAll( Enumerable.Range(1, 100).Select(_ => Task.Run(() => {
    4. while (true)
    5. {
    6. if (limiter.TryAcquire())
    7. {
    8. Console.WriteLine($"[{DateTimeOffset.Now}]{Interlocked.Increment(ref index)}");
    9. }
    10. }
    11. })));

    将FixedWindowRateLimiter应用到上面的演示程序,依然能得到我们希望的输出结果。

    image

  • 相关阅读:
    MySQL内部存储代码常用实现方式
    找不到模块 “path“ 或其相对应的类型声明
    在Jupyter-lab中使用RDKit画分子2D图
    mysql安装与配置及四大引擎和数据类型、建表以及约束、增删改查、常用函数、聚合函数以及合并
    Vue3-admin-template 框架实现表单身份证获取到 出生年月、性别
    MYSQL一站式学习,看完即学完
    MySQL 备份策略详解:完全备份、增量备份和差异备份(InsCode AI 创作助手)
    Springboot整合Mybatis-Plus
    2022-07-21 第四组 java之继承
    SaaSBase:什么是有道云笔记?
  • 原文地址:https://blog.csdn.net/weixin_55305220/article/details/134425654