1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MySQLのテーブルロックについて整理してみた

Last updated at Posted at 2025-12-11

はじめに

Railsアプリケーションでロック系エラーが発生し、処理が止まっている事象を確認しました。
テーブルロックの解像度が低い状態だったので、自分の理解度を上げるために調べたことをまとめます。

なにがあったのか

以下のエラーが発生し、処理が止まっていました。

ActiveRecord::Deadlocked: Deadlock found when trying to get lock; try restarting transaction
ActiveRecord::LockWaitTimeout: Lock wait timeout exceeded; try restarting transaction

どのテーブルが原因なのかわからず、再現もできませんでした。とりあえずログを仕込んで、次発生したときに検知できるようにしました。
また、インフラにはAWS Auroraを使用しているため、Performance Insightからもロックの状況が確認できそうです。

2つのエラーの違い

Deadlock Lock Wait Timeout
原因 2つのトランザクションが互いのロック待ち 単純にロック待ちが長すぎ
挙動 即座に片方がロールバック 50秒(デフォルト)待ってエラー

ロックのかかり方

トランザクション内で複数テーブルを操作する場合、各テーブルへのロックは UPDATE/INSERT/DELETEが実行された時点で順次かかり、COMMITまで全て保持されます

ActiveRecord::Base.transaction do
  user = User.find(1)           # ロックなし(SELECT)
  user.update!(name: "hoge")    # ← Userロック取得
  
  order = Order.find(10)        # ロックなし(SELECT)
  order.update!(status: "paid") # ← Orderロック取得(Userもまだロック中)
  
  Payment.create!(order: order) # ← Paymentロック取得(User, Orderもまだロック中)
  
end                             # ← COMMITで全テーブルのロック解放
時点 User Order Payment
user.update! 🔒 - -
order.update! 🔒 🔒 -
Payment.create! 🔒 🔒 🔒
COMMIT 解放 解放 解放

トランザクションが長くなるほど、最初に触ったテーブルのロック時間も長くなります。

SQL ロック
SELECT なし
SELECT ... FOR UPDATE 排他ロック
UPDATE / DELETE / INSERT 排他ロック

インデックスがないカラムでWHEREすると、テーブルスキャンが発生し、スキャンした全行に行ロックがかかります。(実質テーブル全体がロック状態に)

分離レベルとギャップロック

MySQLのデフォルトは REPEATABLE READ です。この設定だとギャップロック(行間のロック)が有効になっています。

READ COMMITTED にするとギャップロックが無効になり、デッドロックが起きにくくなります。

# config/database.yml
production:
  variables:
    transaction_isolation: 'READ-COMMITTED'

ロック状況の確認方法

rescueでロギング

rescue ActiveRecord::Deadlocked, ActiveRecord::LockWaitTimeout => e
  Rails.logger.error "[LOCK] #{e.class}: #{e.message}"
  Rails.logger.error "[LOCK] #{e.backtrace.first(5).join("\n")}"
  
  # InnoDBステータスも取れたら取る
  begin
    status = ActiveRecord::Base.connection.execute("SHOW ENGINE INNODB STATUS").first
    Rails.logger.error "[LOCK] InnoDB: #{status['Status']}"
  rescue
  end
  
  raise
end

SQL

-- 最後のデッドロック詳細
SHOW ENGINE INNODB STATUS\G

-- 全デッドロックをログに出す
SET GLOBAL innodb_print_all_deadlocks = ON;

-- 現在のロック待ち
SELECT * FROM performance_schema.data_lock_waits;

対策メモ

  • トランザクション内で外部API呼ばない(長くなる)
  • 複数テーブル更新時は順序を統一する
  • WHERE句で使うカラムにインデックス張る
  • リトライ処理を入れる(transaction_retry gem など)

まとめ

今回はロギングを仕込んで次回の検知に備えましたが、本来はそもそもロック系エラーが発生しないようにトランザクションを設計していきたいです。

トランザクションはできるだけ短く、更新順序は統一する。このあたりを意識していこうと思います。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?