トランザクション

お店で注文を受けてから商品を出荷する場合、出荷記録と在庫を減らす記録の両方を行う必要があります。
どちらか一方でも記録が失敗すると、実際の在庫数に矛盾が生じてしまいます。

たとえば、出荷記録を残そうとしても、在庫が足りなければ出荷はできません。
そのため、出荷記録と在庫の減少処理は、どちらも成功するか、どちらも取り消される必要があります。
このように、複数の処理をまとめて一つのまとまった処理として扱い、全てが成功するか全てが失敗するようにする仕組みを「トランザクション」といいます。

ACID特性

トランザクションには、ACID特性と呼ばれる特性があり、その特性は次の4つがあります。

PostgreSQLは全てがトランザクション(暗黙的BEGIN)処理になり、PostgreSQLで後述する「select」「insert」「update」「delete」などのクエリ処理を行えばそれはトランザクションを意図的に行ってなくてもトランザクション処理になります。

直列化可能性

複数のトランザクションを順番に一つずつ実行していくと、実行順に関係なく一貫性が保たれます。
このような実行方法を「直列スケジュール」といいます。

一方、複数のトランザクションを同時に並行して実行しても、直列に実行した場合と同じ結果になるように一貫性を保つ方法を「直列化可能スケジュール」といいます。

ロック

データベース運用では、複数の処理が同時に実行したときに発生する問題を考える必要があります。

在庫数が3つある商品Aを同時に処理してしまった以下のようなケースが発生したとします。

AさんとBさんが在庫を確認したところ在庫がまだあったのですが(3)と(4)が同時に行えてしまうと本来であれば不足してエラーになるべき在庫がマイナスとなってしまい在庫が不足するという問題が発生してしまいます。

このようにデータが不整合にならないように行う処理としてトランザクションと排他制御があり、Isolationが該当します。

この排他制御ですが、データをロックすることで実現します。
ロックには制御する方式が「共有ロック」と「専有ロック」と2つあります。

ロックには、テーブル・レコード・トランザクションなどの粒度を設定できます。
ロックを実施すると、別のトランザクションではロック待ちが発生してトランザクションのスループットが低下します。

2相ロック

複数のトランザクションが同時にデータを操作しても、データの不整合を防ぐことを目的にしたものを2相ロックといいます。
2相ロックは、最初にロックをかけて操作完了後にロックを解除する流れでデータの整合性を確保します。
2相ロックには、ロック取得フェーズとロック解除フェーズがあります。

ロック取得フェーズでは、トランザクションがデータを操作する前に、そのデータにロックをかけます。
この間は、必要なロックを自由に追加できますが、一度でもロックを解除してしまうと、本フェーズを終了します。

ロック解除フェーズは、一度ロックの解除を始めると、その後は新しいロックをかけることができません。
残りのロックを順次解除していきます。

ISOLATIONレベル

ISOLATIONレベル(分離レベル)はトランザクションの処理結果が他のトランザクションに影響をどれぐらい与えないかを設定します。
分離レベルが高ければ矛盾は生じませんがトランザクションの処理性能は落ちます。

  1. READ UNCOMMITTED:Dirty reads=許可/Non-repeatable reads=許可/Phantom reads=許可
  2. READ COMMITTED:Dirty reads=拒否/Non-repeatable reads=許可/Phantom reads=許可
  3. REPEATABLE READ:Dirty reads=拒否/Non-repeatable reads=拒否/Phantom reads=許可
  4. SERIALIZABLE:Dirty reads=拒否/Non-repeatable reads=拒否/Phantom reads=拒否

Dirty reads/Non-repeatable reads/Phantom readsの詳細を次に記載します。

  1. Dirty reads:同時に実行されている他のトランザクションでコミットされていないデータを読み込むこと
  2. Non-repeatable reads:トランザクション中にデータの再読込時、途中でデータが更新されたデータを読み込むこと
  3. Phantom reads:トランザクション中に他のトランザクションが追加したデータを読み込むこと

データベーススペシャリストの試験では、「READ COMMITTED」と「REPEATABLE READ」がよく出題されます。
READ COMMITTEDでは、データを参照する際に共有ロックをかけ、その参照が終わるとすぐにロックを解除します。
データを変更する際は専有ロックをかけ、トランザクションが終了するとロックを解除します。

REPEATABLE READでは、データを参照する際に共有ロックをかけ、トランザクションが終了するまでロックを解除しません。
データを変更する場合も、専有ロックをかけ、トランザクションが終わるまで解除しません。

分離レベルが高いほど、複数のトランザクション間でロックの競合が発生しやすくなり、処理が進まないデッドロックが起きる可能性があります。
デッドロックを解除する方法としては、トランザクションをアボート(中断)するしかありません。
デッドロックの検出には待ちグラフが使われます。

デッドロックの例

このように、お互いが相手のロック解除を待っている状態では、どちらも先に進めず、永遠に処理が止まってしまいます。
基本的にデッドロックが発生するのは、2つ以上のトランザクションがテーブルまたはレコードをロックするときにクロスしてしまうことが問題です。
分離レベルは、デッドロックを避けるために下げるのではなく、複数のトランザクションが同時に動作した場合に互いにどのような影響を与えるかを考慮して設計する必要があります。

デッドロックを防ぐための設計例として、商品の商品コードが主キーである場合、テーブル内のデータを商品コード順に格納します。
そして、トランザクションでも商品コードの順に処理するようにします。
こうすることで、複数のトランザクションが同時に実行されても、処理の順序がクロスすることがなくなるため、デッドロックが発生しにくくなります。

トランザクションの処理

トランザクションのコマンドには以下があります。

  1. BEGIN:トランザクションを開始
  2. COMMIT:SQLの処理結果を確定
  3. ROLLBACK:SQLの処理結果をキャンセル

セーブポイントはトランザクション中にコマンドで目印を付けます。
その目印以降のトランザクションをロールバックすることができます。

障害回復

トランザクションシステムに障害が発生した場合、データベース管理システム(DBMS)には回復機能があります。
障害回復には、バックアップ(ディスク障害用)、チェックポイント、ログファイルが必要です。

チェックポイントとは、トランザクション処理の結果がメモリ内に保存され、そのデータをディスク(HDDやSSDなど)に書き込むタイミングを指します。
PostgreSQLでは、このタイミングは設定ファイル(postgresql.conf)の「max_wal_size」に達すると発生します。

WAL(Write Ahead Log)とは、トランザクションの前にログをディスクに書き出す仕組みです。
障害回復に必要なログファイルはこのWALを指し、先にログを書いておくことで、まだディスクに書き込まれていないデータもリカバリできるようになります。

障害が発生した際、回復処理はチェックポイント以降のデータを対象に行います。
障害前にトランザクションがコミット(確定)されていた場合、そのデータを復元するロールフォワードが行われます。
ロールフォワードとは、コミット時のデータに更新後の情報を使って復元することです。

一方、チェックポイント以降にコミットされていないトランザクションは、ロールバックが行われます。ロールバックは、更新前の情報を使って、トランザクションが行われる前の状態にデータを戻す操作です。

トランザクション

HDD・SSDなどのディスクが故障した場合、それらの情報を復旧するにはバックアップファイルを使用してリストアし、上記と同じように障害回復を実施します。
但し、バックアップやログ情報は別のディスクに保存していて利用できる必要があります。

PostgreSQLのロックモード

PostgreSQLではTRANSACTIONコマンド・postgresql.confなどで設定することがが可能です。
※「default_transaction_isolation = 'レベルを記載'」で設定します。
PostgreSQLには以下のロックモードがあります。

  1. ・ACCESS SHARE(AS):SELECTでロックモード獲得
  2. ・ROW SHARE(RS):SELECT FOR UPDATE,SELECT FOR SHAREなどでロックモード獲得
  3. ・ROW EXCLUSIVE(RE):INSERT、UPDATE、DELETEでロックモード獲得
  4. ・SHARE UPDATE EXCLUSIVE(SUE):ALTER TABLE,VACUUM,ANALYZEなどでロックモード獲得
  5. ・SHARE(S):CREATE INDEXでロックモード獲得
  6. ・SHARE ROW EXCLUSIVE(SRE):対象なし
  7. ・EXCLUSIVE(E):REFRESH MATERIALIZED VIEW CONCURRENTLYでロックモード獲得
  8. ・ACCESS EXCLUSIVE(AE):DROP TABLE,TRUNCATE,REINDEX,CLUSTER,VACUUM FULLでロックモード獲得

ロックモード間で競合する一覧を以下に記載します
ロックモード

[ロック]
ロックには重量ロックと軽量ロックがあります。
Postgresqlでのロックはテーブルのみです。
自身のプロセスでのロックでは競合を起こしません。
デッドロックが重量ロックに辺り、次のテーブルレベルロックモードがあります。

ACCESS SHARE(AS)
→selectで発生し、競合はAE
ROW SHARE
→select updateで発生し、競合はEとAE
※select updateは更新用のロックで、updateやdeleteを拒否します。
ROW EXCLUSIVE(RE)
→update,delete,insertで発生し、競合はS,SRE,E,AE
SHARE UPDATE EXCLUSIVE(SUE)
→vacuum,create indexで発生し、競合はSUE,E,AE
SHARE(S)
create indexで発生し、競合はRE,SUE,S,SRE,E,AE
SHARE ROW EXCLUSIVE(SRE)
→Postgresqlにはありません。
EXCLUSIVE(E)
→REFRESH MATERIALIZED VIEW CONCURRENTLYで発生し、競合はRS,RE,SUE,S,SRE,E,AE
ACCESS EXCLUSIVE(AE)
→DROP TABLE、TRUNCATE、REINDEX、CLUSTER、VACUUM FULLで発生し、全てと競合します。

軽量ロックは,内部的な共有管理リソースアクセスで使用されます。
SHAREDとEXCLUSIVEのテーブルレベルロックモードがあり、対象リソースに対する処理が完了後開放されます。
開発モードのパラメータ設定でログ出力可能です。

trace_lwlocks = on
軽量ロックの使用状況に関する情報を出力するか設定します。