没有不值得去解决的问题,也没有不值得去学习的技术!

Investigating Why Cache Is Unexpectedly Deleted in Laravel 6 (Cache::tags)

Decided to log into Redis to verify based on the Tinker query. Following the command sequence above, to confirm whether the corresponding keys exist in the local environment

1. The code for setting and retrieving the cache is as follows:

PHP

    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);
    }

2. The cache expiration time (in seconds) is 86400, but in the production environment, there is a very low probability that it expires in less than 24 hours. In some cases, the cache cannot be retrieved after only a few hours. Printing $data returns a value of NULL.

3. I decided to rule out external factors first and start the analysis from the program itself. My initial suspicion was that an operation within the program cleared the cache.

4. The cache-related configuration in .env is as follows:

Plaintext

CACHE_DRIVER=redis
REDIS_CLIENT=phpredis

5. Printing the configuration items shows database.reids.client=phpredis; cache.default=redis. See Figure 1.

Print the configuration items, database.reids.client=phpredis; cache.default=redis
Figure 1
Plaintext

/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⏎
>>> 

6. The configuration in /config/cache.php is as follows:

PHP

    'stores' => [

        'redis' => [
            'driver' => 'redis',
            'connection' => 'cache',
        ],



    ],


7. The configuration in /config/database.php is as follows:

PHP


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

    ],

8. In the aliases section of the /config/app.php configuration file, the Illuminate \ Support \ Facades \ Redis facade alias was not renamed. However, no exception was thrown: Please remove or rename the Redis facade alias in your “app” configuration file in order to avoid collision with the PHP Redis extension. I decided to temporarily ignore this issue.

PHP

    'aliases' => [

        'Redis' => Illuminate\Support\Facades\Redis::class,
    ],

9. I still decided to rename it to something else, such as RedisManager, to avoid conflicts with the Redis class.

PHP

    'aliases' => [

        'RedisManager' => Illuminate\Support\Facades\Redis::class,
    ],

10. I decided to log into Redis to verify, first checking the sequence of executed commands in the local environment. See Figure 2.

Decided to log into Redis to verify, first checking the sequence of executed commands in the local environment
Figure 2
Plaintext

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>

11. I decided to log into Redis to verify based on the Tinker query. Following the command sequence above, I confirmed whether the corresponding keys existed in the local environment. See Figure 3.

Decided to log into Redis to verify based on the Tinker query. Following the command sequence above, to confirm whether the corresponding keys exist in the local environment
Figure 3
Plaintext

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

12. The TTL command returns the remaining time to live (TTL) of a given key in seconds. One key was generated at 2022/09/09 15:14. The current time is 2022/09/09 16:44. 1 hour and 30 minutes have passed. 86400 – 1 * 3600 – 30 * 60 = 81000. The remaining time to live in seconds is: int(81060). This matches expectations. See Figure 4.

The TTL command returns the remaining time to live (TTL) of a given key in seconds. One key was generated at 2022/09/09 15:14. The current time is 2022/09/09 16:44. 1 hour and 30 minutes have passed. 86400 - 1 * 3600 - 30 * 60 = 81000. The remaining time to live in seconds is: int(81060). This matches expectations
Figure 4

13. I decided to regenerate one in the production environment and then check the remaining time to live in seconds. The remaining time to live in seconds for a newly generated key is: int(85730). This matches expectations. See Figure 5.

Decided to regenerate one in the production environment and then check the remaining time to live in seconds. The remaining time to live in seconds for a newly generated key is: int(85730). This matches expectations
Figure 5

14. I decided to log into Redis to verify based on the Tinker query. Following the command sequence above, I confirmed whether the corresponding keys existed in the production environment. I confirmed that the corresponding keys had not been completely cleared. If Cache::tags(ThemePreviewInterface::TAG_THEME_EDITOR . ‘_’ . $sessionId)->flush() or Cache::flush() had been executed, running the query var_dump(\Illuminate\Support\Facades\Redis::connection(‘cache’)->get(‘arrest9_cache:tag:theme_editor_Uq6BLXO7GsbWHQovCXQAshX8SdaS6BXh2lWE:key’)); would return null, but right now it has a value. See Figure 6 and Figure 6.1.

Decided to log into Redis to verify based on the Tinker query. Following the command sequence above, to confirm whether the corresponding keys exist in the production environment. Confirmed that the corresponding keys have not been completely cleared.
Figure 6

 

If Cache::tags(ThemePreviewInterface::TAG_THEME_EDITOR . '_' . $sessionId)->flush() or Cache::flush() is executed, running the query var_dump(\Illuminate\Support\Facades\Redis::connection('cache')->get('arrest9_cache:tag:theme_editor_Uq6BLXO7GsbWHQovCXQAshX8SdaS6BXh2lWE:key')); would return null, but right now it has a value
Figure 6.1
Plaintext

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

Plaintext

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

15. I decided to add appropriate logs based on event listeners. I created a listener file /app/Listeners/Cache/LogKeyWrittenListener.php for when a key is written.

PHP

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
            ]);
        }
    }
}


16. The generated log information is as follows. See Figure 7.

The generated log information is as follows
Figure 7
Plaintext

[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
} 

17. I created a listener file /app/Listeners/Cache/LogCacheHitListener.php for when a key is hit.

PHP

    /**
     * 处理事件
     *
     * @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
            ]);
        }
    }

Plaintext

[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": ""
} 

18. I created a listener file /app/Listeners/Cache/LogCacheMissedListener.php for when a key is missed.

PHP

    /**
     * 处理事件
     *
     * @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
            ]);
        }
    }

Plaintext

[2022-09-15 18:10:15] local.DEBUG: theme_editor_session_cache_missed {
    "key": "theme_editor_session:KrV0Nf5rMOiS5ZdF6i2uDandZZZWPPpXcQ4D",
    "tags": [
        "theme_editor",
        "theme_editor_KrV0Nf5rMOiS5ZdF6i2uDandZZZWPPpXcQ4D"
    ]
} 

19. However, when removing tagged cache data using Cache::tags(ThemePreviewInterface::TAG_THEME_EDITOR . ‘_’ . $sessionId)->flush();, the listener /app/Listeners/Cache/LogKeyForgottenListener.php did not generate a log when the key was cleared.

PHP

    /**
     * 处理事件
     *
     * @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
            ]);
        }
    }

20. I decided to use the flush method to clear all caches, Cache::flush();. This confirmed whether the key clearing event would be triggered, and it was not. However, after executing the operation to clear all caches, the tagged cache data could no longer be retrieved. All cache data was cleared, whether tagged or not. See Figure 8.

Decided to use the flush method to clear all caches, Cache::flush();. To confirm whether the key clearing event would be triggered, and confirmed it was not. However, after executing the operation to clear all caches, the tagged cache data could no longer be retrieved. All cache data was cleared, whether tagged or not
Figure 8

21. So even with 4 cache log listeners, it is still difficult to determine when a key is cleared. I could only log the relevant information in the places where it was expected to be cleared first.

22. Regarding step 16, I decided to consult the operations colleagues. Sometimes they run a Python script to manually clean up dead keys in Redis. Therefore, I decided to simulate and reproduce the same Python script in the local environment. Executed: python batch-remove-rediskey.py /tmp. It did not reproduce locally, but after analyzing the script, it might reproduce in the production environment. However, the operations team confirmed that they had not manually run this script during the recent period when the issue occurred. See Figure 9.

Regarding step 16, decided to consult the operations colleagues. Sometimes they run a Python script to manually clean up dead keys in Redis. Therefore, decided to simulate and reproduce the same Python script in the local environment. Executed: python batch-remove-rediskey.py /tmp. It did not reproduce locally, but after analyzing the script, it might reproduce in the production environment. However, the operations team confirmed that they had not manually run this script during the recent period when the issue occurred
Figure 9
Plaintext

PS E:\wwwroot\object> python batch-remove-rediskey.py /tmp
/tmp*
All done

23. Because 2 tags were set, the number of keys under one of the tags would continuously increase, potentially reaching thousands or even tens of thousands within a week. I finally decided to set only 1 tag. The code for setting and retrieving the cache is as follows:

PHP

    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);
    }

24. After setting only 1 tag, with the number of keys under this tag generally maintained at around 20, the cache no longer experienced missed hits, and the overall operation returned to normal. The preliminary analysis concludes that when using Cache::tags, the number of tags should not be too large, and it is recommended to set only 1. Specifically, the number of keys under each tag should also not be too large, and it is recommended not to exceed 100.

关于作者 · 持续学习与技术实践

我是一名拥有 15 年以上开发经验的技术从业者,自 2013 年起持续运营个人技术博客,记录工作与个人项目中遇到的真实问题,以及解决问题过程中的探索、实践和思考。

除了技术写作,我也在持续开发和维护自己的独立项目,探索新技术和新工具在实际场景中的应用。我始终相信,没有不值得去解决的问题,也没有不值得去学习的技术。

如果这篇文章对你有所帮助,欢迎浏览博客中的其他文章,也欢迎交流技术经验与想法。

关于我  ·  GitHub  ·  邮件联系