Laravel 13 与 Redis 的深度集成提供了强大的缓存和数据处理能力,本文介绍 Redis 的高级应用技术。

Redis 配置

多连接配置

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
<?php

return [
'default' => env('REDIS_CLIENT', 'phpredis'),

'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', 'laravel_'),
],

'connections' => [
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
'timeout' => 5,
'read_timeout' => 5,
],

'cache' => [
'url' => env('REDIS_CACHE_URL'),
'host' => env('REDIS_CACHE_HOST', '127.0.0.1'),
'password' => env('REDIS_CACHE_PASSWORD', null),
'port' => env('REDIS_CACHE_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
],

'queue' => [
'url' => env('REDIS_QUEUE_URL'),
'host' => env('REDIS_QUEUE_HOST', '127.0.0.1'),
'password' => env('REDIS_QUEUE_PASSWORD', null),
'port' => env('REDIS_QUEUE_PORT', '6379'),
'database' => env('REDIS_QUEUE_DB', '2'),
],

'session' => [
'url' => env('REDIS_SESSION_URL'),
'host' => env('REDIS_SESSION_HOST', '127.0.0.1'),
'password' => env('REDIS_SESSION_PASSWORD', null),
'port' => env('REDIS_SESSION_PORT', '6379'),
'database' => env('REDIS_SESSION_DB', '3'),
],
],
];

Redis 服务封装

高级 Redis 服务

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
<?php

namespace App\Services\Redis;

use Illuminate\Support\Facades\Redis;

class RedisService
{
protected string $connection;

public function __construct(string $connection = 'default')
{
$this->connection = $connection;
}

protected function client()
{
return Redis::connection($this->connection);
}

public function set(string $key, mixed $value, int $ttl = null): bool
{
$value = is_array($value) ? json_encode($value) : $value;

if ($ttl) {
return $this->client()->setex($key, $ttl, $value);
}

return $this->client()->set($key, $value);
}

public function get(string $key, bool $decode = false): mixed
{
$value = $this->client()->get($key);

if ($value === null) {
return null;
}

return $decode ? json_decode($value, true) : $value;
}

public function delete(string ...$keys): int
{
return $this->client()->del(...$keys);
}

public function exists(string $key): bool
{
return (bool) $this->client()->exists($key);
}

public function expire(string $key, int $seconds): bool
{
return $this->client()->expire($key, $seconds);
}

public function ttl(string $key): int
{
return $this->client()->ttl($key);
}

public function increment(string $key, int $value = 1): int
{
return $this->client()->incrby($key, $value);
}

public function decrement(string $key, int $value = 1): int
{
return $this->client()->decrby($key, $value);
}
}

分布式锁

Redis 锁实现

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
<?php

namespace App\Services\Redis;

use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Str;

class RedisLock
{
protected string $prefix = 'lock:';
protected int $defaultTtl = 30;
protected int $defaultWait = 10;
protected int $sleepMs = 200;

public function acquire(string $key, int $ttl = null): ?string
{
$lockKey = $this->prefix . $key;
$token = Str::random(32);
$ttl = $ttl ?? $this->defaultTtl;

$acquired = Redis::set($lockKey, $token, 'EX', $ttl, 'NX');

return $acquired ? $token : null;
}

public function release(string $key, string $token): bool
{
$lockKey = $this->prefix . $key;

$script = '
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
';

return (bool) Redis::eval($script, 1, $lockKey, $token);
}

public function withLock(string $key, callable $callback, int $ttl = null, int $wait = null): mixed
{
$wait = $wait ?? $this->defaultWait;
$startTime = microtime(true);

while (true) {
$token = $this->acquire($key, $ttl);

if ($token) {
try {
return $callback();
} finally {
$this->release($key, $token);
}
}

if (microtime(true) - $startTime > $wait) {
throw new LockTimeoutException("Could not acquire lock: {$key}");
}

usleep($this->sleepMs * 1000);
}
}

public function extend(string $key, string $token, int $ttl): bool
{
$lockKey = $this->prefix . $key;

$script = '
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("PEXPIRE", KEYS[1], ARGV[2])
else
return 0
end
';

return (bool) Redis::eval($script, 1, $lockKey, $token, $ttl * 1000);
}
}

class LockTimeoutException extends \RuntimeException
{
}

发布订阅

消息发布订阅

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
<?php

namespace App\Services\Redis;

use Illuminate\Support\Facades\Redis;
use Closure;

class PubSubService
{
protected string $connection = 'default';

public function publish(string $channel, mixed $message): int
{
$payload = is_array($message) ? json_encode($message) : $message;

return Redis::connection($this->connection)->publish($channel, $payload);
}

public function subscribe(array $channels, Closure $callback): void
{
Redis::connection($this->connection)->subscribe($channels, function ($message, $channel) use ($callback) {
$data = json_decode($message, true) ?? $message;
$callback($data, $channel);
});
}

public function psubscribe(array $patterns, Closure $callback): void
{
Redis::connection($this->connection)->psubscribe($patterns, function ($message, $channel) use ($callback) {
$data = json_decode($message, true) ?? $message;
$callback($data, $channel);
});
}
}

class EventPublisher
{
protected PubSubService $pubsub;

public function __construct(PubSubService $pubsub)
{
$this->pubsub = $pubsub;
}

public function userCreated(array $user): void
{
$this->pubsub->publish('user.events', [
'event' => 'user.created',
'data' => $user,
'timestamp' => now()->toIso8601String(),
]);
}

public function orderPlaced(array $order): void
{
$this->pubsub->publish('order.events', [
'event' => 'order.placed',
'data' => $order,
'timestamp' => now()->toIso8601String(),
]);
}
}

限流器

高级限流实现

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
<?php

namespace App\Services\Redis;

use Illuminate\Support\Facades\Redis;

class RateLimiter
{
protected string $prefix = 'rate_limit:';

public function attempt(string $key, int $maxAttempts, int $decaySeconds = 60): bool
{
$current = $this->hit($key, $decaySeconds);

return $current <= $maxAttempts;
}

public function hit(string $key, int $decaySeconds = 60): int
{
$redisKey = $this->prefix . $key;

$script = '
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
return current
';

return Redis::eval($script, 1, $redisKey, $decaySeconds);
}

public function remaining(string $key, int $maxAttempts): int
{
$redisKey = $this->prefix . $key;
$current = Redis::get($redisKey);

return max(0, $maxAttempts - ($current ? (int) $current : 0));
}

public function reset(string $key): void
{
Redis::del($this->prefix . $key);
}

public function availableIn(string $key): int
{
return Redis::ttl($this->prefix . $key);
}
}

class SlidingWindowRateLimiter
{
protected string $prefix = 'sliding_rate:';

public function attempt(string $key, int $maxAttempts, int $windowSeconds = 60): bool
{
$redisKey = $this->prefix . $key;
$now = microtime(true);
$windowStart = $now - $windowSeconds;

$script = '
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window_start = tonumber(ARGV[2])
local max_attempts = tonumber(ARGV[3])
local window_seconds = tonumber(ARGV[4])

redis.call("ZREMRANGEBYSCORE", key, "-inf", window_start)

local current = redis.call("ZCARD", key)

if current < max_attempts then
redis.call("ZADD", key, now, now .. "-" .. math.random())
redis.call("EXPIRE", key, window_seconds)
return 1
end

return 0
';

return (bool) Redis::eval($script, 1, $redisKey, $now, $windowStart, $maxAttempts, $windowSeconds);
}

public function remaining(string $key, int $maxAttempts, int $windowSeconds = 60): int
{
$redisKey = $this->prefix . $key;
$windowStart = microtime(true) - $windowSeconds;

Redis::zremrangebyscore($redisKey, '-inf', $windowStart);

$current = Redis::zcard($redisKey);

return max(0, $maxAttempts - $current);
}
}

排行榜

排行榜实现

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
<?php

namespace App\Services\Redis;

use Illuminate\Support\Facades\Redis;

class Leaderboard
{
protected string $key;

public function __construct(string $name)
{
$this->key = 'leaderboard:' . $name;
}

public function add(string $member, float $score): bool
{
return Redis::zadd($this->key, $score, $member) > 0;
}

public function increment(string $member, float $amount = 1): float
{
return Redis::zincrby($this->key, $amount, $member);
}

public function remove(string $member): bool
{
return Redis::zrem($this->key, $member) > 0;
}

public function getRank(string $member, bool $descending = true): ?int
{
$rank = $descending
? Redis::zrevrank($this->key, $member)
: Redis::zrank($this->key, $member);

return $rank !== null ? $rank + 1 : null;
}

public function getScore(string $member): ?float
{
return Redis::zscore($this->key, $member);
}

public function getTop(int $limit = 10, bool $withScores = true): array
{
$results = Redis::zrevrange($this->key, 0, $limit - 1, ['WITHSCORES' => $withScores]);

if (!$withScores) {
return $results;
}

$leaderboard = [];
$rank = 1;

for ($i = 0; $i < count($results); $i += 2) {
$leaderboard[] = [
'rank' => $rank++,
'member' => $results[$i],
'score' => (float) $results[$i + 1],
];
}

return $leaderboard;
}

public function getAroundMe(string $member, int $range = 5): array
{
$rank = $this->getRank($member);

if ($rank === null) {
return [];
}

$start = max(0, $rank - $range - 1);
$end = $rank + $range - 1;

$results = Redis::zrevrange($this->key, $start, $end, ['WITHSCORES' => true]);

$leaderboard = [];
$currentRank = $start + 1;

for ($i = 0; $i < count($results); $i += 2) {
$leaderboard[] = [
'rank' => $currentRank++,
'member' => $results[$i],
'score' => (float) $results[$i + 1],
];
}

return $leaderboard;
}

public function count(): int
{
return Redis::zcard($this->key);
}

public function countByScore(float $min, float $max): int
{
return Redis::zcount($this->key, $min, $max);
}

public function clear(): void
{
Redis::del($this->key);
}
}

地理位置功能

地理位置服务

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
149
150
151
152
<?php

namespace App\Services\Redis;

use Illuminate\Support\Facades\Redis;

class GeoService
{
protected string $key;

public function __construct(string $name)
{
$this->key = 'geo:' . $name;
}

public function add(string $member, float $longitude, float $latitude): bool
{
return Redis::geoadd($this->key, $longitude, $latitude, $member) > 0;
}

public function addMultiple(array $locations): int
{
$args = [];
foreach ($locations as $member => $coords) {
$args[] = $coords['longitude'];
$args[] = $coords['latitude'];
$args[] = $member;
}

return Redis::geoadd($this->key, ...$args);
}

public function get(string $member): ?array
{
$result = Redis::geopos($this->key, $member);

if (empty($result[0])) {
return null;
}

return [
'longitude' => (float) $result[0][0],
'latitude' => (float) $result[0][1],
];
}

public function distance(string $from, string $to, string $unit = 'km'): ?float
{
$result = Redis::geodist($this->key, $from, $to, $unit);

return $result !== null ? (float) $result : null;
}

public function radius(float $longitude, float $latitude, float $radius, string $unit = 'km', array $options = []): array
{
$defaultOptions = [
'WITHDIST' => true,
'WITHCOORD' => true,
'COUNT' => null,
'ASC' => true,
];

$options = array_merge($defaultOptions, $options);

$args = [$this->key, $longitude, $latitude, $radius, $unit];

if ($options['WITHDIST']) {
$args[] = 'WITHDIST';
}

if ($options['WITHCOORD']) {
$args[] = 'WITHCOORD';
}

if ($options['COUNT']) {
$args[] = 'COUNT';
$args[] = $options['COUNT'];
}

if ($options['ASC']) {
$args[] = 'ASC';
}

$results = Redis::georadius(...$args);

return $this->formatRadiusResults($results);
}

public function radiusByMember(string $member, float $radius, string $unit = 'km', array $options = []): array
{
$defaultOptions = [
'WITHDIST' => true,
'WITHCOORD' => true,
'COUNT' => null,
'ASC' => true,
];

$options = array_merge($defaultOptions, $options);

$args = [$this->key, $member, $radius, $unit];

if ($options['WITHDIST']) {
$args[] = 'WITHDIST';
}

if ($options['WITHCOORD']) {
$args[] = 'WITHCOORD';
}

if ($options['COUNT']) {
$args[] = 'COUNT';
$args[] = $options['COUNT'];
}

if ($options['ASC']) {
$args[] = 'ASC';
}

$results = Redis::georadiusbymember(...$args);

return $this->formatRadiusResults($results);
}

public function remove(string $member): bool
{
return Redis::zrem($this->key, $member) > 0;
}

protected function formatRadiusResults(array $results): array
{
$formatted = [];

foreach ($results as $result) {
$item = ['member' => $result[0]];

if (isset($result[1])) {
$item['distance'] = (float) $result[1];
}

if (isset($result[2])) {
$item['coordinates'] = [
'longitude' => (float) $result[2][0],
'latitude' => (float) $result[2][1],
];
}

$formatted[] = $item;
}

return $formatted;
}
}

位图操作

位图服务

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
<?php

namespace App\Services\Redis;

use Illuminate\Support\Facades\Redis;

class BitmapService
{
protected string $prefix = 'bitmap:';

public function set(string $key, int $offset, bool $value): bool
{
return Redis::setbit($this->prefix . $key, $offset, $value ? 1 : 0);
}

public function get(string $key, int $offset): bool
{
return (bool) Redis::getbit($this->prefix . $key, $offset);
}

public function count(string $key): int
{
return Redis::bitcount($this->prefix . $key);
}

public function countRange(string $key, int $start, int $end): int
{
return Redis::bitcount($this->prefix . $key, $start, $end);
}

public function findFirst(string $key, bool $value, int $start = 0): int
{
return Redis::bitpos($this->prefix . $key, $value ? 1 : 0, $start);
}

public function op(string $operation, string $destKey, array $keys): int
{
$fullKeys = array_map(fn($k) => $this->prefix . $k, $keys);

return Redis::bitop($operation, $this->prefix . $destKey, ...$fullKeys);
}

public function and(string $destKey, array $keys): int
{
return $this->op('AND', $destKey, $keys);
}

public function or(string $destKey, array $keys): int
{
return $this->op('OR', $destKey, $keys);
}

public function xor(string $destKey, array $keys): int
{
return $this->op('XOR', $destKey, $keys);
}

public function not(string $destKey, string $key): int
{
return $this->op('NOT', $destKey, [$key]);
}
}

class UserActivityTracker
{
protected BitmapService $bitmap;

public function __construct(BitmapService $bitmap)
{
$this->bitmap = $bitmap;
}

public function markActive(int $userId, ?string $date = null): void
{
$date = $date ?? today()->format('Y-m-d');
$this->bitmap->set("user_activity:{$date}", $userId, true);
}

public function isActive(int $userId, ?string $date = null): bool
{
$date = $date ?? today()->format('Y-m-d');
return $this->bitmap->get("user_activity:{$date}", $userId);
}

public function getActiveCount(?string $date = null): int
{
$date = $date ?? today()->format('Y-m-d');
return $this->bitmap->count("user_activity:{$date}");
}

public function getRetainedUsers(string $date1, string $date2): int
{
$this->bitmap->and('retained_temp', [
"user_activity:{$date1}",
"user_activity:{$date2}",
]);

return $this->bitmap->count('retained_temp');
}
}

总结

Laravel 13 与 Redis 的深度集成提供了丰富的功能,包括分布式锁、发布订阅、限流、排行榜、地理位置和位图操作等。合理利用这些功能可以构建高性能、高可用的应用系统。