处理异步/等待时的限制/速率限制(429错误)

3
我有以下异步代码,它在我的项目中被多处调用:
public async Task<HttpResponseMessage> MakeRequestAsync(HttpRequestMessage request)
{            
    var client = new HttpClient();
    return await client.SendAsync(request).ConfigureAwait(false);                
}

上述方法被调用的示例:
 var tasks = items.Select(async i =>
            {                    
                var response = await MakeRequestAsync(i.Url);           
                //do something with response    
            });

我正在访问的ZenDesk API允许每分钟约200个请求,超过后我会收到429错误。如果遇到429错误,我需要进行一些Thread.sleep操作,但是由于使用了async/await,可能有很多并行线程在等待处理,我不知道如何使它们都暂停5秒左右,然后再继续执行。
面对这个问题,正确的解决方法是什么?我希望听到快速解决方案和良好设计的解决方案。

2
可能会有很多并行线程中的请求 - 你需要安排和需要的一样多的请求,确保安排足够的请求并且保持愉快。如果真的必须要使用,请使用 Task.Delay - Alexei Levenkov
你是如何调用这个方法的?为什么有并行线程? - Yuval Itzchakov
我的意思是并行,也就是MakeRequestAsync从多个地方进行调用,所以请求在同一时间内被发出。 - Prabhu
@AlexeiLevenkov 我该如何控制请求的数量?我需要一些队列吗?但是我仍然需要处理429错误,因为每分钟200个请求只是一个指导方针,有时可能会有所不同。 - Prabhu
类似的问题有一个很好的答案:https://dev59.com/G2LVa4cB1Zd3GeqP1PXg - Martin Liversage
2个回答

6
我认为最近被标记为重复的帖子并不是重复的,因为另一位SO发帖者不需要基于时间的滑动窗口(或基于时间的限流),那个答案也没有涉及到这种情况。该解决方案仅适用于您想在外发请求上设置硬限制的情况。
无论如何,一个准快速的解决方案是在MakeRequestAsync方法中进行限流。可以像这样实现:
public async Task<HttpResponseMessage> MakeRequestAsync(HttpRequestMessage request)
{            
    //Wait while the limit has been reached. 
    while(!_throttlingHelper.RequestAllowed) 
    {
      await Task.Delay(1000);
    }

    var client = new HttpClient();

    _throttlingHelper.StartRequest();
    var result = await client.SendAsync(request).ConfigureAwait(false);
    _throttlingHelper.EndRequest();

    return result; 
}

ThrottlingHelper类是我现在创建的,所以您可能需要调试一下(即可能无法直接使用)。 它尝试模拟时间戳滑动窗口。

public class ThrottlingHelper : IDisposable
{
    //Holds time stamps for all started requests
    private readonly List<long> _requestsTx;
    private readonly ReaderWriterLockSlim _lock;

    private readonly int _maxLimit;
    private TimeSpan _interval;

    public ThrottlingHelper(int maxLimit, TimeSpan interval)
    {
        _requestsTx = new List<long>();
        _maxLimit = maxLimit;
        _interval = interval;
        _lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
    }

    public bool RequestAllowed
    {
        get
        {
            _lock.EnterReadLock();
            try
            {
                var nowTx = DateTime.Now.Ticks;
                return _requestsTx.Count(tx => nowTx - tx < _interval.Ticks) < _maxLimit;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }
    }

    public void StartRequest()
    {
        _lock.EnterWriteLock();
        try
        {
            _requestsTx.Add(DateTime.Now.Ticks);
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void EndRequest()
    {
        _lock.EnterWriteLock();
        try
        {
            var nowTx = DateTime.Now.Ticks;
            _requestsTx.RemoveAll(tx => nowTx - tx >= _interval.Ticks);
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void Dispose()
    {
        _lock.Dispose();
    }
}

您可以将其用作向服务发出请求的类中的成员,并像这样实例化:

_throttlingHelper = new ThrottlingHelper(200, TimeSpan.FromMinutes(1));

当你使用完成后,请不要忘记处理它。

ThrottlingHelper 的一些文档说明:

  1. 构造函数参数是您想在特定时间间隔内执行的最大请求和时间跨度。因此,200和1分钟表示您希望每分钟不超过200个请求。
  2. RequestAllowed 属性可让您了解是否可以使用当前节流设置进行请求。
  3. StartRequestEndRequest 方法使用当前日期/时间注册/取消注册请求。

编辑/陷阱

正如 @PhilipABarnes 所指出的那样,EndRequest 可能会删除仍在进行中的请求。据我所见,这可能发生在两种情况下:

  1. 时间间隔很小,以至于请求无法及时完成。
  2. 请求实际上需要超过时间间隔才能执行。

建议的解决方案涉及实际上通过 GUID 或类似物将 EndRequest 调用与 StartRequest 调用匹配。


@Prabhu:是的,你可以将这个解决方案与另一个帖子中的解决方案相结合,从而允许一定数量的并发请求。 - Marcel N.
1
@Prabhu:是的,我认为那将是你能得到的最快速的解决方案。如果你想要更好的东西,你需要更无缝地结合这两个方面。 - Marcel N.
@Prabhu:已更新答案。 - Marcel N.
2
requestsTx.RemoveAll(tx => nowTx - tx >= _interval.Ticks) 可能会删除仍在运行的请求吗?是否更好地按照键值进行删除,例如 GUID? - InContext
@MarcelN。我认为我发现了这段代码的一个潜在问题。程序第一次运行时,将允许超过最大允许的请求数。_throttlingHelper.StartRequest() 应该放在 while 循环之上,对吗? - Prabhu
显示剩余13条评论

0
如果在 while 循环中有多个请求等待 RequestAllowed,其中一些请求可能会同时开始。那么使用一个简单的 StartRequestIfAllowed 怎么样?
public class ThrottlingHelper : DisposeBase
{
    //Holds time stamps for all started requests
    private readonly List<long> _requestsTx;
    private readonly Mutex _mutex = new Mutex();

    private readonly int _maxLimit;
    private readonly TimeSpan _interval;

    public ThrottlingHelper(int maxLimit, TimeSpan interval)
    {
        _requestsTx = new List<long>();
        _maxLimit = maxLimit;
        _interval = interval;
    }


    public bool StartRequestIfAllowed
    {
        get
        {
            _mutex.WaitOne();
            try
            {
                var nowTx = DateTime.Now.Ticks;
                if (_requestsTx.Count(tx => nowTx - tx < _interval.Ticks) < _maxLimit)
                {
                    _requestsTx.Add(DateTime.Now.Ticks);
                    return true;
                }
                else
                {
                    return false;
                }
            }
            finally
            {
                _mutex.ReleaseMutex();
            }
        }
    }

    public void EndRequest()
    {
        _mutex.WaitOne();
        try
        {
            var nowTx = DateTime.Now.Ticks;
            _requestsTx.RemoveAll(tx => nowTx - tx >= _interval.Ticks);
        }
        finally
        {
            _mutex.ReleaseMutex();
        }
    }

    protected override void DisposeResources()
    {
        _mutex.Dispose();
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接