世界に1つだけの「0001」こうしす! こちら京姫鉄道 広報部システム課 @IT支線(38)

情報セキュリティの啓発を目指した、技術系コメディー自主制作アニメ「こうしす!」の@ITバージョン。第38列車は「レースコンディション」です。※このマンガはフィクションです。

» 2023年06月29日 05時00分 公開

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

こうしす!」とは

ここは姫路と京都を結ぶ中堅私鉄、京姫鉄道株式会社。

その情報システム(鉄道システムを除く)の管理を一手に引き受ける広報部システム課は、いつもセキュリティトラブルにてんてこ舞い。うわーん、アカネちゃーん。

こうしす!@IT支線」とは

「こうしす!」制作参加スタッフが、@IT読者にお届けするセキュリティ啓発4コマ漫画。


今回の登場人物

akane

祝園アカネ(HOSONO Akane)

広報部システム課 係員。情報処理安全確保支援士。計画的怠惰主義者で、有休取得率は100%。しかし、困っている人を放っておけない性格が災いし、いつもシステムトラブルに巻き込まれる

mei

英賀保芽依(AGAHO Mei)

広報部広報課係員。天才的トラブルメーカーで、システム課やシステム子会社からは「アルティメットバグトリガー」として知られる。アカネの同期




第38列車:価値暴落


















井二かけるの追い解説

 マンガのテーマは、「レースコンディション」です。

 記念きっぷのシリアルナンバーのために、全駅共通の連番を新たに採番する仕組みを導入したところ、発売の瞬間に購入が集中し、シリアルナンバーが重複するきっぷが発行されてしまったという設定です。

 例えば、AさんとBさんが順番にシリアルナンバーを採番した場合、意図された通りに採番が行われます。

  1. 現在のシリアルナンバー=0
  2. Aさんが、現在のシリアルナンバー(0)を取得する
  3. Aさんが、取得したシリアルナンバー(0)+1で現在の番号を更新する
  4. 現在のシリアルナンバー=1(Aさんによる更新後)
  5. Bさんが、現在のシリアルナンバー(1)を取得する
  6. Bさんが、取得したシリアルナンバー(1)+1で現在の番号を更新する
  7. 現在のシリアルナンバー=2(Bさんによる更新後)

 しかしAさんとBさんが同時にシリアルナンバーを採番した場合は、以下のような事態が発生します。

  1. 現在のシリアルナンバー=0
  2. Aさんが、現在のシリアルナンバー(0)を取得する
  3. Bさんが、現在のシリアルナンバー(0)を取得する
  4. Aさんが、取得したシリアルナンバー(0)+1で現在の番号を更新する
  5. 現在のシリアルナンバー=1(Aさんによる更新後)
  6. Bさんが、取得したシリアルナンバー(0)+1で現在の番号を更新する
  7. 現在のシリアルナンバー=1(Bさんによる更新後)

 これによって、AさんとBさんは同じシリアルナンバーのきっぷを手にしてしまいます。

 このように複数のトランザクションが同一のデータに対して同時にアクセスすることにより、予期せぬ結果となってしまうことを「レースコンディション(競合状態)」といいます。

 マンガはフィクションなので、現実の券売機でこのようなことは起こらないかもしれません。しかし、一般の開発現場ではよく遭遇する代表的な問題の一つでしょう。

データベースで、トランザクション使っているから大丈夫……ではない

 レースコンディションへの対策として考えられる方法の一つとして、「リレーショナルデータベースのトランザクションを使用する」という方法があります。

  1. 現在のシリアルナンバー=0
  2. Aさんが、トランザクションを開始
  3. Bさんが、トランザクションを開始
  4. Aさんが共有ロック(読み取りロック)を取得し、現在のシリアルナンバー(0)を取得する
  5. Bさんが共有ロック(読み取りロック)を取得し、現在のシリアルナンバー(0)を取得する
  6. Aさんが、取得したシリアルナンバー(0)+1で現在の番号を更新するために排他ロック(書き込みロック)を取得しようとするが、Bさんの共有ロックがあるため、その解除を待機する
  7. Bさんが、取得したシリアルナンバー(0)+1で現在の番号を更新するために排他ロック(書き込みロック)を取得しようとするが、Aさんの共有ロックがあるため、その解除を待機する
  8. Aさんのトランザクションがタイムアウトで失敗する
  9. Bさんが排他ロック(書き込みロック)を取得し、更新&コミット
  10. 現在のシリアルナンバー=1(Bさんによる更新後)

 このように片側のトランザクションを失敗させることで、一貫性を保つことができる――というのが教科書の世界での出来事です。

 しかし現実の世界においては、ただトランザクションを使用すればいいというわけではありません。

 それは、使用するデータベース製品によってデフォルトの挙動が異なるからです。重要なキーワードは、「トランザクション分離レベル」(Transaction Isolation Level)です。

知名度の低い「トランザクション分離レベル」

 トランザクション分離レベルには、一般的には以下の4種類が存在します。

  1. READ UNCOMMITTED
  2. READ COMMITTED
  3. REPEATABLE READ
  4. SERIALIZABLE

 詳しい説明は他に譲りますが、これらの分離レベルの違いを一言で説明すれば、SELECT(取得)クエリで読み取る際のロックの仕方や、ロックを待機するかどうかです。

  • 「READ UNCOMMITTED」分離レベルでは、全くロックを取得せず、他のロックを待機しません
  • 「READ COMMITTED」分離レベルでは、排他ロック(書き込みロック)を待機します。データ取得時には、データを取得する際に共有ロック(読み取りロック)を取得しますが、完了後、即座に解放します
  • 「REPEATABLE READ」分離レベルや「SERIALIZABLE」分離レベルにおいては、データを取得する際に共有ロック(読み取りロック)を取得し、コミットするまで保持します。先ほどの例でいえば、「Aさんが、共有ロック(読み取りロック)を取得し、現在のシリアルナンバー(0)を取得する」がこれに当たります。その中でも「SERIALIZABLE」分離レベルではキー範囲ロック、「REPEATABLE READ」では行ロックを取得するという点で、微妙な違いがあります

 この中で、どのようなトランザクションでもレースコンディションを防ぎ一貫性を保てるのは、「SERIALIZABLE」分離レベルのみとなります。

 先ほどの例では、「SERIALIZABLE」分離レベルを前提としたものでした。しかしなぜそれが、教科書の世界の出来事なのでしょうか。

 それは、前提となるデフォルトのトランザクション分離レベルに違いがあるからです。

 教科書的には、デフォルトのトランザクション分離レベルは「SERIALIZABLE」とされることが一般的です。例えば、国内の標準規格であるJIS規格では、「ISO/IEC 9075-2:2011」を基にした「JIS X 3005-2:2015」が最新版ですが、これによると「トランザクション隔離性水準」(「トランザクション分離レベル」と同義)の既定が「SERIALIZABLE」であるとされているからです。

 しかし現実世界では、データベース製品によってデフォルトのトランザクション分離レベルが異なります。

 実際のところ、既定値を「SERIALIZABLE」としているリレーショナルデータベース製品は少数派です。筆者が探した限りでは1製品しか見つけられませんでした。これは「SERIALIZABLE」は同時実効性能が低く、パフォーマンスの問題から「READ COMITTED」分離レベルを既定値としている製品が多いことによります。

 そのため大半のデータベース製品においては、トランザクション分離レベルを明示的に指定せず、教科書的な挙動を期待してデフォルト設定のままトランザクションを使用しても、期待通りの挙動になりません。レースコンディション対策としてトランザクションを使用したつもりが、結果として対策になっていないというケースがあるのです。

 もし「レースコンディション対策のために、トランザクションを使用する」というようなことが仕様に記載されていた場合、単にトランザクションを使用するのでは不足があります。

 まずは、使用するデータベース製品のデフォルトのトランザクション分離レベルを調べる必要があります。

悩ましいパフォーマンスの問題

 一般的に「SERIALIZABLE」分離レベルは、パフォーマンスの問題があるとされています。

 これには2つの理由があります。1つは共有ロックをコミットされるまで保持すること、もう1つはデッドロックが発生しやすいことです。

 先ほどの例では、「Aさんのトランザクションがタイムアウトで失敗する」という事態が発生しました。

 これは、トランザクションがコミットされるまで共有ロックが保持されることによって起こる事象です。共有ロックは複数のトランザクションが同時に掛けられる読み取り専用のロックで、これが掛かっている間は、他のトランザクションは排他ロック(書き込みロック)を取得できません。そのため、タイムアウトが発生するまで待機し続けることになります。更新トランザクションが殺到している状況では、常に待機とリトライを繰り返す状況となり、状況は悪化する一方となります。

 ただ、そこで無思慮に「トランザクションは危険だ」とトランザクションの使用をやめたり、「SERIALIZABLEは悪い」とSERIALIZABLEトランザクション分離レベルの使用をやめたりするのは最悪手です。

 この問題に対するアプローチは3つあります。

1. トランザクションの最初から排他ロック(書き込みロック)を取得しておく

 1つ目は、データを取得してから更新するのではなく、データを更新してから取得するという方法です。

  1. 現在のシリアルナンバー=0
  2. Aさんが、「SERIALIZABLE」分離レベルでトランザクションを開始
  3. Aさんが排他ロック(書き込みロック)を取得し、現在のシリアルナンバー(0)を+1で更新する
  4. 現在のシリアルナンバー=1(Aさんによる更新後)
  5. Aさんがトランザクションをコミットする
  6. 以下略

 デッドロックが発生するのは、他のトランザクションが共有ロック(読み取りロック)を取得するからでした。

 採番処理のような単純なケースでは、上記のようにいきなり更新してから結果を取得することで、共有ロック(読み取りロック)を取得せずに排他ロック(書き込みロック)のみを取得して更新する方法で、デッドロックを防げます。

 余談になりますが、先ほど説明した通り、トランザクション分離レベルはあくまでもデータを取得する際のロックを制御するものでした。

 このように、いきなり更新クエリを実行する場合では、実は「SERIALIZABLE」分離レベルを使用する必要がない場合があります。「SERIALIZABLE」分離レベルは、他のトランザクションが行を挿入するのを防ぐためにキー範囲ロックを取得しますが、常に1行しかないテーブルで行の更新(UPDATE)のみで採番するのであれば、行の挿入を防ぐ必要がありません。このようなケースでは、そもそも「READ COMITTED」分離レベル以上であれば何でもいいということになります。

2. 飛び番を許容し、トランザクションを可能な限り短くすること、または自動採番列を使用すること

 次に考えられるのが、トランザクションを可能な限り短くすることです。

 現実の業務システムにおいては、採番するだけで処理が完結することはまれで、採番したシリアルナンバーを用いて他の更新処理を行う必要があります。

 その際、採番後、その他の更新を終えるまで同一のトランザクションで処理を実行し、全て成功すればコミット、失敗すれば採番も含めてロールバックするという仕様であれば、ロックを長時間取得したままとなってしまいます。ロックが長時間続けば、同時実効性能が低下するという結果となります。

 そこで、採番だけを別トランザクションとし、後続処理が失敗したときに飛び番を許容するという方法があります。必ずしも連続していなくてもよいのであれば、この方法を採るのがいいでしょう。

 実質的に同じことですが、自動採番列を使用するという方法もあります。

 データベース製品によって名前は異なりますが、列の属性にIDENTITY、AUTO_INCREMENTなどを付加することで、行を追加した際に、自動で採番される仕組みがあります。

3. トランザクションではなく楽観的ロックを使用する

 データの更新条件に、更新前のタイムスタンプや更新前の値を使用し、実際に更新された行数と、期待された更新行数とを比較する方法があります。

  1. 現在のシリアルナンバー=0
  2. Aさんが、現在のシリアルナンバー(0)を取得する
  3. (Bさんがシリアルナンバーを1に更新する)
  4. 現在のシリアルナンバー=1(Bさんによる更新後)
  5. Aさんが、現在のシリアルナンバー(0)が0であることを条件に、取得したシリアルナンバー(0)+1で、現在のシリアルナンバーを更新する
  6. 更新0件→エラーとする

 このような方法を「楽観的ロック」(optimistic locking)といいます。

 採番処理のように頻繁に更新されるような処理には楽観的ロックは向きませんが、Entity FrameworkのようなO/Rマッパーでは、標準の同時実行制御として採用されたりします。

まとめ

 たかが採番処理、されど採番処理。

 このように採番処理一つをとってみても、考慮するべきことがたくさんあります。トランザクションを使えば必ずしも安全とは限らず、トランザクション分離レベルの挙動や、トランザクションを使用しない方法など、さまざまな点を考慮して実装方法を検討しなければなりません。この機会にお使いのデータベース製品で挙動を試してみるのもいいかもしれません。

こうしす! #3.3「セキュリティに完璧を求めるのは間違っているだろうか(Part3/4)」(OPAP-JP公式)

Copyright 2012-2023 OPAP-JP contributors.
本作品は特に注記がない限りCC-BY 4.0の下にご利用いただけます


筆者プロフィール

原作:井二かける

アニメ「こうしす!」監督、脚本。情報処理安全確保支援士。プログラマーの本業の傍ら、セキュリティ普及啓発活動を行う。

著書:「こうしす!社内SE 祝園アカネの情報セキュリティ事件簿」(翔泳社)

ハックしないで監査役!! 小説こうしす!EEシリーズ 元社内SE祝園アカネ 監査役編 [1]」(京姫鉄道出版)

dアニメストアにて、アニメ『こうしす!EE』配信中。

解説:京姫鉄道

「物語の力でIT、セキュリティをもっと面白く」をモットーに、作品制作を行っています。


原作:OPAP-JP contributors

オープンソースなアニメを作ろうというプロジェクト。現在はアニメ「こうしす!」を制作中。


Copyright © ITmedia, Inc. All Rights Reserved.

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。