Redisで100万件のデータを0.3秒で読み込む

## まえがき

梅雨のせいでコインランドリーで乾燥機を回している時ふと、
「どれくらいのデータ量だとRedis(キャッシュ)導入に価値がある？
だって、一般的な処理だと早くなっても数ミリ秒の世界だし」と考え始めました。(唐突)
とりあえず100万件のデータでRedis導入にどれ程の価値があるのか、いざ検証してみます。

## 検証環境

- ローカルPC (RAM 8GB)
- Redis 6.0.5 (maxmemory 0=無制限)
- Rails 6.0.3 (API mode)
- MySQL

※ 今回はDBに存在する100万件のレコードをそのままRedisに突っ込みます。

## 100万件の投稿データ作成

今回検証のアプリケーションとしてRailsを使うので[seed ※](https://railsdoc.com/page/rake_db_seed)でデータ投入しても良かったんですけど、何分遅いのでストアドプロシージャでinsertしていきます。

```sql

delimiter //
 create procedure bulkinsert()
 begin
    SET @count = 0;
    SET @limit = 1000000;
    WHILE @limit > @count DO
      INSERT INTO comedians (name, created_at)
      VALUES ('渡部 建', now());
      SET @count = @count + 1;
    END WHILE;
 end
//
delimiter ;
call bulkinsert();
DROP PROCEDURE IF EXISTS bulkinsert;
```

``` sql:count
[rails_dev_redis]> select count(*) from comedians;
+----------+
| count(*) |
+----------+
|  1000000 |
+----------+
1 row in set (0.26 sec)
```

はい、100万件データがありますね。

## model.to_aの実行結果をキャッシュ
キャッシュの引数にActiveRecordを渡すと、select文だけキャッシュするので、to_aで。
技術ブログによくあるパターンですね。

``` ruby
Comedian.all.to_a
```

### 計測結果

```
Processing by ComediansController#benchmark as HTML
       user     system      total        real
Rails_Cache_Fetch 38.901311   2.737989  41.639300 ( 52.607612)
Completed 200 OK in 52610ms (Views: 0.7ms | ActiveRecord: 0.0ms | Allocations: 14003982)
```

ご、52秒...。（´-`）.｡oO（なぜだ）

## Redisのデータの読み込みが遅い問題

https://redis.io/topics/data-types
> A String value can be at max 512 Megabytes in length.

Redisの公式ドキュメントにあるようにstring型は512MBまでしかサポートされていない。
ここに原因があるのではないかと考え、サイズを調べてみることに。

### 調査結果

```
Processing by ComediansController#benchmark as HTML
Rails_Cache_Fetch length: 2401298
Completed 200 OK in 25ms (Views: 0.9ms | ActiveRecord: 0.0ms | Allocations: 329)
```
2.4MBしか使ってないので、sizeの問題ではなさそう。

Redis側のデータを確認してみると、バイナリ文字が入っている。
Rails.cacheがバイナリから正規化する時間分だけ、遅くなっているのかもと考え、
redis.setでjsonを突っ込んでみることに。

### 計測結果

```
Processing by ComediansController#benchmark as HTML
       user     system      total        real
Redis_Get  7.533286   0.696594   8.229880 ( 10.452844)
Redis_Get length: 86100083
Completed 200 OK in 12306ms (Views: 0.4ms | ActiveRecord: 0.0ms | Allocations: 7000593)
```

10秒か〜。早くはなったんですけど、こういうことじゃないんですよね。
データの中身がバイナリじゃないので、8600万文字入ってます。

## 試行錯誤の末、高速化の成功

検索しても打開策がなかったので、色々試してみることに。
key-valueを複数もつパターンもやってみましたが、結局ループするとその分だけ遅いんですよね。

試行錯誤した結果、Rails.cacheでjsonを突っ込むと高速で参照できることがわかりました。

以下3パターンのRead/Writeの速度計測結果を貼ります。

| オブジェクト・関数|形式|ベンチマーク上の表記
|:-----------------|------------------:|:------------------:|
| redis|json|Redis_Set・Redis_Get|
| Rails.cache|json|Rails_Cache_Write_Json・Rails_Cache_Read_Json|
| Rails.cache|Array|Rails_Cache_Write_Array・Rails_Cache_Read_Array|

### Set時の速度計測結果

```
Processing by ComediansController#benchmark as */*
       user     system      total        real
Redis_Set  Comedian Load (968.2ms)  SELECT `comedians`.* FROM `comedians`
  ↳ app/controllers/comedians_controller.rb:9:in `block (2 levels) in benchmark'
134.868704   4.608951 139.477655 (150.200997)
Rails_Cache_Write_Json  CACHE Comedian Load (0.0ms)  SELECT `comedians`.* FROM `comedians`
  ↳ app/controllers/comedians_controller.rb:12:in `block (2 levels) in benchmark'
117.643508   2.911988 120.555496 (121.557569)
Rails_Cache_Write_Array  CACHE Comedian Load (0.0ms)  SELECT `comedians`.* FROM `comedians`
  ↳ app/controllers/comedians_controller.rb:15:in `block (2 levels) in benchmark'
 39.717218   1.970096  41.687314 ( 41.873064)
```

意外にもArrayが一番早いです。

### Get時の速度計測結果

```
       user     system      total        real
Redis_Get 12.294169   0.375882  12.670051 ( 12.914826)
Rails_Cache_Read_Json  0.196984   0.170824   0.367808 (  0.321568)
Rails_Cache_Read_Array 24.414510   1.228965  25.643475 ( 25.746937)
Get_DB  CACHE Comedian Load (0.1ms)  SELECT `comedians`.* FROM `comedians`
  ↳ app/controllers/comedians_controller.rb:30:in `block (2 levels) in benchmark'
117.189727   4.600185 121.789912 (122.321526)
```

Rails_Cache_Read_Jsonだと**0.3秒で100万件を読み込めています。**
Get_DB(122秒)はDBから直接fetch allした場合の速度で、比較の為に入れてます。

### 文字数のカウント

```
Redis length: 86099997
Rails_Cache_Json length: 2744225
Rails_Cache_Read_Array length: 2399348
```

redis.setでjsonを格納した場合、8600万文字入っていたのに対し、
Rails.cacheは**270万文字と驚異的な短さ**です。
バイナリで保存しているのは容量の面で非常に有利ということですね。

## あとがき
「どれくらいのデータだと導入に価値があるか」という本題でしたが、
100万未満のデータ量で参照に1秒以上かかってたら導入の価値はあるかなと思います。
たった4行のコード追加だけで、100万件読み込むのに1秒切れると思っていなかったので、ただただ脱帽しております。

ありがとうサルバトーレ、ありがとうここまで読んでくれた読者。