ロストアップデート発生!!
トランザクション分離レベルをrepeatable_read(MySQLのデフォルト)にしてロストアップデートを発生させてみる。
ワーカー
class HogeWorker
@queue = :hoge
def self.perform
ActiveRecord::Base.transaction(isolation: :repeatable_read) do
h = Hoge.find_by(id: 1)
sleep(3.0)
h.count = h.count + 1
h.save!
end
rescue => e
# ここを通ることはなかった
Resque.logger.info('ERRORRRRRRRRR s')
Resque.logger.info(e.class)
Resque.logger.info(e)
Resque.logger.info('ERRORRRRRRRRR e')
end
end
ワーカー呼び出し
100.times do
Resque.enqueue(HogeWorker)
end
ワーカー起動
ワーカーが1つだと綺麗に1つずつ処理されるので、ロストアップデートが起こらなかった。
なので、ワーカーは5つ立ち上げた。
VVERBOSE=1 bundle exec env rake resque:workers QUEUE='*' COUNT='5'
結果
countカラム
が0
から始まった場合、本当は100
になっているところ、ロストアップデートが発生したので、21
になっていた。
具体的なSQLとしては、下記のように+1
した結果が19
になっているUPDATE文
が何回も発行されていた(同様にして18(などの他の数字も)
のものも複数発行されている)。
UPDATE `hoges` SET `count` = 19, `updated_at` = '2016-06-28 07:09:22' WHERE `hoges`.`id` = 1
ロストアップデートが発生しないように、SELECT FOR UPDATEでロックしてみる
find_by
のところで、ロックさせてみる。
c = Hoge.find_by(id: 1).lock!
このようにしたとき、count
を0
から始めたとき結果は100
になっていた。
おそらく、SELECT FOR UPDATEがうまく機能した例といえると思う。
ちなみに、トランザクション分離レベルで最も最強の(まったく制約を緩めていない)serializable
を指定してみると...
class HogeWorker
@queue = :hoge
def self.perform
ActiveRecord::Base.transaction(isolation: :serializable) do
h = Hoge.find_by(id: 1)
sleep(3.0)
h.count = h.count + 1
h.save!
end
rescue => e
# ここを通ることはなかった
Resque.logger.info('ERRORRRRRRRRR s')
Resque.logger.info(e.class)
Resque.logger.info(e)
Resque.logger.info('ERRORRRRRRRRR e')
end
end
このように制約をまったく緩めない最強レベルのserializable
を指定した場合は、
ActiveRecord::StatementInvalid
という例外
が発生する。
Mysql2::Error: Deadlock found when trying to get lock; try restarting transaction: UPDATE 以下略
といったメッセージもログに添えられていた。
ちなみに、serializableの場合は lockしてもlockしてないときと同じく例外発生した
serializable
でHoge.find_by(id: 1).lock!
としてみたが、
ActiveRecord::StatementInvalid
となりMysql2::Error: Deadlock
が複数発生した。