大西彰のウェブログ

データベース系技術ネタ、国際化技術ネタなど、徒然なるままに

目次

Blog 利用状況

ニュース


こんにちは。大西 彰です。
私のブログでは、データベース技術、ソフトウェアの国際化などを取り扱っています。ニッチだけど重要なネタがつまっています。
ブログの内容は無保証です。
また、本ブログでの発言やコメントは、マイクロソフトの正式な見解またはコメントではありません。



マイクロソフトライセンスセンター
マイクロソフトライセンスセンター
マイクロソフトライセンスセンター
ウィルコムストア
ソースネクスト
デル株式会社
アフィリエイト リンクシェア ブログ 携帯対応 成果報酬 広告 テンプレート ブログパーツ

テクノラティプロフィール

記事のカテゴリ

過去の記事

カテゴリ

イメージギャラリ

My blog

Visual FoxPro

Visual Studio

Web Sites

Windows Vista

ブログ

免責事項

楽観的ロックでいいじゃん!

データベースというよりは、トランザクション処理ネタなのですが、皆さんは、データベースのトランザクション処理を実行される場合、対象となる行をどのように排他制御されていますか。排他制御というのは、同時実行処理において必要不可欠なものなのですが、使い方を誤ると簡単なはずの処理が難しくなってしまう可能性があります。一般的にリソース(資源)をロックする必要がある場合、2種類の方法が選択できます。
・悲観的ロック(Pessimistic lock)
・楽観的ロック(Optimistic lock)
この記事では、トランザクション処理の大半は「楽観的ロックでいいじゃん!」という話を書いてみたいと思います。

スタンドアロンでプログラムが動いていた時代は、1ユーザ、1プログラム、1データのみが存在するため、排他制御ということを考えなくてもアプリケーションの開発は行えました。コンピュータがネットワークでつながるようになって、データというリソースが複数のユーザからアクセスされるようになると、排他制御ということを無視してプログラムを書くことはできなくなりました。マルチユーザをサポートするOSのファイルシステムは、ファイルの部分的なロックを提供し、アプリケーションがリソースを更新する際に、排他制御をかけられるようにサポートしてくれます。ファイルシステム上のカスタムファイルで排他制御を実行された経験がある開発者というのは、最近ではかなり稀な部類だと思います。DBMSというものが利用できるようになって、ファイルレベルでの同時実行制御から、データベースレベル、テーブルレベル、ページレベル、あるいは行レベルといったロックのメカニズムが提供され、アプリケーションプログラマは、低レベルのファイルアクセスAPIを気にすることなく、論理的なデータの集合を排他制御できるようになりました。今、時代は、クライアント・サーバ型に代表されるネットワークの信頼性が確保できている環境でのトランザクションから、HTTPというとても不安定なプロトコルの上でのトランザクションが求められています。どのようなアーキテクチャでアプリケーションが開発されるにせよ、ロックの問題はきちんと理解した方がいいでしょう。

悲観的ロック、楽観的ロック、これらをきちんと理解して使い分けている開発者の人はどれくらいいるのでしょうか。話を進める前に、これらの違いを簡単に整理してみましょう。難しい説明は、トランザクション処理の参考書籍を読めばいくらでも出てきますので、ここでは簡単にまとめてみましょう。

悲観的ロック:ステートフルなロック
更新したい対象のリソースを照会して取得した直後から更新が終わるまでロックを維持すること => ロック時間は長時間で、ロックは独占的

楽観的ロック:(ほぼ)ステートレスなロック
更新したい対象のリソースを照会してもロックはかけず、本当に更新が必要になった段階でその対象リソースをロックすること => ロック時間は短時間で、ロックは非独占的

悲観的ロックというのは、「俺様が更新するリソースは全部俺様のものだ!他人にはアクセスさせないぞ!」というものなので、悲観的ロックが実行されると、リソースを管理しているシステムがデータベースの場合、DBMS上で、ロックを実行したユーザアカウントのコンテキストでロックが解放されるまで、排他制御が実行され続けます。悲観的ロックのメリットは単純にまとめるとひとつしかありません。ロックを取得したユーザが見ているリソースが他者から変更されないことを保障する、ということです。反対に考えると、悲観的ロックのデメリットが見えてきます。ロックが維持されている間、他のユーザはそのリソースにアクセスできないということになります。また、ロックを維持することにより、デッドロックを引き起こしやすくなる可能性が高いことも上げられます。DBMSの実装によっては、この悲観的ロックが発生している間も読み取りだけは認めるような処理系があります。細かな話は「分離レベル(Isolation level)」といった話になるので、ここでは省略しますが、この記事で提示したいのは、「そのロック、本当に悲観的じゃなきゃいけないの?」という疑問です。

前置きが長くなりましたが、ここからが本題です。ここからは、DBMSにおけるトランザクションに限定して話を進めていきます。
「楽観的ロックでいいじゃん!」と思える理由は主に4つ挙げられます。

(理由1)ユーザの認証と認可をきちんと処理していれば、更新の競合というのはほとんどありえない
(理由2)業務上、同一行を複数人で同時に更新するプロセスはほとんど考えられない
(理由3)動的に変化するデータを対照表の形で作成してしまえば、追加のみなので、ロックは必要ない
(理由4)データの所有権を管理すれば、更新の競合は、ほとんどありえない

それぞれを見ていきましょう。

理由1について:
データベースを設計する際に、考えなければならないことが「ユーザの認証」と「ユーザの権限」の話です。データベースに明るくない人がやってしまう悪い例は、何でもできる特権を持ったユーザアカウントですべてのデータベースオブジェクトを作り、アプリケーションからのアクセスもそのアカウントで実行してしまう、というものです。認証はやっていても認可(権限チェック)をやっていないという方もいらっしゃるかと思います。せめて次の段階くらいは意識された方がいいと思います。

Level0: アクセス許可なし(読み取り・書き込みともに不可)
Level1: 読み取り専用(書き込みは不可)
Level2: 読み取り・書き込み可能
Level3: 特定データベースの変更・構成可能
Level4: 全権限の所有

ちょっと話を横道に・・・実際のデータベースセキュリティは、ロールモデルなどで細かく設定することができるわけですが、理想的にはデータベース設計者がデータベースを完成させたら、権限をDBAに委譲して、データベース設計者からすべての権限を奪うというのが望ましいです。欧米のデータベースマネージメントでは一般的に行われています。設計した人がシステムのリリース後も変更権限を所有しているようでは不正が簡単に行えるためです。日本の大手企業の場合、運用をSIerに丸投げしているケースが多いかと思いますが、これは「情報漏えい喜んで!」って言っているようなものです。

さて、少なくともユーザに関して、Level0からLevel2までの範囲で権限を設定していれば、無駄なトランザクションの発生を抑えられます。たとえば、Level0、Level1のユーザに対しては、更新トランザクションというものが必要ありません。業務の観点で考えると、Level2のユーザというのはそれなりに職位的にも権限があるユーザにマップされることになるでしょう。たとえば、伝票の入力処理を実行する人は「基幹業務」に携わっています。単なるオペレータとして軽んじることなく、重要な職位として認められるべきだと思います。

理由2について:
業務システムにおいて、情報が更新されるためには、何らかのビジネスプロセスと対応している必要があります。たとえば、「人事情報の変更」というものを考えた場合、人事部に変更届を提出するのが普通だと思います。このときの媒体は何であってもかまいません。変更というイベントが認知される証拠が必要という話です。何らかの届けが提出され、それが認知されて処理されるにあたっては、「審査」というワークフローを通る必要があるかと思います。したがって、審査が通るまでは、トランザクション処理は実行されることがありません。審査が通れば、更新権限を持ったユーザのコンテキストにより、トランザクションが実行されることになります。しかし、このような「変更」-「審査」といった業務フローの場合、同じ行が複数の人から同時に更新されることは起こりえません。
業務における「変更」には、必ず権限を持つ人の「承認・審査」が伴うはずです。この制約がある限り、「変更」は即座には実行されず、「変更要求」-「承認・審査」-「変更実行あるいは変更の却下」の流れになります。この承認・審査プロセスを含めてデータベース化するならば、直接行を更新するという流れにはならず、後述の対照表を用いた更新履歴の管理が求められるはずです。

理由3について:
たとえば、小売店における商品の売価を考えて見ましょう。商品というエンティティを用意して、そこに売価を入力して、更新するというやり方は、後に営業分析する際に売価の動きを追跡することが難しくなります。売価を対照表で管理するとたとえば、次のように管理できます。

  商品           商品の売価        店舗    
+--------------------+  +--------------------+  +--------------------+
| 商品ID(PK)    |  | 商品ID(FK)    |  | 店舗ID(PK)    |
+--------------------+  | 店舗ID(FK)    |  +--------------------+
| 商品名      |--●| 発生日時     |●--| 店舗名      |
| ・以下省略・・  |  +--------------------+  | ・以下省略・・  |
|          |  | イベント     |  |          |
|          |  | 売価       |  |          |
|          |  | ・以下省略・・  |  |          |
+--------------------+  +--------------------+  +--------------------+

このモデルに従うと、商品の売価においては、変更が生じても、データベース側では「行の追加」しか発生しません。変更イベントが発生しても商品の売価という対照表の行を更新するということは起こりえないのです。したがって、楽観的ロックすら必要がないのです。(注: 最新の売価をどうやって取得するか、その実装によっては、ロックが必要な場合もありますが、悲観的ロックである必要はなく、楽観的ロックで十分です)

理由4について:
これは、データベースアクセスを実行するビジネスロジックに依存しますが、アプリケーションによって行に所有権を設定し、所有権のある行だけを処理の対象とすることを考えます。


  エンティティ   
+--------------------+
| 主キー(PK)    |
+--------------------+
| 所有者ID     |
| ・以下省略・・  |
|          |
|          |
|          |
+--------------------+

所有者IDをUPDATEステートメントのWHERE句に含めることで、他の所有者の行を更新することがなくなります。行の所有者を明確にしているので、悲観的ロックにすることなく、楽観的ロックで十分になります。しかし、所有者の変更を追跡しようとすると、多対多のモデルになるので、結局は次のような構造に帰着します。

  テーブルA       テーブルAの所有者       ユーザ    
+--------------------+  +--------------------+  +--------------------+
| キーA(PK)    |  | キーA(FK)    |  | ユーザID(PK)   |
+--------------------+  | ユーザID(FK)   |  +--------------------+
|          |--●| 発生日時     |●--|          |
| ・以下省略・・  |  +--------------------+  | ・以下省略・・  |
|          |  | イベント     |  |          |
|          |  | ・以下省略・・  |  |          |
|          |  |          |  |          |
+--------------------+  +--------------------+  +--------------------+


したがって、理由3の繰り返しになり、行を更新するという話がなくなります。

・・・
メインフレームに代表されるTSSによる集中処理から、クライアントサーバの分散型、そしてWebシステムにおける集中処理型、スマートクライアントによる分散型、と時代は集中・分散を繰り返しています。しかし、どのようなアーキテクチャが採用されるにせよ、コンピュータのメモリは有限であり、電源を落とせばDRAMはクリアされてしまうため、最終的にはデータをどこかの2次記憶装置に永続化する必要があります。この永続化を正しく行うためにトランザクション処理があるのですが、工夫次第で、トランザクションを軽量なものにすることができます。最終的に一番負荷がかかるのはやはりデータベースが稼動しているサーバであり、スケーラビリティやパフォーマンスを求めるのであれば、いかにサーバリソースを消費しないで同時実行性を高めるか、ということにつきると思います。

12-13年前にCA-ClipperというXbaseのシステムで悲観的ロックモデルによりアプリケーションを作成した際に、このロックモデルの欠点がよくわかりました。ネットワークのトラフィックの増大、アプリケーションパフォーマンスの低下を招くことが低性能のPCで証明されてしまったからです。低性能のPCとはi286レベルのPC/XTだったのですが、結局、楽観的ロックに切り替えられるよう、クライアントでトランザクション用のキューを作り、サーバ側にバッチアップデート要求を出すような作りにした記憶があります。Xbaseだけでなく、MS-DOSのファイルシステムレベルでの排他制御も経験していますが、やはりロック時間が長くなれば、SHARE.EXEのプロセスに影響を与え、パフォーマンスが低下することがわかりました。ロック時間は短いほうがいいんです。

SQLを利用するDBMSにおいても、結局理屈は変わりません。行ロックや更新専用のカーソルの作成というのは、技術的には重要ではありますが、必要不可欠なものとは言えないと思います。ユーザのロールモデル、業務上のワークフロー、データベースの設計方法を工夫するだけでも「悲観的ロック」から逃れることができると思います。SOAのアプローチになって、サービスとの疎結合が求められると、「楽観的ロック」中心のアプローチでないとサービスの開発ができないように思います。SOAPを使おうが、MQを使おうが、最終的にはデータベース上の永続化にたどり着きます。永続化処理を軽量にすることが今後も求められると思います。

皆さんも「楽観的ロックでいいじゃん!」と思いませんか。

投稿日時 : 2004年10月19日 14:58

コメントを追加

# re: 楽観的ロックでいいじゃん! 2004/10/19 19:25 WR

個人的な判断と感覚では、楽観的オプティミスティックロックを中心に使用すべきと思っていたのですが、残念ながら実務経験が圧倒的に少ないため、この判断と感覚を裏付ける事例が少なく、すこし不安に思っておりました。

この大西さんのエントリを読むことで、自身の判断と感覚に強力な裏づけをいただいたような気分ですし、実際そうでしょう。ありがとうございました!

# 楽観的ロックでいいじゃん! 2004/10/19 22:38 中の技術日誌

皆さん読むべし。

# re: 楽観的ロックでいいじゃん! 2004/10/20 0:20 大西 彰

WRさん、中さん、コメントありがとうございます。

この記事は、データベースの実装形態を特定しない(利用するデータベース管理システムを特定しない)で議論しているので、あえて抽象的にまとめている部分があります。PASSJなのでSQL Serverのコンテキストでまとめてしまってもよかったのですが、他のDBMSのユーザのことも考慮して意図的にぼかしております・・・(^^;

# re: 楽観的ロックでいいじゃん! 2004/10/20 23:42 稍丼

はじめまして

どうしても,悲観的ロックに固執する人に
「そんなに固執する必要はない」と説得するには

・悲観的ロック(Pessimistic lock)
・楽観的ロック(Optimistic lock)

という概念でなく

・悲観的同時実行制御 (optimistic concurrency control)
・楽観的同時実行制御 (pessimistic concurrency control)

という概念でわけて説得した方が,いいような気がします。
また,非接続型での制御の場合でも
きれいに概念分けできて,受け入れられるような気がします。

非接続型の場合,基本的には,データベースが用意する
いわゆる「悲観的ロック」で排他する方法は利用できません。

その場合でも,レコードのカラムに ロックフラグ や
利用ユーザー名や更新日時等を用意することによって,
悲観的同時実行制御が可能になります。

つまり,必ずしも

 楽観的ロック = 楽観的同時実行制御

ではないわけで,意識しないといけないのは,
悲観的ロック で行くのか
楽観的ロック で行くのか
ということではなく,
悲観的同時実行制御 で行くのか
楽観的同時実行制御 で行くのか
ということのような気がします。

ロックは単なる手段であって,同時実行制御が重要なわけで,
あなたが固執したいのは,実は,
悲観的ロック ではなく 悲観的同時実行制御 なんですよ。
だから,悲観的ロックにこだわらなくてもいいんですよ。と。

結果的に,
「そうか,俺がやりたかったのは,悲観的ロックではなく
悲観的同時実行制御だったのかぁ。だったら,
楽観的ロックでいいじゃん!」
となっていいんじゃないでしょうか?...

# re: 楽観的ロックでいいじゃん! 2004/10/20 23:45 稍丼

訂正

・悲観的同時実行制御 (optimistic concurrency control)
・楽観的同時実行制御 (pessimistic concurrency control)

は,

・悲観的同時実行制御 (pessimistic concurrency control)
・楽観的同時実行制御 (optimistic concurrency control)

です。

# re: 楽観的ロックでいいじゃん! 2004/10/21 1:04 大西 彰

稍丼さん、

コメントありがとうございます。
厳密にはConcurrency controlのコンテキストで話をまとめればよかったのですが、あまり難しく語りたくなく、データベースのコンテキストでいう「ロック」で語っています。

同時実行制御において、結局はリソースのロックに焦点が当たるので、ロックを中心に話をしているだけに過ぎません。この記事ではlockとconcurrency controlを同意として捉えています。

ロックフラグといったものを用意することの必要性についてはここでは議論しないほうがいいと思います。私は、ロックフラグなんか使わなくても同時実行制御が実行できればいいと思うからです。開発者の都合で余計なカラムを増やす必要はないかと思うので・・・。ロックフラグなんてものを導入したら、悲観的ロックでないと駄目になりませんか?私はそれで破綻したプロジェクトを見たことがありますが・・・。ロックフラグを中途半端に更新したまま行が放置されたらどうします?ネットワークの切断、アプリケーションの強制終了、いろんな要因でロックしたままの行が放置されたら・・・。アプリケーションでロックを管理すると、ロックの不整合をアプリケーションで面倒みる必要があります。

なんにせよ、私が問いかけたいのは、難しいことはやめてシンプルにやりましょうよ、ということです。排他制御による同時実行制御は本来単純なものであるべきです。それを変にDBMSやミドルウェアの力を借りてテクニカルな興味に走ってしまうと、「動作するけどサーバに高い負荷をかけるトランザクション処理」が簡単に実現してしまい、パフォーマンスがでないというジレンマに陥ると思います。

# re: 楽観的ロックでいいじゃん! 2004/10/21 12:22 ICHIOKA

大西さん たいへん興味深く読ませて頂いております。
昔(いやな表現?)、大西さんに「下手なロックかけるにゃ足りず」と教わって以来ずっと実践。

同時という概念をどこに置くかということなんだと考えます。
たとえ多数のクライアントから「同時」に発信されたとしてもネットワークを流れて、サーバに辿り付くパケットは整列しデータに傷があればチェックサムが通らないという電気的制御が加わってしまいます。安全な信号が整列して届くというのは、同時ではありません。ビジネスロジックを考慮すれば、書き込みに関しては、アンロックでアペンドしていけば良いのだと考えます。これは一見、物理記憶層の消費と言うように考えられますが、このデータに記録時間とIDを絡めれば、人為エラーについてのトレーサビリティが十分確保できます。実務ではハードウェア障害やケーブルアクシデントよりも、まさに人為エラーが一番多いからなので、この部分の補償を十分に行えます。
即時に参照したい集計結果というものは、通常あまり無いので、分析/集計は読み取り専用に出せばよいわけです。(これついては大西さんのタイトルにありますが。関係無いかな?)
本題からはずれますが、悩みは、同時という問題には、クライアントで参照しているデータが、「いつのもの」であるかということが絡んできます。「いつのもの」を長時間、現在でも正当にしておくことがたいへん難しい。

# re: 楽観的ロックでいいじゃん! 2004/10/21 13:58 大西 彰

ICHIOKAさん、コメントありがとうございます。

ご指摘の通り、クライアント側で見ている行データが最新なのか過去なのかを判断するためには「再読み込み」が不可欠になってしまいます。

オブジェクト指向データベースの中には、取得しているオブジェクトの状態変化をサーバ側からクライアントに通知する機能を有したものがあります。このような処理系の場合、クライアント側は他人により変更されたことをイベントとして受け取ることができます。結果としてサーバ側で監視プロセスが動くわけですので、サーバには余計な負荷をかけることになります。
リレーショナルデータベースにおいても、ダイナミックカーソルをサポートしていれば、最新の情報がカーソルに反映されるというのがあるかと思います。しかしユーザインターフェイスに対して表示を更新する必要があるので、カーソルからデータを再読み込みする必要があります。

重要なのは、同一行を複数人で同時に更新するという業務はどのようなものがあるかを整理することだと思います。
有限区間で時不変なデータもあれば、時可変なデータも存在します。しかし、時可変なデータは履歴データとして、複数行で変化を管理すべきではないかと思います。一方、時可変なデータを1行で管理してしまうと、悲観的ロックが必要になります。結果として時系列の分析は困難になり、同時実行性の問題で悩むことになるかと思います。

# re: 楽観的ロックでいいじゃん! 2004/10/21 17:41 大西 彰

この記事が結構読まれているようで、他のサイトで突込みが入っているのも見かけました。Webベースのオンラインショッピングにおける「在庫の引き当て」問題。これは、悲観的ロックでも実行しづらいケースだと思います。
ショッピングカートに商品を入れるのは自由。でも「冷やかし」もあるわけで、カートに入れた段階で在庫の引き当てが実行できない・・・。もしカートに入れた段階で在庫の引き当てが行われていたら、ぞっとします。いたずらに在庫が引き当てられて他人のショッピングを簡単に妨害できるわけですから。
在庫の引き当てを即座に実行しないことにより、ショッピング中には在庫があったのに、精算時に「在庫切れ」という可能性は避けられません。そのあたりは、ショッピングサイトのサービスレベルで定義する必要があるかと思います。

# re: 楽観的ロックでいいじゃん! 2004/11/04 14:10 吉住

排他の本当の意味も知らずに「排他!排他!」って言ってる人いますからねぇ
あとエラーハンドリングのGoToもいっしょですね。
使い方を抑えれば問題ないんですけど、説得するのにいつも苦労します。
GoToについては、throwを使用したエラーハンドリングが普及したので最近は使いませんけど

# re: 楽観的ロックでいいじゃん! 2004/11/16 16:17 河端善博

最近2週間のアクセス数、第四位。
急速に人気コンテンツに成長してきていますね。
たくさんのコメントが支えですね。

# re: 楽観的ロックでいいじゃん! 2004/11/16 17:39 大西 彰

累計1942ヒットを更新しました。
思いのほか反響があったようで良かったです。
お読みいただいた皆様、ありがとうございます。

# re: 楽観的ロックでいいじゃん! 2005/11/21 11:01 OTN

OTNからリンクがはられたので
トラックバックしてみる

# re: 楽観的ロックでいいじゃん! 2005/12/02 16:30 大西 彰

累計9036ヒット更新でございます。
皆様、ありがとうございます。

# re: 楽観的ロックでいいじゃん! 2006/04/29 3:55 大西 彰

累計12040ヒット更新!
「行ロック」でGoogle検索するとこのページがトップに来るのはびっくり(^^;

# 悲観的・楽観的ロック 2006/05/27 23:35 きままにSE雑記

・楽観的ロックでいいじゃん! [大西彰のウェブログ] 研修で習った気がするけど...

# 楽観的同時実行制御でいいかな? 2007/02/13 23:29 囚人のジレンマな日々

楽観的同時実行制御でいいかな?

# 楽観的同時実行制御でいいかな? 2007/02/13 23:31 囚人のジレンマな日々

楽観的同時実行制御でいいかな?

# 楽観的ロックでいいじゃん 2009年版を書こうかと思案中 2009/03/25 18:55 大西彰のウェブログ

楽観的ロックでいいじゃん 2009年版を書こうかと思案中

# 大西彰さんのブログ復活 2009/07/31 9:18 河端善博 ブログ / SQL Server

大西彰さんのブログ復活

# re: 楽観的同時実行制御でいいかな? 2009/08/12 18:37 囚人のジレンマな日々

re: 楽観的同時実行制御でいいかな?

# re: 楽観的ロックでいいじゃん! 2009/09/09 22:44 中の技術日誌ブログ

re: 楽観的ロックでいいじゃん!

# 在庫引当はどうしますか? 2009/12/12 18:58 ゆう

在庫引当のような、複数の顧客が同じレコードにアクセスする可能性のある処理では、悲観的ロックが必要と考えますがいかがですか?

また、楽観的ロックとはどのようなコードを想定されていますか?
名前は一緒でも皆さん異なるコードを描かれているかもしれません。

私のイメージでは、楽観的ロックでバージョン番号をチェックする直前に悲観的ロックを獲得します。
UPDATE SET ... WHERE ID=? AND VERSION = ?;
<-- DB がトランザクションの最後まで自動悲観的ロックをかけます

タイトル  
名前  
URL
コメント