RailsからMemcachedを使いつつその実体を垣間見てみる

この記事は[こちらのブログ記事](https://www.lanches.co.jp/blog/11245)の再掲です。

# TL;DR
- Railsの`ActiveSupport::Cache::MemCacheStore`からMemcachedに書き込んだデータを他から使うのはちょっと難しそう
- 逆にMemcachedに入ってるRAWなデータをRails側で使うのは問題なさそう
- キーの長さ（250バイト制限）を気にしなくても大丈夫

---

RailsからMemcachedと言っても`config.cache_store = :mem_cache_store`的な話ではなく、任意のデータをMemcachedに突っ込んだら何がどうなってるのかを調べてみた感じのものです。

まずはMemcacheにアクセスするためのインスタンスを作ります。

```
[7] pry(main)> a = ActiveSupport::Cache::MemCacheStore.new('localhost:11211', expires_in: 60, namespace: 'NS_ABC', compress: true)
=> #<ActiveSupport::Cache::MemCacheStore:0x00007fc807dba2a0
 @data=#<Dalli::Client:0x00007fc807db9e40 @options={}, @ring=nil, @servers=["localhost:11211"]>,
 @options={:namespace=>"NS_ABC", :expires_in=>60, :compress=>true}>
```

かなり恣意的に`namespace`と`compress`を指定していますが後ほど触れていきます。

さて、データをキャッシュさせたり取り出したりするのはものすごくカンタンです。

```
[9] pry(main)> a.write('key', 'val')
=> 5044031582654955520

[13] pry(main)> a.read('key')
=> "val"
```

存在しないキーで参照した場合は`nil`が返ります。

```
[8] pry(main)> a.read('abc')
=> nil
```

さて、基礎的なお話はこれくらいにして、じゃあ実際にMemcached側にどんな感じでデータが入ってるのか、Memcachedに直接繋いで確認してみることにします。

```
[root@localhost ~]# telnet localhost 11211
Trying ::1...
Connected to localhost.
Escape character is '^]'.
```

はい、これだけのためにわざわざtelnetクライアントをyumでインストールしました。
イマドキはtelnetコマンドが入っていないのがフツーかもしれません。

```
stats items
STAT items:1:number 1
STAT items:1:age 232
STAT items:1:evicted 0
STAT items:1:evicted_nonzero 0
  :

stats cachedump 4 1
ITEM NS_ABC:key [114 b; 1545367943 s]
END
```

はい、やっと見つけました。
どうやら実際に指定したキーの前に`namespace`が自動的に追加されるようです。

せっかくなので値も取得してみましょうか。

```
get NS_ABC:key
VALUE NS_ABC:key 1 114
o: ActiveSupport::Cache::Entry  :
@version0:@created_atf1545877723.9305558:@expires_in6e1
END
```

おっと。
良く分からないものが表示されました。
が、最初の`o:`はオブジェクト、その後に型（クラス）があって、`@expires_in`の`6e1`は60の指数表記でしょう。
つまり`ActiveSupport::Cache::Entry`にデータを詰め込んでいい感じのシリアライズをすると同じようなものが作れそうです。

```
[29] pry(main)> b = ActiveSupport::Cache::Entry.new('val', expires_in: 60)
=> #<ActiveSupport::Cache::Entry:0x00007fc8072b3730 @created_at=1545878427.2002478, @expires_in=60.0, @value="val", @version=nil>
```

あ、なんかそれっぽいのができました。

シリアライズはどうやら`Marshal`というクラスを使うのがスタンダードっぽいのでとりあえず試してみます。

```
[32] pry(main)> Marshal.dump(b)
=> "\x04\bo: ActiveSupport::Cache::Entry\t:\v@valueI\"\bval\x06:\x06ET:\r@version0:\x10@created_atf\x171545878427.2002478:\x10@expires_inf\b6e1"
```

あ、概ね同じに見えますね。
ちょっとそのまま表示してみましょうか。

```
[33] pry(main)> puts Marshal.dump(b)
o: ActiveSupport::Cache::Entry  :
@version0:@created_atf1545878427.2002478:@expires_in6e1
=> nil
```

ビンゴですね。

ビンゴなのはいいんですが、`ActiveSupport::Cache::MemCacheStore`からMemcachedに入れたデータを他で利用するのはちょっと難易度が高そうです。

ところで逆はどうでしょう？
Memcachedに直接データを入れてRails側で参照してみます。

```
set NS_ABC:hoge 0 60 8
hogehoge
STORED

[36] pry(main)> a.read('hoge')
=> "hogehoge"
```

はい、取得できました。

今度はMemcached側でデータを圧縮しつつ入れてみます。

```
set NS_ABC:hoge 1 60 8
hogehoge
STORED

[40] pry(main)> a.read('hoge')
=> nil
```

あらら、これはダメなようです。

`ActiveSupport::Cache::MemCacheStore`の`compress`はここでは関係がないようです。
`compress`に`true`を指定した時は、データ容量が特定の閾値（`compress_threshold`で指定も可能）を超えた際に圧縮するという設定のようです。
やってみましょう。

```
[68] pry(main)> a.write('key', 'val' * 1000)
=> 72057594037927936

get NS_ABC:key
VALUE NS_ABC:key 3 217
.-(�/*��rNL�H��r�+)���v(K�)MU2������fw�]Ԑr]�!һ(��y�q��)
                                                                                C��+^��Ԣ<+���ĒԔCSS

                                                                                                            cCCK=
                                                                                                                 3
                                                                                                                  3C��Ԋ�̢��TC+Q�������&��Y8<��vpQ4�F�(E�h��A�ج�\C@��5%��0
�END
```

あばばばばばばば。
良くは分かりませんが圧縮されてそうです。

きっと`ActiveSupport::Cache::Entry`をシリアライズしたものを圧縮してるんでしょう、という予測を立ててみます。
圧縮と言えばみんな大好き`Deflate`でしょう。
やってみます。

```
[147] pry(main)> puts Zlib::Deflate.deflate( Marshal.dump( ActiveSupport::Cache::Entry.new('val' * 1000, expires_in: 60) ) )
.-(�/*��rNL�H��r�+)���v(K�)MU2������fw�]Ԑr]�!һ(��y�q��)
                                                                                C��+^��Ԣ<+���ĒԔCSS

                                                                                                            #
                                                                                                             =CKS3��Ԋ�̢��TC+Q�������&��Y8<��vpQ4�F�(E�h��A�ج�\C@��5%A�0
�=> nil
```

あばばばばばばばばばばば。
制御コードまみれなので全く同じ表示にはなりませんがきっとこれで合ってると思います（ぉぃ

さて、Memcachedと言えばキーに250文字制限があるのですがその辺りもちょっと確認しておきましょうか。

`namespace`の指定がある場合

```
[45] pry(main)> a.write('1234567890' * 30, 'val')
=> 5692549928996306944

stats cachedump 8 1
ITEM NS_ABC:12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456:md5:116b331c40362c11c387c7bf0eaf8295 [114 b; 1545369320 s]
END
```

`namespace`の指定がない場合

```
[52] pry(main)> a.write('1234567890' * 30, 'val')
=> 5764607523034234880

stats cachedump 8 1
ITEM 123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123:md5:874fa981137f49f7885e1d05a93c21df [114 b; 1545369598 s]
END
```

なるほど、通常通り「`namespace`:指定したキー」にしつつ、250文字を超えないように後ろにそれぞれのMD5値

```
[62] pry(main)> Digest::MD5.hexdigest('NS_ABC:' + '1234567890' * 30)
=> "116b331c40362c11c387c7bf0eaf8295"

[63] pry(main)> Digest::MD5.hexdigest('1234567890' * 30)
=> "874fa981137f49f7885e1d05a93c21df"
```

を自動で追加してくれるんですね。
これは便利。