支持Laravel.io的开发继续 →

详细解析Laravel Redis限流:教程

2024年4月17日 阅读时间:8分钟

Redis Throttle是Redis facade提供的一项极佳功能。这是一种方便的方法,用来限制某些操作可以执行的速度。

Redis::throttle("rate-limiter-{$action->id}") // Rate limiter key
    ->allow(100) // No. executions permitted
    ->every(10) // Time range in seconds
    ->then(function () use ($callback, $failure) {
	
        // Lock acquired
        // Your code here...
        $action->run();
		
    }, function () {
	
        // Lock not acquired.
		
    });

Laravel Redis Throttle的工作方式

throttle() 方法允许你通过以下过程

  • 确定密钥throttle() 方法的第一个参数是一个用作速率限制参考的字符串。你应该为想要控制的每个操作构建键,以区分不同的速率限制。
  • 获取锁:Laravel首先尝试使用Redis获取锁。这个锁可以防止在多个请求同时处理时出现的竞争条件。
  • 跟踪锁:一旦确定了键,Laravel使用Redis存储有关锁获取请求的信息,例如最后请求的时间戳和在一定时间内的请求数量。
  • 限制逻辑then() 方法会评估配置的速率限制信息,以确定回调是否可以执行。速率限制包括允许的请求最大数量以及特定的时间窗口(例如每分钟最多60个请求)。
  • 实施限制:如果请求超过了速率限制,Laravel将阻止进一步的进程,并运行第二个回调,以允许开发人员管理超出速率限制的情况。

什么是Redis中的原子锁

Redis中的原子锁是一种机制,允许多个客户端协调和同步对共享资源的访问。它确保一次只有一个客户端可以持有锁,从而防止并发访问并保持数据完整性。

在Redis中,原子锁通常使用SETNX(如果不存在则设置)命令实现。SETNX命令仅在键不存在时才在Redis中设置键。如果键成功设置,则表示客户端获得了锁。如果键已存在,则表示另一个客户端持有锁,当前客户端无法获得。

Laravel Redis限流微调

除了基本参数外,throttle()方法提供了一个流畅的接口来访问其他参数,允许你根据特定需求调整限流策略。

我将本章的重点放在两个参数:超时和休眠。为了更好地理解这些参数如何改变限流的特性以及可能遭遇的副作用,我们需要分析Illuminate\Redis\Limiters\DurationLimiter类的内部实现。

/**
 * This is the method that launch the throttling process.
 * It uses an instance of \Illuminate\Redis\Limiters\DurationbLimiter class.
 * 
 * Redis::throttle($key)->then(callback, callback);
 */
public function then(callable $callback, callable $failure = null)
{
    try {
        return (new DurationLimiter(
            $this->connection, $this->name, $this->maxLocks, $this->decay
        ))->block($this->timeout, $callback, $this->sleep);
    } catch (LimiterTimeoutException $e) {
        if ($failure) {
            return $failure($e);
        }
        throw $e;
    }
}

/**
 * This is the real implementation of the DurationLimiter::block method.
 */
public function block($timeout, $callback = null, $sleep = 750)
{
    $starting = time();
    while (! $this->acquire()) {
        if (time() - $timeout >= $starting) {
            throw new LimiterTimeoutException;
        }
        Sleep::usleep($sleep * 1000);
    }
    if (is_callable($callback)) {
        return $callback();
    }
    return true;
}

block()方法的目的是从Redis中获取锁以执行限流或速率限制。以下是其工作原理。

Laravel如何使用Redis原子锁进行限流或速率限制

当客户端想要执行需要限流的操作时,Laravel尝试使用acquire()方法获取锁。

Laravel Redis限流使用SETNX命令尝试在Redis中设置锁键。如果键成功设置,则表示客户端获得了锁。

如果锁被获取,Laravel将继续执行限流的操作。

操作完成后,Laravel通过使用DEL命令从Redis中删除锁键来释放锁。

如果锁没有被获取(即键已存在),意味着另一个客户端持有锁。在这种情况下,Laravel的block()方法将进入一个循环,在其中重复尝试获取锁,直到达到指定的超时时间。

在循环中,Laravel使用Sleep::usleep()函数在每次尝试获取锁之间引入延迟。这个延迟帮助防止过度使用CPU,并允许其他客户端获取锁。

如果在指定的超时时间内成功获取了锁,Laravel将继续执行限流的操作。如果超时并且锁还没有获取,Laravel将抛出LimiterTimeoutException异常来表示锁无法获取。

现在,让我们关注一下$timeout$sleep参数的效应或潜在副作用。

超时参数

$timeout参数决定了方法在放弃并允许限流方法调用失败回调之前将等待获取锁的最大时间。

如果将$timeout设置为一个较低的值,当锁未获取时,该方法会更快放弃,可能导致更频繁的超时和异常。

如果将$timeout设置为一个较高的值,该方法会更长时间地等待锁,增加获取锁的机会,但也可能导致更长的阻塞时间。

休眠参数

$sleep参数决定了方法在每次尝试获取锁之间暂停的时间。

它有助于引入延迟并防止通过不断尝试紧密循环获取锁而导致过度使用CPU。

较小的$sleep值会导致频繁尝试获取锁,这可能增加方法的可响应性,但也可能增加CPU使用。

较大的$sleep值会在尝试之间引入更长的延迟,减少CPU使用,但可能增加获取锁所需的总时间。

选择$timeout$sleep的值取决于应用程序的具体要求和预期的负载。

Laravel Redis Throttle的查看器用例

作为Inspector的CTO,我有机会深入了解这个特性,因为我们必须处理大量的流量,而且这个微调对我们的基础设施利用模式和其产生的成本影响很大。

我们为每个Inspector账户都设置了速率限制,以防止某个账户的请求过多而淹没数据库,并减缓其他账户的数据导入。

我们最常用的限制中,每秒允许200条消息。它看起来应该是这样的

Redis::throttle("rate-limiter-{$organization->id}")
    ->allow(200) // No. executions permitted
    ->every(1) // One second time window
    ->then(function () use ($callback, $failure) {
	
        // Lock acquired
        // Your code here...
        $this->process();
		
    }, function () {
	
        // Lock not acquired.
        $this->release($this->attempts());
	
    });

如果一个账户达到这个速率限制,系统会将数据重新计划在稍后进行,延迟与尝试导入的数量成比例。这是等待较空闲的时间窗口的一种方式。没有数据丢失,只是在流量高峰时会有轻微的延迟。

使用默认的$timeout$sleep参数会导致从队列中消耗的工作数量太慢,有时工作会积压在队列中。

我认为问题是,我决定设置一个非常紧凑的时间窗口(一秒),并且有大量 incoming 请求(每分钟10,000条)。

解决方案是将$timeout参数设置为0

Redis::throttle("rate-limiter-{$organization->id}")
    ->allow(200) // No. executions permitted
    ->every(1) // One second time window
    ->block(0) // Set the timeout to zero
    ->then(function () use ($callback, $failure) {
	
        // Lock acquired
        // Your code here...
        $this->process();
		
    }, function () {
	
        // Lock not acquired.
        $this->release($this->attempts());
		
    });

$timeout为0时,while循环不会等待多次获取锁。它只是从队列中取出消息,如果锁立即可用,则调用$failure回调。

失败回调将作业重新计划到队列中并延迟,这样工作人员可以立即从队列中取出另一个消息,而无需等待多次获取锁。这就是为什么工作人员处理的工作不够多的原因。在有超时和睡眠的情况下,工作人员的过程变得忙碌,只是在等待锁。

自动修复错误

在交付周期后发生错误时,Inspector不仅会通过通知向您发出警报,还会在您的Git仓库中创建一个拉取请求来自动修复错误。

现在,您可以在几分钟内发布错误修复,而无需人为干预。有关更多信息,请参阅文档。

您是否在公司负责应用开发?免费使用Inspector监控您的软件产品。您可以在客户发现问题时自动修复代码中的错误和瓶颈。

创建您的帐户或在学习网站上了解更多信息:https://inspector.dev

最后更新:3个月前。

由driesvints喜欢这篇文章

1
喜欢这篇文章?让作者知道并向他们鼓掌!

你可能喜欢其他文章

March 11th 2024

如何使用 Larastan 将你的 Laravel 应用从 0 到 9 进行优化

在 Laravel 应用执行前发现错误是可能的,多亏了 Larastan,它...

阅读文章
July 19th 2024

不使用 traits 标准化 API 响应

我发现大多数用于 API 响应的库都使用 traits 实现,并且...

阅读文章
July 17th 2024

在 Laravel 项目中使用 Discord 通知收集反馈

如何在 Laravel 项目中创建反馈模块,并在收到消息时收到 Discord 通知...

阅读文章

我们感谢这些 出色的公司 对我们的支持

在这里放置你的 logo?

Laravel.io

Laravel 问题解决方案、知识共享和社区建设的门户。

社交媒体

社区

© 2024 Laravel.io - 版权所有。