Redisのkey構造を改善したら処理時間を1/3に短縮できた話

# はじめに
私は現在[LINE株式会社](https://linecorp.com/ja/)でサーバーサイドエンジニアとして開発業務に従事しています。今回の記事では以前勤務していた[株式会社ZUU](https://zuu.co.jp/)での開発に関して投稿したいと思います。
今回取り上げる内容は、Redisのkey構造を改善したら処理時間を1/3以下に短縮できたという話です。

# 提供サービスの概要
株式会社ZUUでは金融に関する記事を掲載する様々なメディアを運営しています。[ZUU Online](https://zuuonline.com/)や[fuelle](https://fuelle.jp/)、[MoneyTimes](https://moneytimes.jp/)、[dメニューマネー](https://money.smt.docomo.ne.jp/)、その他にも多数メディアを運営しております。
これらのメディアでは、ユーザが閲覧できるページの他に、記事の編集者や開発者などが閲覧できる管理画面（CMS）があります。管理画面から記事を投稿したり投稿予約したり、また編集したり、記事で使用される画像をアップロードしたりすることができます。

# ことの発端
あるとき外形監視でレスポンス遅延が発生している旨のエラーが出ました。外形監視は1分毎にページにアクセスし、そのレスポンスタイムが5回連続で5s以上となった場合にアラートが発せられます。
今回の対象のメディアでは記事データのキャッシュにRedisを使用しているのですが、調査してみるとRedisの負荷が増加していたことが原因でした。Redisはシングルスレッドで動作しており、Redisの負荷増加がレスポンスタイムの遅延に影響を与えていました。

# ボトルネック調査
では、なぜRedisの負荷が増加していたのか。

記事データのキャッシュとしてRedisを使用している以上、CMSで当該記事を更新した場合その記事に関するすべてのデータをクリアする必要があります。もしクリアしないと更新前の記事がユーザに表示され続けます。
結論から言うと、このクリア処理の負荷が大きいことがわかりました。

### 従来のkey構造
keyにはstringを使用していました。例えば、記事Aに関わる各種データのkeyは以下のようになっていました（実際とは異なりますが、だいたいこんなイメージのkey）。

- articleA_meta
- articleA_content
- articleA_image_url
- articleA_related_articles
- ・・・（一つの記事に対して数十のkey）

### クリア処理のフロー
1. 更新した記事に関するkey全て（上記例ではarticleA_*）をSCANで一覧で取得 →　O(N)
1. 取得された全てのkeyを DEL　-> DEL 自体は O(1)

### 負荷増のボトルネック
Redisには多くて数十万のデータがRedisに格納されており（記事以外のデータもあります）、O(N)の処理1がネックになっていました。記事を更新するたびに処理1が実行されます。CMSでたった1文字でも記事を更新すると、数十万のデータをSCANする必要があります。そして、このCMS作業でのRedis負荷増が、ユーザ画面へのレスポンスタイムの遅延につながっていたということがわかりました。

# 応急対応
- てっとりばやく運用でカバー、記事編集者さんに更新ボタンのクリック頻度を下げてもらうようにしてもらっていました。
- 1時間ごとにflushdbしてデータがたまり続けることを抑止していました。（たしか、cronjobで実行していたと思います。）

これらでRedisへの負荷がある程度軽減できます。

# 恒久対応（本記事の本題）
### アプローチ
前述した通り、処理1(SCANで関連する全てのkey取得）がボトルネックでした。そこでkeyにstringを使うのではなく<font color="red">hash</font>を使うように変更しました。
hashを使うとある記事に対する関連データを１つのkey（articleA）に集約しfieldを使うことで階層構造にすることができます。

stringからhashへの変更は以下の通りです。

<strong>string(key)</strong>
- articleA_meta
- articleA_content
- articleA_meta
- articleA_meta
- ・・・

<strong>hash(key, field)</strong>
- articleA
    - meta
    - content
    - image_url
    - related_articles

では、stringを使っていた時に実施していた SCAN & DEL が、hashを使うことによってどのように変わるでしょうか。
ズバリ、DEL一発です！！！
```
DEL articleA
```
これでarticleA以下のfieldの全てのデータ、つまりarticleAに関するデータを全て削除できます。処理はO(1)です。かなり期待できます！！！

### 実装
（本題とは話がずれてしまうのでここでは詳しく説明しませんが、結構大変でした。記事の最後で紹介します。）

### 結果
CMSでの記事更新には、従来のstringを用いたkey構造では30秒程度要していましたが、hashを使用したところ10秒以下に低減され、処理時間を<font color="red">1/3以上カット</font>することができました。（O(1)になったのでもっと短縮できそうですが、実は実装が広範囲で時間を要したため、私が退社するまでに全てをhash化できなかったです。）
この効果で外形監視でもエラーが出なくなりました！！

# おわりに
「[SQL改善で処理時間を約98%カットできた話](https://qiita.com/genta-kawabata/items/e631c77a3e7969a2100f)」でも似たようなことを述べましたが、Redisパフォーマンスのサービスへの影響を把握できるのはエンジニアだけだと思います。サービスを運用する運用チームに対してエンジニアチームから課題をボトムアップし、それを改善してユーザへのレスポンスタイムを向上させ、しっかりと事業にコミットできたということはエンジニアとして大きな経験にもなりますし大きな喜びを感じることができました。。
当時ボトルネック調査には同僚のA.NさんとR.Mさんが大きく貢献してくださいました。また大量の差分を丁寧にレビューしてくれて感謝です。

# （付録）実装での苦労話
Redisを使用している部分は広範囲に及び影響範囲がかなり多く、リファクタリングによるデグレの危険性がありました。それを防ぐため、リファクタリング対象の全てにユニットテストを作成して、安全に実装を進めました。（当然っちゃ当然だけど、とにかく実装量が半端なかった・・・）
また、Redisへのアクセスはinterface化されていなかったので、テストを書きやすくするために全てinterface化しました。（手前味噌ですが、「[iterfaceとテストに関する記事](https://qiita.com/genta-kawabata/items/6a4735cd3a163926c332)」も投稿しています）
さらに、ビジネスロジックレイヤではRedisのkey構造を意識した実装になっていたので、そこもinterfaceを利用し隠蔽することにしました。
これらの実装を、とにかく広範囲に及んで実装したので大変な作業になりましたが、いい経験になりました。