きり丸の技術日記

技術検証したり、資格等をここに残していきます。

MySQLではUPSERTしないように設計する

はじめに

※ 概要レベルの解説です。詳しい解説については他の方の記事を参考にしてください。

環境

  • MySQL 8.0

UPSERTとデッドロックの問題

UPSERTはデータが存在する場合はUPDATE、存在しない場合はINSERTを実行する便利な構文です。特にORMを使用している場合、複雑なSQL構文を意識せずに簡単にUPSERTを実装できます。

しかし、注意すべき点があります。この構文はロックを取得するため、デッドロックが発生する原因となります。より正確に言うと、トランザクション分離レベルに依存しており、MySQLのデフォルトであるREPEATABLE READでは特に多くのロックを取得するため、デッドロックが発生しやすくなります。

対策1: トランザクション分離レベルの変更

他のRDBMSでデフォルトとされているREAD COMMITTEDにトランザクション分離レベルを変更すると、デッドロックが発生しにくくなります。

データベース設定での変更

最も簡単な方法は、DB本体のトランザクション分離レベルを変更することです。Amazon Auroraではデータベースを停止せずにトランザクション分離レベルを変更できることを確認していますが、実際の影響範囲が不明なため実行経験はありません。

アプリケーション側での変更

トランザクション発行時にトランザクション分離レベルを指定できる場合は、アプリケーション側で対応も可能です。ただし、最も外側のトランザクションでのみ分離レベル変更が可能な制約があります。

Railsの場合、トランザクション分離レベルを変更するオプションは存在しますが、RSpecのデフォルト設定(use_transactional_fixtures: true)がトランザクションを張った後に本体処理を実行する仕組みのため、そのままではテストが通らなくなります。デッドロックが発生してからトランザクション分離レベル変更を検討する段階では、通常かなり運用が進んでいる状態と考えられるため、アプリケーション側での対応は困難な場合が多いです。

対策2: Event Sourcingによる設計

UPDATEそのものがロックの原因となるため、Event Sourcingで設計できるのであれば、より清潔な設計を実現できます。

まとめ

  • UPSERTは使用しないように設計する
  • どうしても使用する場合は、トランザクション分離レベルがREAD COMMITTEDであることを確認する
  • Event Sourcingで設計してUPDATEそのものを禁止する方法も検討する

おわりに

最近、データベースでデッドロックが増加傾向にあり、何とかしたいという思いがありますが、なかなか根本解決は困難です。

このような問題は事前に設計段階で対応できるよう、設計力を向上させていきたいと思います。