これは、なにをしたくて書いたもの?
TiDBのアーキテクチャーをざっくりと把握しようという、このあたりの続きです。
※最初のエントリーがこのシリーズのインデックスページにもなっています
TiDBのアーキテクチャーをざっくりと眺めてみる(全体概要、ストレージ概要まで) - CLOVER🍀
TiDBのアーキテクチャーをざっくりと眺めてみる(コンピューティング概要) - CLOVER🍀
TiDBのアーキテクチャーをざっくりと眺めてみる(ストレージエンジン概要:TiKV編) - CLOVER🍀
TiDBのアーキテクチャーをざっくりと眺めてみる(ストレージエンジン概要:TiFlash編) - CLOVER🍀
今回はトランザクションについて扱ってみようと思います。
また、このシリーズはいったん今回で終わりにしようと思います。
TiDBのトランザクション
TiDBのトランザクションについて書かれたページはこちら。
TiDBは悲観的、楽観的のいずれかのトランザクションモードを使用した分散トランザクションをサポートしていて、デフォルトでは
悲観的トランザクションモードを使用するようです。
TiDB supports distributed transactions using either pessimistic or optimistic transaction mode. Starting from TiDB 3.0.8, TiDB uses the pessimistic transaction mode by default.
このページでは一般的なトランザクションの操作について書かれていますが、特徴的なものを挙げておきましょう。
BEGINまたはSTART TRANSACTIONで新しいトランザクションを開始する- デフォルトはオートコミット(MySQLとの互換性のため)
- DDLはオートコミットされ、ロールバックはできない
- 楽観的トランザクションはデフォルトでDMLの実行時に主キー制約または一意制約をチェックしない
- チェックはコミット時に行われる
- この遅延チェックの最適化は、ネットワーク通信を削減することでパフォーマンスの向上を狙ったもの
- 設定で無効にすることもできる
- デフォルトではひとつのトランザクションのサイズは100MB以下である必要がある
Causal consistency(因果一貫性)
TiDBはCausal consistency(因果一貫性)をサポートしていて、コミット時にPDからタイムスタンプを取得する必要がなくなり、コミットの
待ち時間も短くなります。
Causal consistencyを有効にしたトランザクションは以下のコマンドで開始できます。
START TRANSACTION WITH CAUSAL CONSISTENCY ONLY;
TiDBがデフォルトで保証するのはLinear Consistency(線形一貫性)です。
参考)
Eventual Consistencyまでの一貫性図解大全 #分散システム - Qiita
たとえば。
トランザクション1とトランザクション2があり、トランザクション1がコミットされた後にトランザクション2がコミットされた場合、
論理的にはトランザクション2はトランザクション1の後に発生します。
Causal consistencyはLinear Consistencyよりも弱く、Causal consistencyではトランザクション1とトランザクション2によってロックまたは
書き込まれるデータが交錯している場合のみ、2つのトランザクションのコミット順と発生順が保証されます。
これは、データベースが2つのトランザクションの間に因果関係があることを知っていることを意味しています。また、現在のTiDBは
外部から因果関係を渡されることをサポートしていません。
Causal consistencyが有効になっているトランザクションには、次の3つの特徴があります。
- 潜在的な因果関係を持つトランザクションは、一貫した論理的な順序と物理的なコミット順序を持つ
- 因果関係のないトランザクションでは、一貫した論理的な順序と物理的なコミット順序が保証されない
- ロックなしの読み取りでは因果関係は作られない
潜在的な因果関係を持つトランザクションは、一貫した論理的な順序と物理的なコミット順序を持つ
具体的な例はこちら。Causal consistencyを有効にした2つのトランザクションを示しています。

この例の場合、2つのトランザクションは同じidのレコードをロック、変更しています。つまり2つのトランザクションには潜在的な
因果関係があります。この場合、Causal consistencyを有効にしていてもトランザクション2はトランザクション1の後に発生する必要が
あります。
なので、トランザクション2のidが2のレコードに対する変更を読み取るためには、トランザクション1が完了している必要があります。
因果関係のないトランザクションでは、一貫した論理的な順序と物理的なコミット順序が保証されない
具体的な例はこちら。Causal consistencyを有効にした2つのトランザクションと、もうひとつ別のトランザクションの例を示しています。

トランザクション1が更新しているレコードと、トランザクション2が更新しているレコードは、それぞれ別のidなので2つのトランザクションの
間に因果関係はありません。これらのトランザクションにCausal consistencyが有効になっている場合、物理的な順番でトランザクション1、
トランザクション2の順にコミットされたとしてもTiDBはトランザクションが論理的にこの順番になることを保証しません。
トランザクション3がトランザクション1のコミット前に開始され、トランザクション2のコミット後にidが1のレコードを読み取る場合、
idが1のレコードの値は2、idが2のレコードの値は0(初期値)を読み取る可能性があります。
※つまり、トランザクション2のデータはコミット後、トランザクション1のデータはコミット前の状態のものが見える可能性がある
ロックなしの読み取りでは因果関係は作られない
具体的な例はこちら。Causal consistencyを有効にした2つのトランザクションを示しています。

Transactions / Causal consistency / Reads without lock do not create causal relationship
トランザクション2はidが1のレコードを更新し、トランザクション1は同じくidが1のレコードを読み取っていますが、読み取り時にロックを
取得していないので2つのトランザクションの間に因果関係は作られません。
2つのトランザクションはWrite Skew(※)を生じています。
※あるトランザクションT1がxの値を読み取ってyの値を変更し、別のトランザクションT2がyの値を読み取ってxの値を変更する時、一貫性のない変更になってしまうこと
この場合、2つのトランザクションに因果関係があるとしたら不合理であり、Causal consistencyが有効になっているこの2つの
トランザクションに明確な論理的な順序がありません。
コミット分離レベル
コミット分離レベルについて書かれているページはこちら。
TiDB Transaction Isolation Levels | TiDB Docs
TiDBはREAD COMMITTEDをサポートしていますが、これは悲観的トランザクションモードでのみ有効なことが書かれています。
The Read Committed isolation level only takes effect in the pessimistic transaction mode. In the optimistic transaction mode, setting the transaction isolation level to Read Committed does not take effect and transactions still use the Repeatable Read isolation level.
楽観的トランザクションモードの場合は、トランザクション分離レベルをREAD COMMITTEDに設定してもREPEATABLE READで
動作することが書かれています。
ちなみに、TiDBがサポートしているトランザクション分離レベルはREAD COMMITTEDとREPEATABLE READの2つのようです。
TiDB supports the following isolation levels: READ COMMITTED and REPEATABLE READ
Transaction overview / Transaction isolation levels
楽観的トランザクション
楽観的トランザクションについて書かれているページはこちら。
TiDB Optimistic Transaction Model | TiDB Docs
TiDB 3.0.8以降のデフォルトのトランザクションモードは悲観的トランザクションなのですが、悲観的トランザクションについて書かれた
ページは楽観的トランザクションについての説明を読んでいることが前提になっているようなので、先にこちらを見ていきます。
楽観的トランザクションは競合する更新をコミット時に検出することでレコードロックを取得するプロセスをスキップでき、同時に実行される
トランザクションが同じレコードを頻繁に更新しない場合にパフォーマンスが向上するモードです。同時実行されるトランザクションが
同じレコードを更新する状況が頻発する場合は、悲観的トランザクションよりもパフォーマンスが悪くなる可能性があります。
よって、特徴的なのはコミットが失敗することがあり、アプリケーションがそのエラーを適切に処理する必要があるということですね。
楽観的トランザクションの原則
楽観的トランザクションは、分散トランザクションをサポートするために2フェーズコミットを採用しています。
TiDB Optimistic Transaction Model / Principles of optimistic transactions
そのシーケンスを図示したものはこちらです。

以下の手順になっています。
- クライアントがトランザクションを開始する
- クライアントが読み取りリクエストを行う
- TiDBはPDからルーティング(TiKVノード間のデータの分散状況)を受信する
- TiDBはTiKVからstart_tsのバージョンのデータを受け取る
- クライアントが書き込みリクエストを行う
- TiDBは2フェーズコミットを開始し、トランザクションのアトミック性を保証しながらデータをストアに保持する
- TiDBは書き込むデータから主キーを選択する
- TiDBはPDからリージョン分布の情報を受け取り、それに応じてすべてのキーをリージョンごとにグループ化する
- TiDBは関係するすべてのTiKVのノードに事前書き込みリクエストを送信し、TiKVは競合または期限切れのバージョンがあるか確認する
- 有効なデータがあった場合はロックする
- TiDBは事前書き込みフェーズですべてのレスポンスを受信し、事前書き込みは成功する
- TiDBはPDからコミットバージョン番号を受け取り、commit_tsとしてマークする
- TiDBは主キーが配置されているTiKVノードへの2回目のコミットを開始し、TiKVはデータをチェックして事前書き込みフェーズで残っているロックを削除する
- TiDBは2つ目のフェーズが正常に終了したことを報告するメッセージを受信する
- TiDBはトランザクションが正常にコミットされたことをクライアントに通知するメッセージを返す
- TiDBはトランザクションに残っているロックを非同期に削除する
長所・短所
楽観的トランザクションの長所と短所は以下のようになっています。
- 長所
- 理解しやすい
- 単一の行にもとづくクロスノードトランザクションを実装している
- 分散ロック管理
- 短所
- 2フェーズコミットによるトランザクションの遅延
- 中央集権的なタイムスタンプを割り当てるサービスが必要
- 大量にデータが書き込まれるとOOM(out of memory)になる
TiDB Optimistic Transaction Model / Advantages and disadvantages
トランザクションのリトライ
楽観的トランザクションモデルでは、競合が激しいシナリオでは書き込み同士がコンフリクトするため、トランザクションがコミットされない
場合があります。
MySQLでは悲観的同時実行制御を利用するため、書き込みのSQL実行中にロックを追加し、REPEATABLE READ分離レベルでは
現在の読み取りが許可されるのでコミット時に例外が発生することはありません。
TiDBで楽観的同時実行制御を利用しますが、アプリケーションがコミット時のエラーハンドリングの難しさを軽減するために内部での
リトライメカニズムを提供します。
TiDB Optimistic Transaction Model / Transaction retries
トランザクションのリトライはtidb_disable_txn_auto_retryをOFFにすることで有効になり、デフォルトでは無効になっています。
これは再試行を行うことで更新が失われたり、REPEATABLE READ分離レベルを損なう可能性があるためです。
これはリトライの制限であり、その理由は以下の手順で説明できます。
- 新しいタイムスタンプを割り当て、start_tsとする
- 書き込み操作を含むSQL文をリトライする
- 2フェーズコミットを実装する
2番目のステップではTiDBは書き込み操作を含むSQL文のみをリトライします。しかし、リトライ中にTiDBはトランザクションの開始を
表す新しいバージョン番号を受け取ります。つまり、TiDBは新しいstart_tsバージョンのデータを使用してSQL文をリトライするため、
他のトランザクションの更新結果を使用してデータを更新するとREPEATABLE READに違反し、結果が矛盾する可能性があります。
アプリケーションがLost Updateを許容でき、REPEATABLE READ分離レベルを必要としない場合はトランザクションのリトライ機能を
有効にできます。
なお、リトライ回数の上限はtidb_retry_limitで指定します。
競合の検出
競合の検出はTiKVレイヤーで行われて、Scheduler latch wait durationメトリクスを参照することで確認できるようです。
TiDB Optimistic Transaction Model / Conflict detection
Scheduler latch wait durationが高く低速な書き込みがない場合は、書き込み競合が多数発生していると言えるようです。
悲観的トランザクション
悲観的トランザクションは、TiDB 3.0.8以降はデフォルトのトランザクションモードです。悲観的トランザクションについて
書かれているページはこちら。
TiDB Pessimistic Transaction Mode | TiDB Docs
新しく開始したトランザクションがどのモードになるかは、tidb_txn_modeで設定できるようです。またBEGIN PESSIMISTICで
明示的に悲観的トランザクションを開始することもできるようです。
TiDB Pessimistic Transaction Mode / Switch transaction mode
悲観的トランザクションの振る舞い
悲観的トランザクションの振る舞いの例はこちら。同じデータを参照する、3つのセッションとトランザクションについて書かれています。

TiDB Pessimistic Transaction Mode / Behaviors
- セッション1はテーブルを作成して、悲観的トランザクションを開始してデータを更新(a = a + 1)する
- セッション1がコミットする前に、セッション2が悲観的トランザクションを開始
- セッション2は、トランザクションを開始した時より前にコミットされたデータ、つまりセッション1が更新する前のデータ(a = 1)を参照する
- セッション3が悲観的トランザクションを開始し、select for updateで同じデータに悲観的ロックを取得しようとするが、セッション1のトランザクションがロックしているため待ちになる
- セッション1がコミットする
- セッション2はトランザクションを終了していないので、値が1のままのaを参照し続ける
ポイントはこちら。
- insert、update、deleteはコミットされた"最新の"データを読み取る
- トランザクションがコミットまたはロールバックされると、ロックは解除される
- selectを行うトランザクションはブロックされない
- ただしselect for updateの場合は、コミットされた"最新の"データに悲観的ロックを適用する
MySQL(InnoDB)との違い
MySQL(InnoDB)との違いはこちらに書かれています。
TiDB Pessimistic Transaction Mode / Difference with MySQL InnoDB
こんなことが書かれています。
- ギャップロックをサポートしていない
- select lock in share modeをサポートしていない
- MySQLではDDLの実行はブロックされる可能性があるが、TiDBはブロックされないので悲観的トランザクションが失敗することがある
- start transaction with consistent snapshotを実行した際、MySQLでは他のトランザクションが作成したデータを読み取ることができるが、TiDBではできない
- トランザクションが自動コミットの場合、楽観的ロックが優先される
- 悲観的モデルでは自動コミットのトランザクションでは最初にオーバーヘッドが少ない楽観的モデルを使ってコミットを試みる
- 書き込みが競合した場合は悲観的モデルを使ってリトライする
- embedded selectで読み取られたデータはロックされない
- TiDBのトランザクションはGCをブロックせず、デフォルトでは悲観的トランザクションの最大実行時間は1時間に制限される
悲観的トランザクションモードでサポートされているトランザクション分離レベルは、REPEATABLE READとREAD COMMITTEDです。
TiDB Pessimistic Transaction Mode / Isolation level
悲観的トランザクションのコミットプロセス
悲観的トランザクションのコミットプロセスについて書かれているのはこちら。2フェーズコミットであることが書かれています。

TiDB Pessimistic Transaction Mode / Pessimistic transaction commit process
悲観的トランザクションは、2フェーズコミットの前にAcquire Pessimistic Lockフェーズを追加します。このフェーズには次の手順が
含まれています。
- TiDBはクライアントからトランザクションを開始するリクエストであるbeginを受け取ると、現在のタイムスタンプをstart_tsとして扱う
- これは楽観的トランザクションモードと同じ
- TiDBサーバーはクライアントから書き込みリクエストを受け取ると、 TiKVサーバーに悲観的ロックを取得するリクエストを開始し、ロックはTiKVサーバーに保持される
- クライアントがコミットをリクエストすると、TiDBは楽観的トランザクションモードと同様に2フェーズコミットを開始する

パイプライン化されたロック処理
悲観的ロックを取得する際のパイプライン化について、こちらに書かれています。
TiDB Pessimistic Transaction Mode / Pipelined locking process
そもそもパイプライン化とは?ということですが、こちらが背景になっています。
- 悲観的ロックを行うということはTiKVにデータを書き込むことを意味しており、Raftを通してコミットおよび適用できた後にTiDBに成功したというレスポンスを返すことができる
- このため、楽観的トランザクションと比較するとレイテンシーが高くなる
このロックのオーバーヘッドを削減するためのものが、TiKVに実装されたパイプライン化されたロック処理です。
こちらの図のように、データがロックの要件を満たすとTiKVはすぐにTiDBにレスポンスを返し、後続のロック処理は非同期に実行します。

この結果、レイテンシーが削減され悲観的トランザクションのパフォーマンスが向上します。トレードオフもあり、TiKVでネットワーク
パーティションが発生したり、TiKVノードがダウンすると悲観的ロックの非同期書き込みが失敗し次のような状況を生む可能性があります。
- 同じデータを変更する他のトランザクションをブロックできなくなる
- アプリケーションがロックやロック待機の仕組みに依存している場合、アプリケーションロジックの正確性が影響を受ける
- 低い可能性でトランザクションが失敗する
- トランザクションの正確性には影響しない
ロックのパイプライン化はデフォルトで有効になっています。
アプリケーションがロックまたはロック待機の仕組みに依存している場合や、TiKVクラスターに異常が発生しても可能な限りトランザクションの
コミットを成功させたい場合は、ロックのパイプライン化を無効にする必要があります。
おわりに
TiDBのアーキテクチャーのうち、トランザクションまわりを見てみました。
けっこう気になるところだったのですが、楽観的、悲観的の2種類があることやその基本的な考え方などを知ることができて良かったかなと
思います。本当に、導入部分だけですけれど。
というわけで、TiDBのアーキテクチャーをざっくりと把握しようというシリーズは今回でいったんおしまいです。