鳩舎

レースしない

Sequelでモデルキャッシュできるライブラリ書いた

Sequelってなんかそこそこ使われてる印象だったんですけど、マジで皆さんプロダクションとかで使ってます?

Sequel + Redisでモデルキャッシュしようとしたらまともに動くライブラリないじゃないですかヤダー!と思って探しに探したんですが、どうにも見つからない。

見つけたSequelで使えるキャッシュライブラリは

  • Sequel::Plugins::Caching
    • Memcache前提っぽいインターフェース。っていうか primary_key lookup しかキャッシュしないじゃん!
    • updateしたらリストアされるのかと思ったらキャッシュ消しただけで終わりやがった!すっきりした面してんじゃねぇよぶん殴るぞ。
    • コードはこちら
    • ていうかこれサンプル扱いなんでは
  • sequel_act_as_cacheable
    • リポジトリ 最終更新6ヶ月前
    • primary_keyしかキャッシュしない。Sequel界隈ってなんか男らしいのが流行ってんの?オレはクエリキャッシュもして欲しいんだけど。せめてオプションで有効にしたい。
    • RedisなんてSequelの世界にはない
    • Model[id]の時はキャッシュされる、されるが、Model.find(id)はキャシュしない。えっ、マジこの人何いってんの……

これ以外にあったら教えてください。そっち使います。

とりあえず

  • Redisが使えるキャッシュライブラリがない
  • クエリキャッシュなんてSequelの男らしい世界にはない

ということがわかったので、とりあえずそれができるライブラリを書きました。一応テストしてますが、いろんなタイプのカラムあたりが怪しいです。バグ見つけたらgithubのissuesにください。

sequel-cacheable

Githubにあがってます。rubygemsにもあがってるので、gem install sequel-cacheableで使えるはずです。

とりあえずの使い方
Sequel::Model.plugin :cacheable, Redis.new(host: 'localhost', port: 6379)

動いた。とりあえずキャッシュクライアントにはRedisとMemcacheどっちも使えるようになっています。Memcacheの方はクエリキャッシュがデフォルトで無効です。keysメソッドがないのでどうしたらいいかわからんかった。

クエリキャッシュが効かなくてもモデルキャッシュは効きます。primary_keyでひっぱったらどういう書き方だろうととにかくキャッシュを見に行きます。

Model[id]
Model.find(id)
Model.where(:id => id).limit(1)

だいたいこのへんは全部モデルキャッシュに飛ぶはず。updateしたりするとキャッシュを更新します。createするとDBにinsertすると同時にキャッシュを作ります。嬉しいですね。

クエリキャッシュはモデルキャッシュが更新・削除されると対象モデル内のものは全部消えます。本当は対象レコードを含むクエリだけ殺したかったんですが、クエリによっては新しく含まれることもあるので考えた末やめました。クエリはそのままキーになっているので、本当なら合致しているか見て消したりできるだろうけど、全部消したほうが速かった。

って書いてて思ったけど関連テーブル系の挙動怪しそうだなぁ……

んで、肝心のクエリキャッシュなのですが、なんかほっといても勝手にがんがんキャッシュされます。

具体的にどれくらいキャッシュされるかっていうと、select一発1キャッシュぐらいキャッシュします。キャッシュの中身はヒットしたレコードのprimary_keyが入ってます。なのでprimary_keyを含まないselectをするとキャッシュできません。

キャッシュが落ちた時なんかの為にprimary_keyを保存する機構になっていて、例えば別のバックエンドシステムから特定のレコードを更新して、それのモデルキャッシュをぶっ飛ばした時、クエリキャッシュの中に復元可能なレコードの古いデータがあると困ってしまうので、キャッシュが消えていたらそのレコードだけ再度取りに行くような構造です。

んで、肝心のキャッシュの中身なのですが、すべてMessagePackで入っています。Marshalなオブジェクトだったりをstringfyしたものを入れても良かったんですが、圧縮してたほうがよかろう、というのと、Timeオブジェクト周りの挙動が不安だったのでMessagePackで圧縮したHashが入っています。

んでTimeのあたりがまた鬼門なのですが、MessagePackにはTimeがありません。なので、Time.to_iとTime.usecの配列が入っています。このへんはカラムの型情報を見て取り出すときに変換を自動でかけています(元はto_iだけ入れてたけど、取り出した時に一致しなくて1時間くらいハマった)。

そういや元あった2つのうちどっちかがMarshalオブジェクトを突っ込んでてto_sされた結果になってて、#とか帰ってきてパースできなくてキャッシュの意味をなしてなかったな……

クエリキャッシュは現時点ではRedisにストアする時だけの機能です。理由は2点あって

  • Memcacheにkeysがないのでクエリキャッシュのみを狙ってFlushさせることができない(力技で対応可能だが、それもしかしてクエリキャッシュしないほうが速いのでは)
  • クエリキャッシュのid配列からオブジェクトを取り出すmultiple getがMemcacheにない(これも何回か回せばいいし、現に最初はそうしてたけど、それもやっぱキャッシュしないほうが速いのではとか思う……)

というような状況です。

オプション

Sequel::Model.plugin :cacheable, Redis, option

第3引数にオプションのハッシュが渡せます。詳しいところは多分コードを読むのが一番です。そう大きなコードでもないので、気が向いたらどうぞ。

ttl

time to liveです。キャッシュの生存時間を設定します。デフォルトは3600秒(他ライブラリに倣って)。秒単位で数値指定します。

ignore_exception

キャッシュを取り出すときに例外が発生しても無視するという設定です。デフォルトはfalse。

キャッシュサーバーが死んでる時なんかに使われることになると思います。

pack_lib

MessagePackが渡っている、キャッシュにストアする時にpackするライブラリを選ぶ部分です。packとunpackのあるオブジェクトなら大抵なんでも入ります。ちなみにここにnilをいれるとまともに動かなくなります。これは将来的に治る予定。

query_cache

クエリキャッシュするかどうかをBooleanで指定します。Memcacheクライアントの時はデフォルトOFF、Redisの時はデフォルトONです。前述したとおり、現時点ではあまり正確な挙動ではないので、今はオフにしておくのが正解かもしれません。

ということで

バグみつけたら教えて下さい!!!!