排他制御とは、1つのリソースに複数のスレッドからアクセスするときに、調停を行うことだ。
教科書的な排他制御の説明はこんな感じだろう。
ある変数に対して、2つのスレッドがインクリメントを試みる。
インクリメントとは、
- 変数の現在の値を取得する
- それに1を足す
- 足した結果を変数に書き戻す
という一連の操作である。
変数の初期値が1であるとして、2つのスレッドが競合すると、
- スレッドAが変数から1を読み取る。
- スレッドBが変数から1を読み取る。
- スレッドAが読み取った値に1を足す
- スレッドBが読み取った値に1を足す。
- スレッドAが結果を書き戻す。変数の値は2になる。
- スレッドBが結果を書き戻す。変数の値は2になる。
となる。
インクリメントが2回行われているから、結果は3にならなければいけないのに、2になってしまう。
これを防ぐために、ロックが使われる。
- スレッドAが変数から1を読み取る。
- スレッドBが変数を読み取ろうとするが、スレッドAがロックしているため待つ。
- スレッドAが読み取った値に1を足す。
- スレッドAが結果を書き戻す。変数の値は2になる。
- スレッドAがロックを解放する。
- スレッドBが動き出し、変数から2を読み取る。
- スレッドBが読み取った値に1を足す。
- スレッドBが結果を書き戻す。変数の値は3になる。
万事めでたし、というわけだ。
データベースで言うと、変数から読み取る操作がSelect、書き戻す操作がInsertやUpdateになる。
上記の例で言えば、スレッドAがロックをかけたら、スレッドBのSelectも止められなければならない。でなければ、スレッドAが更新した後の値を読み取れないからだ。
だが、どうもOracleには、他人がSelectするのを止める術が無いように思える(Select for Updateなら止められるが、更新がInsertのケースでは使えない)。
SQL Serverでは可能らしいと聞くのだが。
こういう場合、Oracleでは、
- 同じ行をUpdateするなら、Select for Updateか、タイムスタンプ列を使った楽観的排他制御
- 新しい行をInsertするなら、該当列に一意性制約をかけておき、成功するまでループ
という方法になるのだろうか。
Insertの際に一意でなければならない列の値をユーザに決定させる場合は、成功するまで何度でもトライアンドエラーさせるしかなさそうだ。