Rails
MySQL

Railsのcounter_cacheを使ったらdeadlockが頻発した

More than 5 years have passed since last update.

rails (3.2.3)

activerecord (3.2.3)

mysql2 (0.3.11)

ちょっと遡って話をしたほうがいいのかも。


前提:Mysql(InnoDB)でcount(1)はテーブルスキャンが発生して遅い

非力なインスタンスを使っていたので、

数十万件ってレベルからこれがボトルネックになっていた。

そのために、Railsが装備しているcounter_cacheという機能を使う。

ただ、counter_cacheってのは関連するモデルの件数を覚えておくっていう方法で、

テーブルの全レコードの数を保存しておく方法ではない。

それをしたいなら別の実装を。

参考:A Guide to Active Record Associations


counter cahceってなんだ

繰り返しになるけど、関連するモデルの件数を覚えておく。

class Order < ActiveRecord::Base

belongs_to :customer, :counter_cache => true
end
class Customer < ActiveRecord::Base
has_many :orders
end

例えば、以下のようにしたときに、

count(*)なクエリーが発行されず、キャッシュから件数が導かれる。

@customer.orders.size

#=> 100

設定の書き方が直感的じゃないので違和感を感じるかもしれないです。

上記のような書き方をした時に、

件数を記憶しておくカラムはcustomersテーブルのorders_countというカラムになる。

カラム名を変えたいのならば、:counter_cacheにbool値ではなく、カラム名を渡せばOK

class Order < ActiveRecord::Base

belongs_to :customer, :counter_cache => :count_of_orders
end
class Customer < ActiveRecord::Base
has_many :orders
end

ここまではドキュメントをほぼ和訳しただけ笑


counter cache早いけど、あれ、deadlock...

"counter cache"を使うと毎回select count(*)が発行されないので早いです。

特にpagingしてるときとか、都度件数とる必要があるので体感出来るほど早くなりました。

(というか、改善前は数十秒かかることもあって使用が困難だった)

この通り、読み込みするときは良かったのですが、

書き込みがちょっと残念でした。

そのモデルの追加はresqueで遅延処理していたのですが、

workerが2-4程度でもdeadlockが発生してしまいました。。


解決方法を調べる

Mysql側を深掘りすると泥沼な気がしたので、

railsのcounter cacheを使ってdeadlock発生してる人居ないかなー?って探したら一応いた。

Rails Counter Cache Updates Leading to MySQL Deadlock

PostgreSQL transactions wrapping child+parent modifications, deadlocks, and ActiveRecord

けど、ちょっと解決策からは遠い気がした。

そしたら素敵なgem発見。

bestvendor / counter_culture

これ使ったら治りました。ハイ。


counter_cultureの仕組み

どうやってdeadlockを起きないようにしているかっていうと、

counter cacheの更新をトランザクションをcommitした後にしているということ。

当然のごとく、そのトランザクション直後になんらかの障害が起きた場合は不整合が発生することは容易に予想がつく。

けど、そこまでの精度を求めない場合や、

都度backgroundでcountしなおしたり(そういう手段をcounter_cultureは提供している)すれば見逃せる場合はこのgemはとてもカッコイイ。

ということで、ひとまずこれで代用することにしました。

今のところdeadlockの再発はないのでいいかな。