キャッシュについて、わかりやすくまとめてみた

こんにちは株式会社Atraeでエンジニアをしている中村です。
この記事は、[Atrae Advent Calendar 2020](https://qiita.com/advent-calendar/2020/atrae) の23日目の記事です。
業務でキャッシュに触れる機会があったので、キャッシュについてお話ししようと思います。

## キャッシュとは？を簡単に
動画の中のスクリーンショットのようなものです。

例えば、牛丼屋の状況がずっと流れ続ける配信があったとして、ずっとLIVEで繋ぎ続けるのは通信が重くなります。
特に、深夜の映像はシャッターが閉まっているだけで大きな変化はありません。この時に役立つのがキャッシュ（スクリーンショット）で、夜12時の映像をスクリーンショットし、朝の開店前の6時までスクリーンショットを貼っておけば、動画をLIVEで配信し続ける必要がないため、通信もスムーズになります。

この動画を見て、お店の混み具合で牛丼を食べに来る人がいたとします。
その場合、昼間の11時30分にキャッシュ（スクリーンショット）を撮ってしまいスクリーンショットを写続けると、困ったことになります。なぜなら、昼間の11時30分の混み具合と12時30分の混み具合は全く違うからです。

このように、適切な場合に適切にキャッシュを使いこなすことが重要です。
詳細はこれから説明します。

## 今回の対象
Webアプリケーションレイヤーでのキャッシュについて取り扱う。
いわゆるキャッシュメモリ、ブラウザのキャッシュ、HTTPにおけるキャッシュ、CDN（コンテンツデリバリーネットワーク）におけるキャッシュなどとは文脈上異なるので注意してください。

整理すべきは、クライアント（Client）、サーバ（Server）、キャッシュ（Cache）、データベース（Database）の４つ。
以下のアーキテクチャをベースとしていく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/905182/7876a81b-ce8b-c3f2-266c-f9b77943fbc8.png)

## (Webアプリケーションにおける)キャッシュとは？
サーバーが返すべきコンテンツにおいて

 - １次ストレージより高速なストレージに配置しておいてそこから返す
  - アプリケーションレイヤーでのキャッシュ 
 - もしくは返さない
  - HTTPレイヤーでのキャッシュ
  - 一定時間ローカルストレージに蓄積してクライアントがアクセスしようとした時に再利用するやつ

## キャッシュに適するデータの性質
まずキャッシュとして扱うデータに求められる性質をいくつか紹介

### 消去が許容されること（データ保存の期間がある）

大前提としてキャッシュとして扱うデータは消失することが許容されている方が望ましい。キャッシュの性質上、以下のような事象や操作によって保存されているデータが消える。

 - キャッシュの有効期限（TTL）が切れる
 - キャッシュサーバを再起動する
 - 無効化（invalidate）された

などが考えられる。
このようにキャッシュの消去が発生するため、データベースから値を再取得したり、キャッシュを再生成したりする必要がある。つまり、データのライフタイムの管理をする必要がある。

### 読み込み頻度が高いこと
当然ですが頻繁に読み込まれるデータをキャッシュにする方が有利。逆に読み込み回数が少ないデータの場合は、キャッシュの恩恵は受けられないと言える。

言い方を変えると、データはユニークではない方が良い。何度も呼び出される冗長なデータをキャッシュ化することが高い効果を発揮する。

### 書き込み頻度が低いこと
データの更新が多ければ、その分キャッシュの整合性を保つためのコストがかかる。そのため更新の少ない静的なデータがキャッシュするデータとして有利。

## なぜキャッシュを使うのか？
 - リクエストを高速に返却するため
  - UXの向上
  - 高速に返却することでストレスが少なくなりUXがアップする！
 - 時間あたりの処理数を増加させるため
  - 計算機リソースの利用効率上昇（Request Per Secondの増加）
  - 単位時間あたりの処理数増加により一度にたくさんのリクエストを捌ける
 - 特定コンポーネント（SQLなどのRDBMS）の負荷低減のため
  - システムの安定運用
  - サーバーからストレージにリクエストがたくさん走って負荷が集中している場合に、キャッシュに逃してあげることで過負荷コンポーネントの負荷を軽減できる

 - DBからキャッシュをとってくることと、キャッシュからとってくるのは何が違うのか？
たくさんの名前から検索するのではなく、キーバリュー型で引っ張ってこれるので検索負荷がかからない

## どのような時にキャッシュを使うか
**どうやってもクエリが遅い**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/905182/285ff977-9eef-b9c6-3598-830953e44f14.png)

 - キャッシュに逃すことで現実的な時間で返し、負荷も分散させる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/905182/0a558ca6-a36f-44ef-4ae3-92155a8bfbc1.png)

**色々な理由で常軌を逸した量のアクセスがある**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/905182/a18eb5a7-06ab-b3ae-bb60-8c5b7911d820.png)

 - もっと高速なストレージで暴力的になんとかする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/905182/043b2656-2efd-9ad3-5e73-01d9a9c3efc7.png)

**外部サービスに依存している**
 
 - 外部サービスに都度リクエストを飛ばしていては遅い
  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/905182/f426f0c7-f8c2-3177-1efe-56fa2760ad8e.png)

  - キャッシュを仲介することで高速に結果を得ると共に不要なリクエストを削減する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/905182/fc76ad2b-3ba9-18f1-bbee-2bf87a953ac8.png)


- などなど

## キャッシュのアーキテクチャパターン例
### Cache-Aside Pattern
最もシンプルな方式で頻繁に用いられるパターン

1.  クライアントからリクエストする
2.  該当するデータが存在するかをキャッシュに問い合わせる
3.  ヒットすれば、キャッシュからデータを取得する
4.  ヒットしなければ、データベースにアクセスして取得する
5.  取得したデータをキャッシュに保存する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/905182/8e43c3ee-ef95-35e9-6414-7e47ae967778.png)


#### メリット🙆
 - シンプルで理解が容易
  - キャッシュ戦略のなかでも最もシンプルで扱いやすい。そのおかげで参考ドキュメントなども豊富。アーキテクチャに一度でも複雑性を持ち寄ると管理コストが爆発的に大きくなるので、それを最小限に抑えてくれる。

 - キャッシュとデータベースでデータ構造を変えられる
  - アプリケーション側でキャッシュとデータベースを区別しなければいけない。これを上手く利用すれば、例えばデータベースに保存している全情報のうちIDだけをキャッシュサーバに保存する、などのデータ構造の応用を効かせることができる。

 - データをWriteする場合にデータベースに書き込むだけで良い
  - つまりキャッシュに対するWriteを意識しなくてよいということ。
  - キャッシュにWriteされるのはキャッシュミスした直後。キャッシュへとWriteしたタイミングで、データを無効化（invalidate）しなければ、取得できるデータの不整合が発生するので注意が必要。

#### デメリット🙅
 - 初回のデータ取得が遅い
  - キャッシュとデータベース両方にアクセスをする必要があるので、初回データの読み込み時は遅くなることが想定される。さらにデータの追加・更新でデータベースにWriteした後に、初めてデータを取得しようとすると必ずキャッシュミスが発生する。

 - アプリケーション側でキャッシュとデータベースを区別する必要がある
  - 先述の通り、キャッシュとデータベースのうち、どちらから値を取得するかを制御する必要がある。アプリケーションコード内のあらゆる箇所でこの制御を入れようとすれば、あっという間にカオスになる。そのため、データソース層で抽象化するなどして、設計ルールを敷くことが必要。

この方式を採用するための具体的なミドルウェアとしては、RedisやMemcacheなどが利用されることが多い。


## キャッシュストレージ概観
 - Redis
 - memcached
 - BerkeleyDB
 - mmap()
 - などなど


## Redisとは？

Redisはオープンソースの永続化可能なインメモリデータベース（In-memory database）で、BSDライセンスで公開されている。Twitter・Github・PinterestなどのサービスがRedisを採用しており、大規模データの処理や処理速度の向上を実現している。
※インメモリデータベースとは、データをメインメモリ（主記憶装置、RAM）上の領域に格納するよう設計されたデータベース。

メモリ上のデータは揮発性なので、何もせずに再起動すると全て消えてしまうが、Redisはメモリを利用しながらも、メモリ上のデータを任意のタイミングでディスクに格納して保持する仕組みがある。

2つのやり方があり、
1. RDB（Database Backup File）はダンプファイルのようなものをスナップショットとして保存する方法。速度面への影響は少ないですが、この方法だと障害発生前数分ほどのデータは復旧できない。
2. 2つ目はAOF（Append Only File）という実行時の全コマンドを記録しておく方法。こちらは障害発生前1秒ほどのデータのみが失われるだけだが、代わりに実行速度は著しく低下する。

#### メリット🙆
 - Webアプリケーションの高速化
  - RedisはインメモリのNoSQLデータベースのため、RDBよりも処理速度が高速。Webアプリケーションのセッションやキャッシュの一時的な保存先に指定することで、アクセス速度が飛躍的に向上する。

 - アトミック（原子性）
  - ある操作がアトミックであるとは、複数の操作が全て実行されるかあるいは全く実行されないと保証される性質のこと。実際Redisのコマンド実行もトランザクション（複数の操作をまとめてやるもの）もアトミックなので不整合が生じない。当たり前のように思うかもしれないが、他の多くのキーバリューストアにはトランザクション機能がないのが普通。

 - データ型のサポート
  - 一般的なキーバリューストアにはデータ型がない。保存したJSONやCSVをアプリケーション側で解釈して使用。しかしRedisには以下のようなデータ型が用意されており、各言語のクライアントで取得したデータはそのまま使用可能。

#### デメリット🙅
 - 常にメモリを消費する
  - メモリ上で動作するため常にメモリを消費する。またメモリに乗り切るデータしか扱えないため、大容量のデータを一度に取り扱うような処理はできない。またメモリの断片化の問題もある。大量のデータの書き込み・削除を繰り返すとパフォーマンスが低下する。

 - データの揮発性
  - RDB・AOFの2種類の永続化機能があるとはいえ、基本的には揮発性がある。データの消失を許さないシステムではRedisを用いることはできない。


## 参考
https://codeahoy.com/2017/08/11/caching-strategies-and-how-to-choose-the-right-one/
https://buildersbox.corp-sansan.com/entry/2019/03/25/150000
https://www.codementor.io/@meysamsamanpour/caching-in-web-applications-fz1gzizpa
https://speakerdeck.com/moznion/pattern-and-strategy-of-web-application-caching?slide=148
https://itiskj.hatenablog.com/entry/2017/08/18/000000
https://agency-star.co.jp/column/redis/
https://www.slideshare.net/yujiotani16/redis-76504393
http://ccs-tech-blog.blogspot.com/2017/05/redis.html