Chapter 01

はじめに 〜SQLキャッシュにまつわる失敗〜

Shigeru Nakajima
Shigeru Nakajima
2021.07.11に更新

Ruby on RailsにはSQLキャッシュという機能があります。僕は、普段は意識せずに使っていました。ある体験からSQLキャッシュの存在を意識するようになりました。

次のRubyのソースコードを見てください。

@job = Job.find_by active_job_id: active_job.job_id
if @job.nil?
  sleep 0.1
  @job = Job.find_by active_job_id: active_job.job_id
end

raise "Could not find its job object" if @job.nil?

一回データベースを検索し、0.1秒待ってから再検索します。おそらく別のスレッドかプロセスからデータベースにジョブ情報が書き込まれるのを待っているのでしょう。SQLキャッシュのことを意識していれば、この時点で「変だな?」と思ったはずです。このソースコードで正常に動いていたため、特に何も考えませんでした。

このソースコードはDelayedJobで書かれたジョブです。最近はDelayedJobの開発があまり活発でないので、バックエンドをDelayedJobからSidekiqに変えてみました。すると6行目で例外が頻発するようになりました。ただ、環境によって起きたり起きなかったりでした。環境に依存していたので、最初はデータベースのトランザクションを疑いました。「オートコミットに失敗している?」や「トランザクション分離レベルの設定が違う?」を疑い、設定を確認しました。しかし、それらは原因ではありませんでした。

ふとRuby on RailsにはSQLキャッシュがあるはずでは?と思いつきました。そう思ってソースコードを読み直すと、SQLキャッシュがあるなら、途端に無意味に思えてきます。

いままでの解釈は次のコメントの通りです。

# SQLクエリを実行して結果が得られる
@job = Job.find_by active_job_id: active_job.job_id
if @job.nil?
  sleep 0.1
  # SQLクエリを実行して結果が得られる
  # => データベースの値が更新されていれば、新しい値が得られる
  @job = Job.find_by active_job_id: active_job.job_id
end

raise "Could not find its job object" if @job.nil?

SQLキャッシュを意識すると、次のように解釈できます。

# SQLクエリを実行して結果が得られる
@job = Job.find_by active_job_id: active_job.job_id
if @job.nil?
  sleep 0.1
  # SQLクエリは実行されない。キャッシュに残っている、前回のSQLクエリの実行結果が得られる
  # => データベースの値が更新されていても、得られる値は古いまま
  @job = Job.find_by active_job_id: active_job.job_id
end

raise "Could not find its job object" if @job.nil?

SQLクエリの実行結果がキャッシュされているなら、何秒待っても二つのfind_byの結果は変わりません。当然、例外がよく起きるはずです。では、なんで今までは正常に動いていたのでしょうか?急に不安になってきました。今まで気がついていなかっただけで、すでに正常に動いていなかったでは?

落ち着きましょう。冷静に考えるとそんなことはありません。あるときから明確に挙動が変わりました。その原因を今調べているのです。不思議なのは、今まで動いていたソースコードが、ソースコード自体を変更していないのに、動かなくなったのです。そう。変えたのは、ソースコードではなく。Jobのバックエンドです。JobのバックエンドをDelayedJobからSidekiqに変えました。これが原因でした。

DelayedJobではSQLキャッシュは働きませんが、、SidekiqではSQLキャッシュが働きます。
(ここは厳密には嘘です。DelayedJobでもActiveJobを経由するとSQLキャッシュが働きます。)

SQLキャッシュへの僕の認識は次でした

  1. キャッシュにヒットしたら、データベースへの問い合わせを省略できて、高速化できる
  2. Ruby on Railsではデフォルトで有効だし、有効にするか無効にするかすら考えなくてよい
  3. とくにペナルティーのない便利機能
  4. まれに、ActiveRecordオブジェクトが古いことがあるので、そのときにreloadすれば良い

この事件から、Ruby on Railsを使いこなすには「SQLキャッシュ」について理解しておかなくてはいけないな、と思って書いたのがこの本です。