楽観的ロックでいいじゃん (2004年10月19日 14:58 PASSJブログで公開した内容のまま)

※ 以前にPASSJブログで累計25,000人くらいに読まれたエントリーです。SQL Server以外のコミュニティにも参照していただきました。原文のまま、MSDNブログに転載します。

データベースというよりは、トランザクション処理ネタなのですが、皆さんは、データベースのトランザクション処理を実行される場合、対象となる行をどのように排他制御されていますか。排他制御というのは、同時実行処理において必要不可欠なものなのですが、使い方を誤ると簡単なはずの処理が難しくなってしまう可能性があります。一般的にリソース(資源)をロックする必要がある場合、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を使おうが、最終的にはデータベース上の永続化にたどり着きます。永続化処理を軽量にすることが今後も求められると思います。

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