Laravel cache 使用 AWS ElastiCache for Redis cluster

前言

Redis 是有名的記憶體資料庫,Laravel 其實預設設定檔中也有 Redis 的相關設定,相關使用都有不少資料。但如果是 AWS ElastiCache for Redis cluster 呢?資料雖然少很多,但也不至於無法繼續。

不過今天在實際使用上,卻發現很多奇怪的問題,不僅官方文件寫的籠統,很多找到的資料也都沒什麼幫助。所以今天寫這篇文,希望能造福後續有需要的人。

本文內容適用於開啟叢集模式的情況;未開啟叢集模式的話不會有本文提到的問題。

TL;DR

使用時請注意:

  1. database 只能填 0
  2. config/database.php 中的 redis key 中,無論有無包在 clusters 底下,其 connection 名稱皆不可重複

細節版

本文環境為:

  • Laravel 8.73.1
  • predis 2.0.0

來看看快取設定

我們要設定什麼,當然就是要先去看對應的設定檔都寫了什麼!所以下面是初始化設定檔 config/cache.php 擷取 redis 的部分:

1
2
3
4
5
'redis' => [
'driver' => 'redis',
'connection' => 'cache',
'lock_connection' => 'default',
],

嗯……有點簡潔,那我們看看文件怎麼說吧。

官方文件怎麼說

官方文件中關於設定 Redis 快取的部分:https://laravel.com/docs/8.x/cache#redis

Before using a Redis cache with Laravel, you will need to either install the PhpRedis PHP extension via PECL or install the predis/predis package (~1.0) via Composer. Laravel Sail already includes this extension. In addition, official Laravel deployment platforms such as Laravel Forge and Laravel Vapor have the PhpRedis extension installed by default.

For more information on configuring Redis, consult its Laravel documentation page.

文件中說 predis 套件應該是 v1,不過我下 composer require predis/predis 解析出來卻是安裝 v2,不知道是不是文件沒更新到還是怎樣。但有去看 release log 應該是不影響。

總之在 cache 這邊著墨很少,因此我們轉移陣地到文件的另外一邊:https://laravel.com/docs/8.x/redis

OK 文件叫我們來去 config/database.php 看看 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
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer body of commands than a typical key-value system
| such as APC or Memcached. Laravel makes it easy to dig right in.
|
*/

'redis' => [

'client' => env('REDIS_CLIENT', 'phpredis'),

'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
],

'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'),
],

'cache' => [
'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_CACHE_DB', '1'),
],

],

好的好像看一遍後沒什麼,我們繼續往下。接下來是 clusters:https://laravel.com/docs/8.x/redis#clusters

大致看了下,不是很懂,官方文件只是簡單帶過,說如果是叢集要加上 clusters key:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'redis' => [

'client' => env('REDIS_CLIENT', 'phpredis'),

'clusters' => [
'default' => [
[
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
],
],
],

],

好吧沒關係,先放旁邊之後再研究,繼續往下是 Predis:https://laravel.com/docs/8.x/redis#predis

這邊告訴我們記得把 env 的 REDIS_CLIENT 設定成 predis 或是直接改 config 的預設值。這個 key 在預設產生的 env 中是沒有的。

再往下看好像就和這次的主題沒什麼關係了,所以文件導覽到這邊。

實際動手

在建立好 ElastiCache for Redis cluster 後,依照有無啟動叢集模式,AWS 給你的 Redis host 也不同,詳細可見此篇官方文件說明:https://docs.aws.amazon.com/zh_tw/AmazonElastiCache/latest/red-ug/Endpoints.html

總之呢在我設定好 host、client 的參數後,上機一改,讓我來 php artisan tinkerCache::get('key') 試試!結果:

1
Predis\Connection\ConnectionException with message 'SELECT failed: ERR SELECT is not allowed in cluster mode [tcp://<AWS domain>.cache.amazonaws.com:6379]'

查了半天也不知道是為什麼,但有「cluster」關鍵字,不然我來把剛剛文件說的加上去試試!結果一樣(倒

結果最後在日文網站找到前人的筆記:https://qiita.com/mangano-ito/items/92e8a6b434c4b79b790c

其中有講到 DB 的號碼的限制,大致上就是在叢集模式的 Redis 只能用 0 號 DB

再回頭看官方文件,難怪寫 'database' => 0, 而不是初始化設定的 'database' => env('REDIS_DB', '0'),。或許這對 Redis 高手是常識,可是我菜逼八嗚嗚。

Redis 叢集模式下,database 只能指定 0

解決DB號碼

回到 config/cache.php,大家還記得剛剛貼的初始化設定檔嗎?其中有一項是指定連線的:

1
2
3
4
5
'redis' => [
'driver' => 'redis',
'connection' => 'cache', // 就是這裡
'lock_connection' => 'default',
],

這個名稱好像剛剛在 config/database.php 有看到?

1
2
3
4
5
6
7
8
9
10
11
12
13
'redis' => [
/* 省略 */

// 在這邊
'cache' => [
'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_CACHE_DB', '1'),
],

],

原來是這邊,那我把 env REDIS_CACHE_DB 改成 0 就好了吧!

可惜事情沒這麼簡單:

1
Predis\Response\ServerException with message 'MOVED 299 <內網IP>:6379'

解決MOVE的錯誤

很好,在找 db 號碼錯誤的時候就有看到很多人有類似的問題,不過我問題是 299,大家的號碼都不一樣,不曉得是連接埠號還是?總之讓我來試試看!

大家好像都是走官方文件的方式,還額外加了文件上沒說的東西:

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
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),

'cluster' => true, // 新增的,官方文件沒有,但很多解法都有寫到這行

'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
],

'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'),
],

'cache' => [
'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_CACHE_DB', '1'),
],

// 新增的,官方文件複製貼上
'clusters' => [
'default' => [
[
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
],
],
],

],

執行一下還是錯,然後觀察下想到,難道是 clusters 裡沒有 cache 的關係嗎?試試看吧:

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
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),

'cluster' => true,

'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
],

'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'),
],

'cache' => [
'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_CACHE_DB', '1'),
],

'clusters' => [
'default' => [
[
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
],
],
// 新增 cache 試試
'cache' => [
[
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
],
],
],

],

還是錯ㄚㄚㄚㄚㄚㄚㄚ
◢▆▅▄▃ 崩╰(〒皿〒)╯潰 ▃▄▅▆◣


就在我來回測試好多東西後,突發奇想,註解一下原本的試試?死馬當活馬醫,就上吧:

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
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),

'cluster' => true,

'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
],

// 註解了原先初始產生的部分設定檔
/*
'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'),
],

'cache' => [
'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_CACHE_DB', '1'),
],
*/

'clusters' => [
'default' => [
[
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
],
],
'cache' => [
[
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
],
],
],

],

太神奇了傑克,通了!

那我現在就開始測試到底是哪邊發揮作用。


首先拿掉了 'cluster' => true,,因為這個不存在在官方文件上。

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
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),

// 拿掉了 'cluster' => true,

'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
],

/*
'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'),
],

'cache' => [
'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_CACHE_DB', '1'),
],
*/

'clusters' => [
'default' => [
[
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
],
],
'cache' => [
[
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
],
],
],

],

很好可以用,看來這行在這次我的使用情境下可能沒啥用。

接著我把註解的部分拿掉試試:

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
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),

'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
],

// 取消這邊的註解
'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'),
],

'cache' => [
'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_CACHE_DB', '1'),
],

'clusters' => [
'default' => [
[
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
],
],
'cache' => [
[
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
],
],
],

],

WTF,居然又不能用了。

這時我開始思考到底是為什麼,不過當然想不出所以然。不過有一點很明顯,就是撇開被 clusters 包住不說,裡外的 connection key 是相同的。

那是不是我 config/cache.php 指定錯了?於是我到這邊改了連線名稱:

1
2
3
4
5
'redis' => [
'driver' => 'redis',
'connection' => 'clusters.cache', // 這裡
'lock_connection' => 'default',
],

執行後直接跟我說這個 connection 沒被設定,那看來不是。Ctrl + Z


延續剛剛的想法,那我用 clusters 陣列包住的 cache 陣列套在外頭呢?

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
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),

'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
],

'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'),
],

// 多包一層 array
'cache' => [
[
'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_CACHE_DB', '1'),
],
]

// 移除 clusters key
],

太神奇了,又可以用了!什麼 'cluster' => true, 根本是寫心安的,不然就是哪邊出了 bug,反正對我來說沒用。


原先想說不然就這樣吧,但又想到我也只有 production 是叢集模式,測試和開發環境都不是,雖然有看到有人說就算不是叢集模式也能用相同設定檔,可是我被騙怕了,決定還是另外寫一個新的吧:

config/cache.php

1
2
3
4
5
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), // 這裡改吃 env
'lock_connection' => 'default',
],

config/database.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
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),

'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
],

'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'),
],

'cache' => [
'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_CACHE_DB', '1'),
],

// 新增一個 connection,但裡面比照官方文件的方式用兩個陣列
'elasticache' => [
[
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
],
],
],

OK 測試後發現可以,差不多可以收工了吧。


不!我想看看那個 clusters 到底有沒有影響!

於是上面的 config/database.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
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),

'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
],

'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'),
],

'cache' => [
'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_CACHE_DB', '1'),
],

// 用 clusters 包住剛剛新增的 connection
'clusters' => [
'elasticache' => [
[
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
],
],
],
],

嘿,居然一樣可動!?那這個 clusters 到底做啥用的?總之雖然都可以,但最後我選擇比照官方文件的方式包起來,然後裡面新增自己的 unique connection name。

config/database.php 中的 redis key 中,無論有無包在 clusters 底下,其 connection 名稱皆不可重複

到這邊,才是真的可以收工了呼~

結語

這次踩坑讓我再次體會到下面這張圖:

官方文件在這上面講的很籠統,除了 database 號碼要是 0 之外,connection name 的推論是否正確也不知道。因此把這次經驗一路上如何測試和 debug 的過程分享出來,如果有大大知道原因或是發現有誤歡迎一起探討和分享。

打了兩個小時,該收工享受 Friday night 了,我們下次有機會再見哩。