在 Laravel 6 中,缓存无故被删除的排查分析(Cache::tags)
1、设置与获取缓存的代码如下2、缓存的过期时间(秒)为 86400,但是在生产环境中,有极低概率出现不到 24 小时就过期的情况,甚至有才过去几个小时就无法获取缓存的情况。打印 $data,其值为 NULL。
3、决定先排除掉外部因素的干扰,先从程序本身入手分析。初步怀疑是程序本身的某处操作了缓存,导致了缓存被清空。
4、在 .env 的缓存相关配置如下5、打印配置项,database.reids.client=phpredis;cache.default=redis。如图16、在 /config/cache.php 中的配置如下7、在 /config/database.php 中的配置如下8、在 /config/app.php 配置文件的别名部分,未重命名 Illuminate \ Support \ Facades \ Redis 外观别名。但是,并未抛出异常:Please remove or rename the Redis facade alias in your “app” configuration file in order to avoid collision with the PHP Redis extension.。决定暂时忽略此问题。9、仍然决定重命名为其他内容,例如 RedisManager,以避免与 Redis 类发生冲突。10、决定登录至 Redis 中确认一下,先在本地环境确认一下执行命令的顺序。如图211、决定登录至 Redis 中确认一下,基于 Tinker 查询。按照如上的执行命令的顺序,以确认本地环境中是否存在对应的 key。如图312、TTL key,以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。一个 key 是 2022/09/09 15:14 生成的。现在时间是 2022/09/09 16:44 。已经过去了 1 小时 30 分钟。86400 – 1 * 3600 – 30 * 60 = 81000。剩余生存秒数为:int(81060)。符合预期。如图413、决定在线上环境重新生成一个,然后查看剩余生存时间秒数。刚生成的一个 key 的剩余生存秒数为:int(85730)。符合预期。如图514、决定登录至 Redis 中确认一下,基于 Tinker 查询。按照如上的执行命令的顺序,以确认线上环境中是否存在对应的 key。确定对应的 key 未被完全清空。如果执行了 Cache::tags(ThemePreviewInterface::TAG_THEME_EDITOR . ‘_’ . $sessionId)->flush() 或者 Cache::flush(),则 执行查询 var_dump(\Illuminate\Support\Facades\Redis::connection(‘cache’)->get(‘arrest9_cache:tag:theme_editor_Uq6BLXO7GsbWHQovCXQAshX8SdaS6BXh2lWE:key’)); 时,会返回 null,但是现在是有值的。如图6、图6.1 15、决定基于事件监听,添加相应的日志。新建监听器文件 /app/Listeners/Cache/LogKeyWrittenListener.php ,当写入 key 时。16、生成的日志信息如下。如图717、新建监听器文件 /app/Listeners/Cache/LogCacheHitListener.php ,当命中 key 时。18、新建监听器文件 /app/Listeners/Cache/LogCacheMissedListener.php ,当未命中 key 时。19、但是,当移除带有标签的缓存数据时,使用 Cache::tags(ThemePreviewInterface::TAG_THEME_EDITOR . ‘_’ . $sessionId)->flush(); ,监听器 /app/Listeners/Cache/LogKeyForgottenListener.php 并未生成日志,当清除 key 时。20、决定使用 flush 方法清空所有的缓存,Cache::flush();。以确认是否会触发清除 key 的事件,确认未触发。但是当执行了清空所有的缓存操作后,已经无法获取带有标签的缓存数据了。不论是否带有标签,所有的缓存数据皆被清空。如图821、所以即使有 4 个缓存日志监听器,仍然很难判断 key 是什么时候被清掉的。只能够先在预计会被清掉的地方,记录一下相应的日志。
22、针对第 16 步骤,决定咨询一下运维同事,有时候运维会针对 Redis 的 死 key ,运行 Python 脚本,手动执行清理操作。因此,决定将同样的 Python 脚本在本地环境模拟复现。执行:python batch-remove-rediskey.py /tmp。本地未复现,分析脚本,线上有可能复现。不过,运维已经确定,在最近出现问题的时间段内,并未手动运行过此脚本。如图923、由于设置了2个 tag,其中一个 tag 下的 key 的数量会不断地递增,其数量最终可能在一周内达到几千甚至几万个。最终决定仅设置1个 tag。设置与获取缓存的代码如下24、在仅设置了1个 tag 后,且这1个 tag 中的 key 的数量一般就维持在 20 个左右,缓存未再出现无法命中的情况,整体运行正常。初步分析得出结论:在使用 Cache::tags 时,其下的 tag 的数量不宜过多,建议仅设置 1 个,具体到每个 tag 下的 key 的数量也不宜过多,建议不要超过 100。
const TAG_THEME_EDITOR = 'theme_editor'; // 缓存标签:主题编辑
const THEME_EDITOR_SESSION_PREFIX = 'theme_editor_session:'; // 缓存前缀:主题编辑会话
const TTL = 86400; // 缓存的过期时间(秒)
public static function saveSessionMetadata(string $sessionId, array $data): bool
{
return Cache::tags([
ThemePreviewInterface::TAG_THEME_EDITOR,
ThemePreviewInterface::TAG_THEME_EDITOR . '_' . $sessionId]
)
->put(ThemePreviewInterface::THEME_EDITOR_SESSION_PREFIX . $sessionId, json_encode($data), ThemePreviewInterface::TTL);
}
public static function getSessionMetadata(string $sessionId): array
{
$data = Cache::tags([
ThemePreviewInterface::TAG_THEME_EDITOR,
ThemePreviewInterface::TAG_THEME_EDITOR . '_' . $sessionId
])
->get(ThemePreviewInterface::THEME_EDITOR_SESSION_PREFIX . $sessionId);
if (empty($data)) {
return [];
}
return JsonDecoder::jsonToArray($data);
}
CACHE_DRIVER=redis
REDIS_CLIENT=phpredis
/object # php artisan tinker
Psy Shell v0.9.12 (PHP 7.4.26 — cli) by Justin Hileman
>>> echo config('database.redis.client');
>>> echo config('database.redis.client');
phpredis⏎
>>> echo config('cache.default');
redis⏎
>>>
'stores' => [
'redis' => [
'driver' => 'redis',
'connection' => 'cache',
],
],
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'predis'),
],
'cache' => [
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_database_'),
'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),
'persistent'=>env('REDIS_PERSISTENT', true),
'persistent_id'=>env('REDIS_PERSISTENT_ID', 'cache'),
'retry_interval'=>env('REDIS_PERSISTENT_RETRY', 3000),
],
],
'aliases' => [
'Redis' => Illuminate\Support\Facades\Redis::class,
],
'aliases' => [
'RedisManager' => Illuminate\Support\Facades\Redis::class,
],
127.0.0.1:1>GET object_cache:tag:theme_editor_48wJgWKHnabdr3AznlDhP5onmEVedTz67ULb:key
"s:22:"631ae7ba62b83808649247";"
127.0.0.1:1>SMEMBERS object_cache:631ae7ba62b83808649247:standard_ref
1) "object_cache:c613daaf6b394e496382fd07bc808ea8a497556f:theme_editor_session:9734e008-f539-4f76-9e9f-d8037334b968:48wJgWKHnabdr3AznlDhP5onmEVedTz67ULb:product_detail"
2) "object_cache:c613daaf6b394e496382fd07bc808ea8a497556f:theme_editor_session:48wJgWKHnabdr3AznlDhP5onmEVedTz67ULb"
127.0.0.1:1>GET object_cache:c613daaf6b394e496382fd07bc808ea8a497556f:theme_editor_session:48wJgWKHnabdr3AznlDhP5onmEVedTz67ULb
"s:9329:"{"id":372}";"
127.0.0.1:1>
PS E:\wwwroot\object> php artisan tinker
Psy Shell v0.9.12 (PHP 7.4.27 — cli) by Justin Hileman
>>> var_dump(\Illuminate\Support\Facades\Redis::connection('cache')->get('object_cache:tag:theme_
editor_48wJgWKHnabdr3AznlDhP5onmEVedTz67ULb:key'));
string(30) "s:22:"631ae7ba62b83808649247";"
=> null
>>> var_dump(\Illuminate\Support\Facades\Redis::connection('cache')->SMEMBERS('object_cache:631ae
7ba62b83808649247:standard_ref'));
array(2) {
[0]=>
string(186) "object_cache:c613daaf6b394e496382fd07bc808ea8a497556f:theme_editor_session:9734e008-f539-4f76-9e9f-d8037334b968:48wJgWKHnabdr3AznlDhP5onmEVedTz67ULb:product_detail"
[1]=>
string(134) "object_cache:c613daaf6b394e496382fd07bc808ea8a497556f:theme_editor_session:48wJgWKHnabdr3AznlDhP5onmEVedTz67ULb"
}
=> null
>>> var_dump(\Illuminate\Support\Facades\Redis::connection('cache')->GET('object_cache:c613daaf6b
394e496382fd07bc808ea8a497556f:theme_editor_session:48wJgWKHnabdr3AznlDhP5onmEVedTz67ULb'));
string(9339) "s:9329:"{"id":372}";"
=> null
>>> var_dump(\Illuminate\Support\Facades\Redis::connection('cache')->TTL('object_cache:c613daaf6b
394e496382fd07bc808ea8a497556f:theme_editor_session:48wJgWKHnabdr3AznlDhP5onmEVedTz67ULb'));
int(81060)
=> null
>>>
>>> var_dump(\Illuminate\Support\Facades\Redis::connection('cache')->get('arrest9_cache:tag:theme_editor_Uq6BLXO7GsbWHQovCXQAshX8SdaS6BXh2lWE:key'));
string(30) "s:22:"63243db5c01db958165221";
=> null
>>> var_dump(\Illuminate\Support\Facades\Redis::connection('cache')->TTL('arrest9_cache:tag:theme_editor_Uq6BLXO7GsbWHQovCXQAshX8SdaS6BXh2lWE:key'));
int(-1)
=> null
>>> var_dump(\Illuminate\Support\Facades\Redis::connection('cache')->SMEMBERS('arrest9_cache:63243db5c01db958165221:standard_ref'));
array(0) {
}
=> null
>>>
>>> var_dump(\Illuminate\Support\Facades\Redis::connection('cache')->get('object_cache:tag:theme_
editor_ERo5GoALUnDuxcMtW3yPYPKsjwci8DVZR2ij:key'));
NULL
=> null
>>> var_dump(\Illuminate\Support\Facades\Redis::connection('cache')->SMEMBERS('object_cache:632a7
fda56e94065934346:standard_ref'));
array(0) {
}
=> null
>>>
<pre class="wp-block-syntaxhighlighter-code">
<?php
namespace App\Listeners\Cache;
use Illuminate\Cache\Events\KeyWritten;
use Illuminate\Support\Facades\Log;
use Modules\OnlineStoreThemeGraphQL\Preview\ThemePreviewInterface;
class LogKeyWrittenListener
{
/**
* 处理事件
*
* @param KeyWritten $event
* @return void
*/
public function handle(KeyWritten $event)
{
if (config('app.debug') === true && strpos($event->key, ThemePreviewInterface::THEME_EDITOR_SESSION_PREFIX) !== false) {
Log::debug('theme_editor_session_key_written', [
'key' => $event->key,
'tags' => $event->tags,
'value' => $event->value,
'seconds' => $event->seconds
]);
}
}
}
</pre>
[2022-09-15 17:26:05] local.DEBUG: theme_editor_session_key_written {
"key": "theme_editor_session:7JivgR1WqXVeE4hsyA335ILc60k153yTynIN",
"tags": [
"theme_editor",
"theme_editor_7JivgR1WqXVeE4hsyA335ILc60k153yTynIN"
],
"value": "",
"seconds": 86400
}
/**
* 处理事件
*
* @param CacheHit $event
* @return void
*/
public function handle(CacheHit $event)
{
if (config('app.debug') === true && strpos($event->key, ThemePreviewInterface::THEME_EDITOR_SESSION_PREFIX) !== false) {
Log::debug('theme_editor_session_cache_hit', [
'key' => $event->key,
'tags' => $event->tags,
'value' => $event->value
]);
}
}
[2022-09-15 18:09:08] local.DEBUG: theme_editor_session_cache_hit {
"key": "theme_editor_session:KrV0Nf5rMOiS5ZdF6i2uDandZZZWPPpXcQ4D",
"tags": [
"theme_editor",
"theme_editor_KrV0Nf5rMOiS5ZdF6i2uDandZZZWPPpXcQ4D"
],
"value": ""
}
/**
* 处理事件
*
* @param CacheMissed $event
* @return void
*/
public function handle(CacheMissed $event)
{
if (config('app.debug') === true && strpos($event->key, ThemePreviewInterface::THEME_EDITOR_SESSION_PREFIX) !== false) {
Log::debug('theme_editor_session_cache_missed', [
'key' => $event->key,
'tags' => $event->tags
]);
}
}
[2022-09-15 18:10:15] local.DEBUG: theme_editor_session_cache_missed {
"key": "theme_editor_session:KrV0Nf5rMOiS5ZdF6i2uDandZZZWPPpXcQ4D",
"tags": [
"theme_editor",
"theme_editor_KrV0Nf5rMOiS5ZdF6i2uDandZZZWPPpXcQ4D"
]
}
/**
* 处理事件
*
* @param KeyForgotten $event
* @return void
*/
public function handle(KeyForgotten $event)
{
if (config('app.debug') === true && strpos($event->key, ThemePreviewInterface::THEME_EDITOR_SESSION_PREFIX) !== false) {
Log::debug('theme_editor_session_key_forgotten', [
'key' => $event->key,
'tags' => $event->tags
]);
}
}
PS E:\wwwroot\object> python batch-remove-rediskey.py /tmp
/tmp*
All done
const TAG_THEME_EDITOR = 'theme_editor'; // 缓存标签:主题编辑
const THEME_EDITOR_SESSION_PREFIX = 'theme_editor_session:'; // 缓存前缀:主题编辑会话
const TTL = 86400; // 缓存的过期时间(秒)
public static function saveSessionMetadata(string $sessionId, array $data): bool
{
return Cache::tags([
ThemePreviewInterface::TAG_THEME_EDITOR . '_' . $sessionId]
)
->put(ThemePreviewInterface::THEME_EDITOR_SESSION_PREFIX . $sessionId, json_encode($data), ThemePreviewInterface::TTL);
}
public static function getSessionMetadata(string $sessionId): array
{
$data = Cache::tags([
ThemePreviewInterface::TAG_THEME_EDITOR . '_' . $sessionId
])
->get(ThemePreviewInterface::THEME_EDITOR_SESSION_PREFIX . $sessionId);
if (empty($data)) {
return [];
}
return JsonDecoder::jsonToArray($data);
}









