数日前、オンラインでデータベースのデッドロックの問題が発生しました。この問題は長い間調査されてきました。この過程で、データベースのロックメカニズムについて深く理解しました。
image.png
この記事では、このデッドロック調査のプロセス全体を要約し、デッドロックの原因と解決策を分析します。行き詰まりの調査と解決策のアイデアを提供したいと考えています。
この記事には、MySQL実行エンジン、データベース分離レベル、InnoDBロックメカニズム、インデックス、データベーストランザクション、およびその他の知識分野が含まれます。読者の皆様に過去と未来から何かを学んでいただければ幸いです。
現象
ある夜、同僚がアナウンスをしていたところ、突然多数のオンラインアラームが発生し、その多くはデータベースのデッドロックに関するものでした。アラーム情報は次のとおりです。
{"errorCode": "SYSTEM_ERROR"、 "errorMsg": "ネストされた例外はorg.apache.ibatis.exceptions.PersistenceException:
データベースの更新中にエラーが発生しました。原因:ERR-CODE:[TDDL-4614] [ERR_EXECUTE_ON_MYSQL]
しようとしたときにデッドロックが見つかりましたロックを取得;
パラメータの設定中にエラーが発生しました\ n ### SQL:
update Fund_transfer_stream set gmt_modified = now()、state =?where Fund_transfer_order_no =?and Seller_id =?and state = 'NEW'
アラームを通じて、基本的にデッドロックが発生しているデータベースとデータベーステーブルを見つけることができます。まず、この記事の場合に関係するデータベース関連の情報を紹介します。
背景状況
使用するデータベースはMySQL5.7、エンジンはInnoDB、トランザクション分離レベルはREAD-COMMITEDです。
データベースバージョンのクエリ方法:
version();を選択します。
エンジンクエリメソッド:
show create table Fund_transfer_stream;
ストレージエンジンの情報は、ENGINE = InnoDBなどのテーブル作成ステートメントに表示されます。
トランザクション分離レベルのクエリ方法:
@@ tx_isolationを選択します。
トランザクション分離レベルの設定方法(現在のセッションでのみ有効):
セッショントランザクション分離レベルの読み取りコミットを設定します。
PS:データベースがサブデータベースの場合、上記のいくつかのSQLステートメントは、ロジックデータベースではなく、単一のデータベースで実行する必要があることに注意してください。
デッドロックが発生するテーブル構造とインデックスの状況(一部の無関係なフィールドとインデックスは非表示になっています):
CREATE TABLE fund_transfer_stream
(id
bigint(20)unsigned NOT NULL AUTO_INCREMENT COMMENT'primary key '、gmt_create
datetime NOT NULL COMMENT'creation time'、gmt_modified
datetime NOT NULL COMMENT'modification time '、pay_scene_name
varchar(256)NOT NULL COMMENT'支払いシーン名 '、pay_scene_version
varchar(256 )DEFAULT NULL COMMENT '支払いシナリオバージョン'、identifier
varchar(256)NOT NULL COMMENT '一意のID'、seller_id
varchar(64)NOT NULL COMMENT 'セラーID'、state
varchar(64)DEFAULT NULL COMMENT 'ステータス'、fund_transfer_order_no
varchar(256)
DEFAULT NULL COMMENT 'ファンドプラットフォームから返されるステータス'、
PRIMARY KEY(id
)、UNIQUE KEY uk_scene_identifier
(KEY idx_seller
(seller_id
)、
KEY idx_seller_transNo
(seller_id
、fund_transfer_order_no
(20))
)ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'ファンドフロー';
データベースには3つのインデックスがあり、1つはクラスター化インデックス(プライマリキーインデックス)、2つは非クラスター化インデックス(非プライマリキーインデックス)です。
クラスター化されたインデックス:
主キー(id
)
非クラスター化インデックス:
キーidx_seller
(seller_id
)、
キーidx_seller_transNo
(seller_id
、fund_transfer_order_no
(20))
上記の2つのインデックスでは、idx_seller_transNoが実際にidx_sellerをカバーしています。歴史的な理由により、このテーブルはseller_idで分割されているため、最初にidx_seller、後でidx_seller_transNoになります。
デッドロックログ
データベースでデッドロックが発生した場合、デッドロックログは次のコマンドで取得できます。
エンジンのinnodbステータスを表示する
デッドロックが発生した場合は、初めてデッドロックログを確認してください。デッドロックログの内容は以下のとおりです。
トランザクションのデッドロックが検出され、詳細情報がダンプされました。
2019-03-19T21:44:23.516263 + 08:00 5877341 [注] InnoDB:
***(1)
トランザクション:トランザクション173268495、アクティブ0秒
使用中のmysqlテーブルのフェッチ1、ロック1
LOCK WAIT 304ロック構造体、ヒープサイズ41168、6行ロック、ログエントリの取り消し1MySQL
スレッドID 5877358、OSスレッドハンドル47356539049728、クエリID 557970181 11.183.244.150fin_instant_appの更新
update fund_transfer_stream
set gmt_modified
= NOW()、state
= 'PROCESSING' where((state
= 'NEW')AND(seller_id
= '38921111')AND(fund_transfer_order_no
= '99010015000805619031958363857'))
2019-03-19T21:44:23.516321 + 08:00 5877341 [注] InnoDB:
***(1)ロックを保持します:
レコードロックスペースID173ページ番号13726nビット248インデックスidx_seller_transテーブルの番号xxx
。fund_transfer_stream
trx id 173268495 lock_mode Xはrecを
ロックしますが、ギャップはロックしませんレコードロック、ヒープ番号168物理レコード:n_fields 3; コンパクトフォーマット; 情報ビット0
2019-03-19T21:44:23.516565 + 08:00 5877341 [注] InnoDB:
***(1)このロックが付与されるのを待っています:
レコードロックスペースID173ページ番号12416nビット128インデックステーブルのプライマリxxx
。fund_transfer_stream
trx id 173268495 lock_mode Xはrecを
ロックしますが、ギャップ待機はしませんレコードロック、ヒープ番号56物理レコード:n_fields 17; コンパクトフォーマット; 情報ビット
02019-03-19T21:44:23.517793 + 08:00 5877341 [注] InnoDB:
***(2)
トランザクション:トランザクション173268500、アクティブ0秒の行のフェッチ
、使用中のInnoDB 81 mysqlテーブル内で宣言されたスレッド1、ロックされた1
302ロック構造体、ヒープサイズ41168、2行ロック、元に戻すログエントリ
1MySQLスレッドID5877341、OSスレッドハンドル47362313119488、クエリID 557970189 11.131.81.107fin_instant_app更新
update fund_transfer_stream_0056
set gmt_modified
= NOW()、state
= 'PROCESSING' where((state
= 'NEW')AND(seller_id
= '38921111')AND(fund_transfer_order_no
= '99010015000805619031957477256'))
2019-03-19T21:44:23.517855 + 08:00 5877341 [注] InnoDB:
***(2)ロックを保持します:
レコードロックスペースID173ページ番号12416nビット128インデックステーブルのプライマリfin_instant_0003
。fund_transfer_stream_0056
trx id 173268500 lock_mode Xはrecを
ロックしますが、ギャップはロックしませんレコードロック、ヒープ番号56物理レコード:n_fields 17; コンパクトフォーマット; 情報ビット0
2019-03-19T21:44:23.519053 + 08:00 5877341 [注] InnoDB:
***(2)このロックが付与されるのを待っています:
レコードロックスペースID173ページ番号13726nビット248インデックスidx_seller_transテーブルの番号fin_instant_0003
。fund_transfer_stream_0056
trx id 173268500 lock_mode Xはrecを
ロックしますが、ギャップ待機はしませんレコードロック、ヒープ番号168物理レコード:n_fields 3; コンパクトフォーマット; 情報ビット0
2019-03-19T21:44:23.519297 + 08:00 5877341 [注] InnoDB:***トランザクションをロールバックします(2)
デッドロックログを簡単に解釈すると、次の情報を取得できます。
①デッドロックの原因となった2つのSQLステートメントは次のとおりです。
更新fund_transfer_stream_0056
セットgmt_modified
= NOW()、state
= 'PROCESSING'
where((state
= 'NEW')AND(seller_id
= '38921111')AND(fund_transfer_order_no
= '99010015000805619031957477256'))
更新fund_transfer_stream_0056
セットgmt_modified
= NOW()、state
= 'PROCESSING'
where((state
= 'NEW')AND(seller_id
= '38921111')AND(fund_transfer_order_no
= '99010015000805619031958363857'))
②インデックスidx_seller_transNoのロックを保持しているトランザクション1は、PRIMARYのロックの取得を待機しています。
③PRIMARYロックを保持しているトランザクション2は、idx_seller_transNoロックの取得を待機しています。
④トランザクション1とトランザクション2の間の循環待機が原因でデッドロックが発生します。
⑤現在トランザクション1とトランザクション2で保持されているロックは次のとおりです。lock_modeXはrecをロックしますが、ギャップはロックしません。
どちらのトランザクションも、レコードにXロックを追加し、ギャップなしロック、つまり、ギャップロックを追加せずに、現在の行レコードをロックします(レコードロック)。
Xロック:排他ロック。書き込みロックとも呼ばれます。トランザクションTがデータオブジェクトAにXロックを追加する場合、トランザクションTはAを読み取るかAを変更でき、他のトランザクションはTがAのロックを解放するまでAにロックを追加できません。これにより、TがAのロックを解除する前に、他のトランザクションがAを読み取ったり変更したりできなくなります。
これに対応するのはSロック:共有ロック、別名読み取りロックです。トランザクションTがデータオブジェクトAにSロックを追加する場合、トランザクションTはAを読み取ることはできますが、Aを変更することはできません。他のトランザクションはAにSロックのみを追加できます。 TがAのSロックを解除するまで、Xロックを追加することはできません。
これにより、他のトランザクションはAを読み取ることができますが、TがAのSロックを解放するまでAに変更を加えることはできません。
ギャップロック:ギャップロック、範囲をロックしますが、レコード自体は含まれません。ギャップロックの目的は、同じトランザクションの2つの現在の読み取りがファントム読み取りから行われるのを防ぐことです。
Next-Key Lock:1 + 2、範囲をロックし、レコード自体をロックします。行クエリの場合、この方法が使用され、主な目的はファントム読み取りの問題を解決することです。
トラブルシューティング
現在わかっているデータベース関連の情報とデッドロックログに基づいて、基本的にいくつかの簡単な判断を下すことができます。
まず第一に、このデッドロックはギャップロックやネクストキーロックとは何の関係もないはずです。データベースの分離レベルはRC(READ-COMMITED)であるため、この分離レベルはギャップロックを追加しません。以前のデッドロックログにもこれが記載されています。
次に、コードを調べて、コード内でトランザクションがどのように行われるかを確認する必要があります。コアコードとSQLは次のとおりです。
@Transactional(rollbackFor = Exception.class)
public int doProcessing(String SellerId、Long id、String FundTransferOrderNo){
fundTreansferStreamDAO.updateFundStreamId(sellerId、id、fundTransferOrderNo);
FundTreansferStreamDAO.updateStatus(sellerId、fundTransferOrderNo、 "PROCESSING");を返します。
}
このコードの目的は、同じレコードの2つの異なるフィールドを連続して変更することです。updateFundStreamIdSQL:
更新fund_transfer_streamset
gmt_modified = now()、fund_transfer_order_no =#{fundTransferOrderNo}
ここで、id =#{id}およびseller_id =#{sellerId}
updateStatus SQL:
更新fund_transfer_streamset
gmt_modified = now()、state =#{state}
ここで、fund_transfer_order_no =#{fundTransferOrderNo}およびseller_id =#{sellerId}
およびstate = 'NEW'
ご覧のとおり、同じトランザクションで2つのUpdateステートメントを実行しました。次の2つのSQLの実行計画は次のとおりです。
PRIMARYインデックスは、updateFundStreamIdが実行されるときに使用されます。
idx_seller_transNoインデックスは、updateStatusの実行時に使用されます。
実行計画を通じて、updateStatusには実際に2つのインデックスが使用可能であり、idx_seller_transNoインデックスが実際に実行中に使用されることがわかりました。これは、MySQLクエリオプティマイザがコストベースのクエリメソッドであるためです。
したがって、クエリプロセスで最も重要な部分は、クエリSQLステートメントと複数のインデックスに基づいてクエリのコストを計算し、クエリプランを生成するための最適なインデックス方法を選択することです。
クエリ実行計画は、デッドロックが発生した後に実行されます。事後クエリの実行計画とデッドロック時のインデックスの使用法は、必ずしも同じではありません。
ただし、デッドロックログと組み合わせると、上記の2つのSQLステートメントの実行時に使用されるインデックスを見つけることもできます。
つまり、updateFundStreamIdの実行時にPRIMARYインデックスが使用され、updateStatusの実行時にidx_seller_transNoインデックスが使用されます。
上記の既知の情報を使用して、デッドロックの原因とその背後にある原理の調査を開始できます。
コードとデータベーステーブル作成ステートメントを組み合わせてデッドロックログを分析したところ、主な問題はidx_seller_transNoインデックスにあることがわかりました。
キーidx_seller_transNo
(seller_id
、fund_transfer_order_no
(20))
インデックス作成ステートメントでは、プレフィックスインデックスを使用しました。インデックススペースを節約し、インデックスの効率を向上させるために、fund_transfer_order_noフィールドの最初の20ビットのみをインデックス値として選択しました。
Fund_transfer_order_noは単なる通常のインデックスであり、一意のインデックスではないためです。また、特別な場合には、同じユーザーの2つのfund_transfer_order_noの最初の20ビットが同じになるためです。
これにより、2つの異なるレコードのインデックス値が同じになります(seller_idとfund_transfer_order_no(20)が同じであるため)。
この記事の例のように、デッドロックが発生する2つのレコードのfund_transfer_order_noフィールドの2つの値は、最初の20ビットで同じです:
99010015000805619031958363857
99010015000805619031957477256
image.png
では、なぜfund_transfer_order_noの最初の20ビットがデッドロックを引き起こすのでしょうか。
ロックの原理
このケースを取り上げて、MySQLデータベースロックの原理と、この記事のデッドロックの背後で何が起こったのかを見てみましょう。
データベースでデッドロックシナリオをシミュレートします。実行シーケンスは次のとおりです。
image.png
MySQLでは、行レベルのロックはレコードを直接ロックするのではなく、インデックスをロックすることを知っています。インデックスは、プライマリキーインデックスと非プライマリキーインデックスに分けられます。
SQLステートメントがプライマリキーインデックスを操作する場合、MySQLはプライマリキーインデックスをロックします。
ステートメントが非プライマリキーインデックスを操作する場合、MySQLは最初に非プライマリキーインデックスをロックし、次に関連するプライマリキーインデックスをロックします。
主キーインデックスのリーフノードには、データの行全体が格納されます。InnoDBでは、プライマリキーインデックスはクラスター化インデックス(クラスター化インデックス)とも呼ばれます。
非プライマリキーインデックスのリーフノードの内容は、プライマリキーの値です。InnoDBでは、非プライマリキーインデックスは、非クラスタ化インデックス(セカンダリインデックス)とも呼ばれます。
したがって、この記事の例に含まれるインデックス構造(インデックスはB +ツリーであり、テーブルに簡略化されています)を次の図に示します。
image.png
デッドロックの発生は、トランザクション内のSQLステートメントの数に依存しません。デッドロックの鍵は、2つ(またはそれ以上)のセッションロックの一貫性のないシーケンスにあります。
次に、上記の例の2つのトランザクションのロック順序を見てみましょう。
image.png
次の図は展開図であり、各SQLが実行されたときのロック状況です。
image.png
上記の2つの写真を組み合わせると、デッドロックの原因が見つかりました。
トランザクション1はupdate1を実行してPRIMARY = 1ロックを占有し、トランザクション2はupdate1を実行してPRIMARY = 2ロックを占有します。
トランザクション1はupdate2を実行し、idx_seller_transNo =(3111095611、99010015000805619031)のロックを保持します。PRIMARY= 2を保持しようとすると失敗します(ブロッキング)。
idx_seller_transNo =(3111095611、99010015000805619031)のロックを保持しようとしたときに、トランザクション2がupdate2(デッドロック)の実行に失敗しました。
トランザクションが非プライマリキーインデックスをWhere条件として更新すると、最初に非プライマリキーインデックスがロックされ、次に非プライマリキーインデックスに対応するプライマリキーインデックスがクエリされ、次にこれらのプライマリキーインデックスがロックされます。)
解決
これまで、デッドロックにつながる基本原則とその背後にある原則を明確に分析してきました。そうすれば、この問題を解決するのは難しくありません。
次の2つの側面から始めることができます。
インデックスを変更する
コードを変更する(SQLステートメントを含む)
インデックスを変更します。プレフィックスインデックスidx_seller_transNoのfund_transfer_order_noのプレフィックス長を変更する限り。
たとえば、50に変更すると、デッドロックを回避できます。ただし、idx_seller_transNoのプレフィックス長を変更した後、デッドロックを解決するための前提条件は、Updateステートメントが実際に実行されるときにfund_transfer_order_noインデックスが使用されることです。
コスト分析後にMySQLクエリオプティマイザがインデックスKEYidx_seller(seller_id)を使用することを決定した場合でも、デッドロックの問題が発生します。原理はこの記事に似ています。
したがって、基本的な解決策はコードを変更することです。
すべての更新は、プライマリキーIDを介して実行されます。
同じトランザクションで、同じレコードを変更するために複数のUpdateステートメントを使用しないでください。
まとめと考察
デッドロックが発生してから1週間後、ほぼ毎日時間をかけて調査し、問題を早期に特定し、修正計画も入手できましたが、原理がわかりませんでした。
私は前後に多くの推論と仮定をしました、そしてそれらは私自身によって何度も何度も覆されました。結局、あなたはあなたの考えを検証するために練習に頼らなければなりません。
そこで、データベースをローカルにインストールし、実際のテストをいくつか行い、データベースのロックステータスをリアルタイムで確認しました。show engine innodbstatus;ロックステータスを表示できます。最終的に原理を理解しました。
簡単にいくつかの考え:
問題があるかどうか推測しないでください。!!自分で問題を再現し、分析します。
コンテキストを無視しないでください!!!最初は、デッドロックログにのみ注意を払い、コード内のトランザクションを常に無視して、実際に別のSQLステートメント(updateFundStreamId)を実行しました。
理論的な知識がどれほど十分であっても、決定的な瞬間にそれを覚えていないかもしれません!!!
穴は自分で埋められます!!!