5.1 同時性の問題

少し遠い理論から始めましょう。

プログラマーが作成する情報システム (または単にアプリケーション) は、いくつかの典型的なブロックで構成されており、それぞれが必要な機能の一部を提供します。たとえば、キャッシュはリソースを大量に消費する操作の結果を記憶し、クライアントによるデータの読み取りを高速化するために使用されます。ストリーム処理ツールを使用すると、非同期処理のために他のコンポーネントにメッセージを送信できます。また、バッチ処理ツールは次の目的で使用されます。一定の周期性を持って蓄積されたデータ量を「レイク」します。

そして、ほぼすべてのアプリケーションで、データベース (DB) が何らかの形で関与しており、通常は 2 つの機能を実行します。つまり、ユーザーから受信したデータを保存し、後で要求に応じてデータを提供します。既製のソリューションがすでにたくさんあるため、独自のデータベースを作成しようと考える人はほとんどいません。しかし、アプリケーションに適したものをどのように選択すればよいでしょうか?

そこで、以前に保存した家の周りのタスクのリストをロードできる、つまり、データベースから読み取って新しいタスクを追加し、それぞれの特定のタスクに優先順位を付けることができる、モバイル インターフェイスを備えたアプリケーションを作成したと想像してみましょう。タスク - 1 (最高) から 3 (最低)。モバイル アプリケーションを一度に 1 人だけが使用するとします。しかし今、あなたは思い切って自分の作品について母親に話しました。そして今、母親は 2 人目の定期ユーザーになりました。同時に、同じミリ秒内に、あるタスク (「窓を洗う」) に異なる優先度を設定することを決定した場合はどうなるでしょうか?

専門用語で言えば、あなたと母親のデータベース クエリは、データベースにクエリを実行する 2 つのプロセスと考えることができます。プロセスは、1 つ以上のスレッドで実行できるコンピューター プログラム内のエンティティです。通常、プロセスにはマシン コード イメージ、メモリ、コンテキスト、その他のリソースがあります。言い換えれば、プロセスはプロセッサ上でのプログラム命令の実行として特徴付けることができます。アプリケーションがデータベースにリクエストを送信するとき、これはデータベースが 1 つのプロセスからネットワーク経由で受信したリクエストを処理するという事実について話しています。同時に 2 人のユーザーがアプリケーションに座っている場合、特定の瞬間に 2 つのプロセスが存在する可能性があります。

あるプロセスがデータベースにリクエストを行うと、データベースが特定の状態にあることがわかります。ステートフル システムは、以前のイベントを記憶し、「状態」と呼ばれる何らかの情報を保存するシステムです。として宣言された変数は、integer0、1、2、あるいは 42 の状態を持つことができます。ミューテックス(相互排他) には、バイナリ セマフォ (「必須」と「解放」) と同様に、ロックまたはロック解除の2 つの状態があり、通常はバイナリです。 1 または 0 の 2 つの状態のみを持つことができる (バイナリ) データ型と変数。

状態の概念に基づいて、有限オートマトン (1 つの入力と 1 つの出力を持ち、各時点で有限セットの状態の 1 つにあるモデル) や「状態」など、いくつかの数学的および工学的構造が基礎となります。 」設計パターンでは、内部状態に応じて (たとえば、1 つまたは別の変数にどのような値が割り当てられているかに応じて) オブジェクトの動作が変わります。

したがって、マシン世界のほとんどのオブジェクトには、時間の経過とともに変化する可能性のある状態があります。たとえば、大きなデータ パケットを処理するパイプラインがエラーをスローして失敗したり、ユーザーの残高を保存する Wallet オブジェクト プロパティなどですアカウント、給与受け取り後に変更されます。

ある状態から別の状態への遷移 (「遷移」)、たとえば、進行中から失敗した状態への遷移は、操作と呼ばれます。おそらく誰もがCRUD操作( createreadupdatedeleteまたは同様のHTTPメソッド、POSTGETPUT)を知っているでしょうDELETE。しかし、プログラマは、コード内の操作に別の名前を付けることがよくあります。操作は、データベースから特定の値を読み取るだけよりも複雑になる可能性があるためです。データをチェックしたり、関数の形式をとった操作をチェックしたりすることもできるためです。たとえば、validate()これらの操作機能は誰が実行するのでしょうか? プロセスはすでに説明されています。

もう少し詳しく読めば、私が用語をこれほど詳しく説明した理由がわかるでしょう。

関数であっても、分散システムでは別のサーバーにリクエストを送信する操作であっても、すべての操作には 2 つのプロパティがあります。呼び出し時間と完了時間(完了時間)です。これらの時間は呼び出し時間よりも厳密に長くなります (Jepsen の研究者)これらのタイムスタンプの両方には、完全に同期された、世界的に利用可能な架空のクロックが与えられるという理論的な仮定に基づいています)。

To Do リスト アプリケーションを想像してみましょう。あなたは のモバイル インターフェイスを通じてデータベースにリクエストを出し14:00:00.014、母親は13:59:59.678(つまり 336 ミリ秒前に)同じインターフェイスを通じて To Do リストを更新し、皿洗いを追加しました。ネットワークの遅延とデータベースのタスクのキューを考慮すると、あなたと母親に加えて、母親の友人全員もアプリケーションを使用している場合、データベースはあなたのリクエストを処理した後に母親のリクエストを実行できます。言い換えれば、あなたの 2 つのリクエストと、母親のガールフレンドからのリクエストが同じデータに同時に (同時に) 送信される可能性があります。

そこで、データベースと分散アプリケーションの分野で最も重要な用語である同時実行性に到達しました。2 つの操作の同時性とは、正確には何を意味するのでしょうか? 何らかの操作 T1 と何らかの操作 T2 が与えられた場合、次のようになります。

  • T1 は実行 T2 の開始時刻より前に開始でき、T2 の開始時刻と終了時刻の間に終了できます。
  • T2 は T1 の開始時刻より前に開始でき、T1 の開始と終了の間に終了できます。
  • T1 は、T1 実行の開始時刻と終了時刻の間に開始および終了できます。
  • T1 と T2 が共通の実行時間を有するその他のシナリオ

この講義の枠組みの中で、主にデータベースに入るクエリと、データベース管理システムがこれらのクエリをどのように認識するかについて話していることは明らかですが、たとえばオペレーティング システムのコンテキストでは、同時実行性という用語が重要です。この記事の主題から大きく逸脱することはありませんが、ここで話している同時実行性は、コンテキストで説明されている同時実行性と同時実行性のジレンマとその違いとは無関係であることに言及することが重要だと思います。オペレーティング システムとハイパフォーマンス コンピューティングの開発。並列処理は、複数のコア、プロセッサ、またはコンピューターを備えた環境で同時実行性を実現する 1 つの方法です。私たちは、さまざまなプロセスが共通のデータに同時にアクセスするという意味での同時実行性について話しています。

そして、純粋に理論的に、実際に何が問題になる可能性があるのでしょうか?

共有データを操作する場合、「競合状態」とも呼ばれる、同時実行性に関連するさまざまな問題が発生する可能性があります。最初の問題は、プロセスが受信すべきでないデータ、つまり不完全なデータ、一時的なデータ、キャンセルされたデータ、またはその他の「不正な」データを受信したときに発生します。2 番目の問題は、プロセスが古いデータ、つまりデータベースの最後に保存された状態に対応しないデータを受信した場合です。あるアプリケーションがユーザーのアカウントから残高ゼロでお金を引き出したとします。これは、データベースがアプリケーションにアカウントのステータスを返し、ほんの数ミリ秒前に行われた最後のお金の引き出しは考慮されていないためです。状況はまあまあですよね。

5.2 トランザクションが私たちを救ってくれた

このような問題を解決するために、トランザクションという概念が登場しました。これは、論理的には単一の操作である、データベースに対する一連の一連の操作 (状態変更) です。もう一度銀行の例を挙げますが、これは偶然ではありません。なぜなら、トランザクションの概念は、明らかに、まさにお金を扱うという文脈で登場したからです。トランザクションの典型的な例は、ある銀行口座から別の銀行口座への送金です。最初に送金元の口座から金額を引き出し、それから目的の口座に入金する必要があります。

このトランザクションを実行するには、アプリケーションはデータベース内でいくつかのアクションを実行する必要があります。つまり、送信者の残高の確認、送信者のアカウントの金額のブロック、受信者のアカウントへの金額の追加、送信者からの金額の差し引きです。このような取引にはいくつかの要件があります。たとえば、アプリケーションは、残高に関する古い情報や不正確な情報を受け取ることはできません。たとえば、並行トランザクションが途中でエラーで終了し、資金が口座から引き落とされなかった場合などです。アプリケーションはすでに情報を受け取っています。資金が帳消しになったこと。

この問題を解決するために、トランザクションの「分離」などの特性が必要とされました。トランザクションは、同時に実行されている他のトランザクションがないかのように実行されます。私たちのデータベースは、あたかも次々と順番に実行しているかのように同時操作を実行します。実際、最も高い分離レベルはStrict Serializableと呼ばれます。はい、最高です。これは、いくつかのレベルがあることを意味します。

「やめて」とあなたは言います。馬を抱いてください、先生。

各操作には呼び出し時間と実行時間があることを説明したことを思い出してください。便宜上、呼び出しと実行を 2 つのアクションとして考えることができます。すべての呼び出しおよび実行アクションのソートされたリストをデータベースの履歴と呼ぶことができます。したがって、トランザクション分離レベルは一連の履歴になります。どのストーリーが「良い」かを判断するために分離レベルを使用します。ストーリーが「連載可能性を損なう」または「連載可能ではない」と言う場合、そのストーリーが連載可能なストーリーのセットに含まれていないことを意味します。

どのような話について話しているのかを明確にするために、例を挙げて説明します。たとえば、このような種類の履歴 -中間読み取り があります。この問題は、トランザクション A が、実行中の別のトランザクション B によって変更され、まだコミットされていない (「未コミット」) 行からデータを読み取ることが許可されている場合に発生します。つまり、実際には、変更が最終的にコミットされていないということです。トランザクション B はいつでもキャンセルできます。そして、たとえば、中止された読み取りは、キャンセルされた引き出しトランザクションの単なる例です。

いくつかの異常が考えられます。つまり、異常とは、データベースへの競合アクセス中に発生する可能性がある、ある種の望ましくないデータ状態です。また、特定の望ましくない状態を回避するために、データベースはさまざまなレベルの分離、つまり望ましくない状態からのさまざまなレベルのデータ保護を使用します。これらのレベル (4 つの部分) は、ANSI SQL-92 標準にリストされています。

一部の研究者にとって、これらのレベルの説明は曖昧に見えるため、独自のより詳細な分類を提供しています。すでに述べた Jepsen と、MySQL や PostgreSQL などの特定の DBMS によって提供される分離レベルを正確に明らかにすることを目的とした Hermitage プロジェクトに注目することをお勧めします。このリポジトリからファイルを開くと、特定の異常についてデータベースをテストするためにどのような SQL コマンドが使用されているかを確認でき、関心のあるデータベースに対して同様の操作を行うことができます。興味を引くために、リポジトリからの 1 つの例を次に示します。

-- Database: MySQL

-- Setup before test
create table test (id int primary key, value int) engine=innodb;
insert into test (id, value) values (1, 10), (2, 20);

-- Test the "read uncommited" isolation level on the "Intermediate Reads" (G1b) anomaly
set session transaction isolation level read uncommitted; begin; -- T1
set session transaction isolation level read uncommitted; begin; -- T2
update test set value = 101 where id = 1; -- T1
select * from test; -- T2. Shows 1 => 101
update test set value = 11 where id = 1; -- T1
commit; -- T1
select * from test; -- T2. Now shows 1 => 11
commit; -- T2

-- Result: doesn't prevent G1b

同じデータベースについては、原則として、複数の種類の分離のいずれかを選択できることを理解することが重要です。最強の断熱材を選んでみませんか? コンピューター サイエンスのあらゆるものと同様、選択した分離レベルは、準備ができているトレードオフに対応する必要があります。この場合、実行速度のトレードオフです。分離レベルが強いほど、リクエストは遅くなります。加工された。必要な分離レベルを理解するには、アプリケーションの要件を理解する必要があります。また、選択したデータベースがこのレベルを提供しているかどうかを理解するには、ドキュメントを調べる必要があります。ほとんどのアプリケーションではこれで十分ですが、特に厳しい要件がある場合は、エルミタージュ プロジェクトの人たちが行っているようなテストを手配する方がよいでしょう。

5.3 ACID の「I」とその他の文字

一般的に ACID について話すとき、隔離とは基本的に意味します。私がこの頭字語の分析を単独で始めたのはこのためであり、この概念を説明しようとする人が通常行うように順序立てて分析したわけではありません。では、残りの 3 文字を見てみましょう。

銀行振込の例をもう一度思い出してください。ある口座から別の口座に資金を移動するトランザクションには、最初の口座からの出金操作と 2 番目の口座での補充操作が含まれます。2 番目のアカウントの補充操作が失敗した場合は、最初のアカウントからの引き出し操作が発生することを望まないでしょう。つまり、トランザクションは完全に成功するか、まったく起こらないかのどちらかですが、一部だけ成功することはありません。この特性は「アトミック性」と呼ばれ、ACID の「A」に相当します。

トランザクションが実行されると、他の操作と同様に、データベースが 1 つの有効な状態から別の有効な状態に転送されます。一部のデータベースでは、いわゆる制約、つまり、主キーまたは副キー、インデックス、デフォルト値、列の型などに関して、保存されたデータに適用されるルールが提供されています。したがって、トランザクションを行うときは、これらの制約がすべて満たされていることを確認する必要があります。

この保証は、ACID では「一貫性」と呼ばれますC(後で説明する分散アプリケーションの世界からの一貫性と混同しないでください)。ordersACID の意味での一貫性について、明確な例を示します。オンライン ストアのアプリケーションはテーブルに行を追加したいと考えており、テーブルのIDが列- 典型にproduct_id示されます。productsforeign key

たとえば、製品が品揃えから削除され、それに応じてデータベースからも削除された場合、行挿入操作は実行されるべきではなく、エラーが発生します。私の意見では、この保証は他の保証と比較して、少し突飛なものです。データベースからの制約を積極的に使用するということは、データに対する責任を変更することを意味するからです (ビジネス ロジックの部分的な変更も同様です)。 CHECK などの制約をアプリケーションからデータベースに適用します。これは、今言われているように、まさにそのとおりです。

そして最後に、D「抵抗」(耐久性)が残ります。システム障害またはその他の障害によって、トランザクション結果やデータベースのコンテンツが失われることがあってはなりません。つまり、データベースがトランザクションが成功したと応答した場合、これはデータが不揮発性メモリ (ハードディスクなど) に記録されたことを意味します。ちなみに、これは、次の読み取りリクエストでデータがすぐに表示されるという意味ではありません。

つい先日、私は AWS (アマゾン ウェブ サービス) の DynamoDB を使用していて、保存するためにデータを送信し、応答HTTP 200(OK) などを受け取った後、それを確認することにしました - しかし、これは表示されませんでした次の 10 秒間、データベース内のデータを保存します。つまり、DynamoDB はデータをコミットしましたが、すべてのノードが即座に同期してデータの最新コピーを取得したわけではありません (ただし、データはキャッシュ内にある可能性があります)。ここで私たちは分散システムのコンテキストにおける一貫性の領域に再び登りましたが、それについて話す時期はまだ来ていません。

これで、ACID 保証が何であるかがわかりました。そして、それらがなぜ役立つのかもわかっています。しかし、本当にすべてのアプリケーションにそれらが必要なのでしょうか? そうでない場合、正確にはいつですか? すべての DB はこれらの保証を提供しますか? そうでない場合、代わりに何を提供しますか?