Laravel-Throttle

思考并回答以下问题:

使用

可以使用时间或并发(Laravel-Funnel)来控制队列任务。该功能特性在队列任务与有频率限制的API交互时很有帮助。

通过throttle方法,你可以限定给定类型任务每60秒只运行10次。如果不能获取锁,需要将任务释放回队列以便可以再次执行:

1
2
3
4
5
6
7
8
9
Redis::throttle('key')->allow(10)->every(60)->then(function () {
// Job logic...
// 1分钟点击了100下,但是1分钟只有10条数据写入文件
// file_put_contents('1.txt', date('Y-m-d H:i:s').PHP_EOL, FILE_APPEND);
}, function () {
// Could not obtain lock...

return $this->release(10);
});

注:在上面的例子中,key可以是任意可以唯一标识你想要限定访问频率的任务类型的字符串。举个例子,这个键可以基于任务类名和操作Eloquent模型的ID进行构建。

源码

Illuminate\Redis\Limiters\DurationLimiter.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
<?php

namespace Illuminate\Redis\Limiters;

use Illuminate\Contracts\Redis\LimiterTimeoutException;

class DurationLimiter
{
/**
* The Redis factory implementation.
*
* @var \Illuminate\Redis\Connections\Connection
*/
private $redis;

/**
* The unique name of the lock.
*
* @var string
*/
private $name;

/**
* The allowed number of concurrent tasks.
*
* @var int
*/
private $maxLocks;

/**
* The number of seconds a slot should be maintained.
*
* @var int
*/
private $decay;

/**
* The timestamp of the end of the current duration.
*
* @var int
*/
public $decaysAt;

/**
* The number of remaining slots.
*
* @var int
*/
public $remaining;

/**
* Create a new duration limiter instance.
*
* @param \Illuminate\Redis\Connections\Connection $redis
* @param string $name
* @param int $maxLocks
* @param int $decay
* @return void
*/
public function __construct($redis, $name, $maxLocks, $decay)
{
$this->name = $name;
$this->decay = $decay;
$this->redis = $redis;
$this->maxLocks = $maxLocks;
}

/**
* Attempt to acquire the lock for the given number of seconds.
*
* @param int $timeout
* @param callable|null $callback
* @return mixed
*
* @throws \Illuminate\Contracts\Redis\LimiterTimeoutException
*/
public function block($timeout, $callback = null)
{
$starting = time();

while (! $this->acquire()) {
if (time() - $timeout >= $starting) {
throw new LimiterTimeoutException;
}

usleep(750 * 1000);
}

if (is_callable($callback)) {
return $callback();
}

return true;
}

/**
* Attempt to acquire the lock.
*
* @return bool
*/
public function acquire()
{
$results = $this->redis->eval(
$this->luaScript(), 1, $this->name, microtime(true), time(), $this->decay, $this->maxLocks
);

$this->decaysAt = $results[1];

$this->remaining = max(0, $results[2]);

return (bool) $results[0];
}

/**
* Get the Lua script for acquiring a lock.
*
* KEYS[1] - The limiter name
* ARGV[1] - Current time in microseconds
* ARGV[2] - Current time in seconds
* ARGV[3] - Duration of the bucket
* ARGV[4] - Allowed number of tasks
*
* @return string
*/
protected function luaScript()
{
return <<<'LUA'
local function reset()
redis.call('HMSET', KEYS[1], 'start', ARGV[2], 'end', ARGV[2] + ARGV[3], 'count', 1)
return redis.call('EXPIRE', KEYS[1], ARGV[3] * 2)
end

if redis.call('EXISTS', KEYS[1]) == 0 then
return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1}
end

if ARGV[1] >= redis.call('HGET', KEYS[1], 'start') and ARGV[1] <= redis.call('HGET', KEYS[1], 'end') then
return {
tonumber(redis.call('HINCRBY', KEYS[1], 'count', 1)) <= tonumber(ARGV[4]),
redis.call('HGET', KEYS[1], 'end'),
ARGV[4] - redis.call('HGET', KEYS[1], 'count')
}
end

return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1}
LUA;
}
}

Illuminate\Redis\Limiters\DurationLimiterBuilder.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
<?php

namespace Illuminate\Redis\Limiters;

use Illuminate\Contracts\Redis\LimiterTimeoutException;
use Illuminate\Support\InteractsWithTime;

class DurationLimiterBuilder
{
use InteractsWithTime;

/**
* The Redis connection.
*
* @var \Illuminate\Redis\Connections\Connection
*/
public $connection;

/**
* The name of the lock.
*
* @var string
*/
public $name;

/**
* The maximum number of locks that can obtained per time window.
*
* @var int
*/
public $maxLocks;

/**
* The amount of time the lock window is maintained.
*
* @var int
*/
public $decay;

/**
* The amount of time to block until a lock is available.
*
* @var int
*/
public $timeout = 3;

/**
* Create a new builder instance.
*
* @param \Illuminate\Redis\Connections\Connection $connection
* @param string $name
* @return void
*/
public function __construct($connection, $name)
{
$this->name = $name;
$this->connection = $connection;
}

/**
* Set the maximum number of locks that can obtained per time window.
*
* @param int $maxLocks
* @return $this
*/
public function allow($maxLocks)
{
$this->maxLocks = $maxLocks;

return $this;
}

/**
* Set the amount of time the lock window is maintained.
*
* @param int $decay
* @return $this
*/
public function every($decay)
{
$this->decay = $this->secondsUntil($decay);

return $this;
}

/**
* Set the amount of time to block until a lock is available.
*
* @param int $timeout
* @return $this
*/
public function block($timeout)
{
$this->timeout = $timeout;

return $this;
}

/**
* Execute the given callback if a lock is obtained, otherwise call the failure callback.
*
* @param callable $callback
* @param callable|null $failure
* @return mixed
*
* @throws \Illuminate\Contracts\Redis\LimiterTimeoutException
*/
public function then(callable $callback, callable $failure = null)
{
try {
return (new DurationLimiter(
$this->connection, $this->name, $this->maxLocks, $this->decay
))->block($this->timeout, $callback);
} catch (LimiterTimeoutException $e) {
if ($failure) {
return $failure($e);
}

throw $e;
}
}
}

Illuminate\Redis\Connections\Connection.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php

namespace Illuminate\Redis\Connections;

use Closure;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Redis\Events\CommandExecuted;
use Illuminate\Redis\Limiters\ConcurrencyLimiterBuilder;
use Illuminate\Redis\Limiters\DurationLimiterBuilder;
use Illuminate\Support\Traits\Macroable;

abstract class Connection
{
use Macroable {
__call as macroCall;
}

/**
* Throttle a callback for a maximum number of executions over a given duration.
*
* @param string $name
* @return \Illuminate\Redis\Limiters\DurationLimiterBuilder
*/
public function throttle($name)
{
return new DurationLimiterBuilder($this, $name);
}
}
0%