Laravelで特定のアプリケーションキャッシュを狙ったタイミングで消す

バックエンドでAPIを構築している時、そのレスポンス速度とは常に戦い続けなければなりません。

`Laravel` をバックエンドサーバーとして利用している場合には、`Cache` ファサードを利用することが多いでしょう。

# キャッシュは処理速度向上に効果絶大

ご存知のように、多くの場合、「キャッシュ」がとてつもない効果を発揮します。

[Laravel 8.x キャッシュ](https://readouble.com/laravel/8.x/ja/cache.html)

特に、レスポンスが重い（複雑なORMを書いてしまっている / データーベースの容量がとてつもなく大きい / レスポンス結果が大きい / ...）場合には、キャッシュは非常に効きます。

シンプルに書くと、キャッシュとは以下のようなコードを指します。

```php:HogeController.php

$key = 'users';
$seconds = 60 * 60; // 1 hour

// もしキャッシュがあればキャッシュを利用する
// もしキャッシュがなければSQLを叩いて新しいキャッシュを保存する
$value = Cache::remember($key, $seconds, function () {
    return DB::table('users')->get();
});

// レスポンス
return response($value, 200);

```

キャッシュを導入するだけで、ウェブサイトが**明らかに体感で感じられるほど速くなる**ので、気持ち良いものです。

一方で、実際にキャッシュの運用を開始すると、

Aさん「すみません、記事の本文を編集したのですが、ウェブサイトで反映されていません」

Bさん「すみません、画像を更新したのですが、ウェブサイトで反映されていません」

Cさん「すみません、記事のタイトルを更新したのですが、ウェブサイトで反映されていません」

というような苦情を社内で定期的にいただくようになります。

# 「すみません、仕様です」

「キャッシュが...」と言っても、エンジニアでないとなかなか正確には伝わりません。

したがって、わたしたちエンジニアには、以下をいずれも満たす都合の良い設計が求められます。

**1: レスポンスは速く** 
**2: 更新は即時に** 

2はキャッシュの思想と相反するものです。それでも、私たちはやらなければなりません。

# キャッシュは捨てられないので

とは言ってもキャッシュは捨てられません。

そこで、「**記事を編集・更新したタイミングでキャッシュを削除する**」ことになります。

ここで大切なことは

**1: 更新が必要なキャッシュは確実に消す** 
**2: 更新する必要がないキャッシュには何もしない**

ことです。

以下のキャッシュクリアのコマンド

```terminal
php artisan cache:clear
``` 

を打つと、確かに1は満たすことができます。しかし、2を満たすことができません。すべてのキャッシュが消えてしまいますので。

別の方法で、上記の要件をクリアできます。

# 狙ったキャッシュだけを消す

Laravelにはキャッシュを削除するメソッドが用意されています。

> `forget` メソッドを使用してキャッシュからアイテムを削除できます。

>```php
>Cache::forget('key');
>```


特定のキャッシュを消すためには、この `forgot` メソッドを利用して、以下のようにします。


```php:web.php
Route::match(['post', 'options'], '/flush', 'CacheFlushController@index');
```

```php:CacheFlushController.php
class CacheFlushController extends Controller
{
    /**
     * 特定のキャッシュを削除する
     */
    public function index(Request $request)
    {
        $key = $request->input('key', "");
        Cache::forget($key);

        return "Deleted cache at {$key}";
    }
}
```

バックエンドサーバーとは別のレポジトリ（例えば記事管理ツールなど）からリクエストを送信するケースを想定しています。

この `/flush` というURLに、外部からキャッシュのキーを含めたリクエストを送信すればキャッシュを削除できます。

## 例

実際には、以下のような仕組みでバックエンドにリクエストを送信できます。（PHPの場合）

```php
    /*
    *** キャッシュを削除する
    *** 引数 $key は削除したいキャッシュのキー
    */
    public function flushApiCache($key = '')
    {
        try {
            $BASE_URL = '*******';
            $url = $BASE_URL . '/flush';
            $data = ['key' => $key];

            $context = [
                'http' => [
                    'method'  => 'POST',
                    'header'  => 'Content-Type: application/x-www-form-urlencoded',
                    'content' => http_build_query($data)
                ],
            ];

            $html = file_get_contents($url, false, stream_context_create($context));
            return $html;
        } catch (\Exception $e) {
            return $e->getMessage();
        }
    }
```

あとは、記事を編集したタイミングでコントローラーからこの関数を呼び出せば良いでしょう。

もし、別のレポジトリがない（つまり、バックエンドとCMSが一体化している）という場合は、 そもそも`route.php` で `/flush` を開く必要がありません。コントローラーから直接 `Cache::forgot()` を呼び出せばそれで大丈夫です。

# キーは一意に

したがって、キャッシュのキーはなるべく一意に定めなければなりません。

```php:HogeController.php

$key = 'users'; // ここは一意に定まるように
$seconds = 60 * 60;

```

こうすることで、キャッシュは確実に消えますので、リロードすれば記事は最新状態になっています。

また、「更新が頻繁なデータであるなら、そもそもキャッシュを使うべきかどうかを考える」ということにも注意しましょう。

- キャッシュへのアクセス数
- キャッシュの更新頻度
- キャッシュはどれくらい体感速度の向上に効くのか

キャッシュの使い方はここの兼ね合いですので、サービスの仕様と相談ですね。

# バックエンドサーバーが複数台ある場合

コメント欄でご質問いただきましたので記事にて追記させていただきます。

上記の方法はざっくりまとめると
- CMSサーバーからバックエンドサーバーにリクエストを送る
- バックエンドサーバーでキャッシュを消す
という流れになっているわけですが、バックエンドサーバーが複数台（A,B）ある場合だと、特定の状況によっては上記の方法は不完全です。

「特定の状況」というのは、キャッシュのドライバ（保存先）が `file` に指定されている場合です。

```php:config/cache.php
'default' => env('CACHE_DRIVER', 'file'), // ここ
```

デフォルト値ですので、変更を加えていない場合は `file` として設定されています。

この場合、キャッシュの実態は ` storage/framework/cache` にあります。つまり、サーバーに依存しています。

厳密にいえば、サーバーAとサーバーBではキャッシュの状態が違うはずです。（初めてキャッシュが保存される時間も異なります。）

さらにいえば、 `/flush` にリクエストを送信した時、そのリクエストがサーバーAに当たれば確かにサーバーAのキャッシュは消えます。しかし、サーバーBでは依然としてキャッシュは消去されていません。

したがって、ドライバが `file` である場合のように、キャッシュがサーバーに依存している状況では解決しづらい問題となります。

そこで、**キャッシュの保存先をサーバーに依存させないようにしましょう。**

今回のように、「データの更新時にすべてのキャッシュを確実に更新したい」という要件があるケースでは、サーバーごとにキャッシュの状態が違うという設計から見直すのが良さそうです。一番お手軽に導入できるのは `database` です。

その他、本格的なドライバとして  `Redis` や `Memcached` を検討することもできます。キャッシュをバックエンドサーバーに依存させない仕組みであることが重要です。