一覧SD-12-2 · 業務ロジックと競合の防御

同時実行と二重処理の防止

事前チェックだけに頼らず、確定する更新の条件や制約で二重利用を止める方法を確認します。

説明

二つの要求が同じ残高を読むと、どちらも利用可能だと判断する場合があります。読んだ値で後から上書きするだけでは、台帳と残高が一致しなくなることがあります。

別々のSQLite接続を使う二つのスレッドで、残高100から80を使う処理を同時に動かします。更新条件をSQLへ含めた方式、一回限りのクーポン、版番号による競合検出を比較します。

事前チェックだけに頼らず、確定する更新の条件や制約で二重利用を止める方法を確認します。

1残高と台帳の初期状態を確認する

balanceは100、支出台帳は0件です。

from sd12 import * lab = Race() print(lab.state()残高・台帳件数・支出合計を読む)

100と、台帳件数0・合計0です。残高だけでなく台帳も照合することで、上書きによる不整合を検出できます。

2同じ残高を読んだ二要求を実行する

両スレッドが残高100を読み終えたところで待ち合わせ、その後100-80を上書きします。

print(lab.run(atomic=False読んだ値で後から上書きする方式を実行する)) print(lab.state())

両方成功となり台帳は2件160、残高は20です。100-160なら-60のはずなので、残高表示だけでは二重利用を見落とします。

3残高条件を更新文へ入れる

初期状態へ戻し、balance>=80の行だけをbalance-80へ更新します。

lab.reset()同じ初期条件へ戻す print(lab.run(atomic=True現在残高の条件付き更新を行う)) print(lab.state())

成功は一つ、もう一つはFalseです。残高20、台帳1件80で100-80=20に一致します。更新0行を支出成功として扱いません。

4別要求の処理後にも整合性を照合する

支出合計と残高を別々に取得して等式を検査します。

balance,ledger = lab.state() print(balance+ledger[1]==100残高と台帳の独立した不変条件を調べる,ledger[0])

Trueで台帳1件です。残高と支出追加を同じトランザクションへ入れているため、片方だけの確定を避けます。

5クーポンを二主体から同時に使う

coupon-1の利用記録には、クーポンIDの一意制約を置きます。

print(lab.coupon()二つの接続から同じクーポンを挿入する) print(lab.db.execute("SELECT coupon,COUNT(*) FROM redeemed GROUP BY coupon確定した利用件数を読む").fetchall())

grantedとusedが一つずつ、利用記録は1件です。先着した主体は実行順によりますが、成功件数は制約で一つに固定されます。

6版番号を条件にして古い更新を検出する

版1を読んだ二要求が、異なる値へ更新する場合を順に再現します。

with lab.db: first = lab.db.execute("UPDATE wallet SET balance=30,version=version+1 WHERE id=1 AND version=1読んだ版と現在版が一致するときだけ更新する").rowcount second = lab.db.execute("UPDATE wallet SET balance=40,version=version+1 WHERE id=1 AND version=1").rowcount print(first,second)古い版の更新が0行になることを確認する print(lab.db.execute("SELECT balance,version FROM wallet").fetchone())

最初は1行、次は0行で30・版2です。これは独立した版管理の例として残高値を差し替えています。実支出では残高条件と台帳の更新も組み合わせます。

7競合した側は再取得して判断する

0行だった要求は、以前の残高と版を使って無条件上書きしてはいけません。

current = lab.db.execute("SELECT balance,version競合後の現在値を取得する FROM wallet WHERE id=1").fetchone() print(current) print(lab.db.execute("SELECT COUNT(*) FROM redeemed").fetchone())

現在値は30・版2で、クーポン利用は1件のままです。競合後に最新状態を取り直し、再試行の可否を業務条件で決めます。

まとめ

事前に読んだ値だけで利用可否を決めません。

条件付き更新と台帳を同じ確定単位にします。

一意制約と版番号を使い、成功件数や競合を検出します。

操作履歴と解説を振り返れます。

Python 3.15 / 固定の公開試料

1
from sd12 import * lab = Race() print(lab.state())
(100, (0, 0))
2
print(lab.run(atomic=False)) print(lab.state())
[True, True]
(20, (2, 160))
3
lab.reset() print(lab.run(atomic=True)) print(lab.state())
[False, True]
(20, (1, 80))
4
balance,ledger = lab.state() print(balance+ledger[1]==100,ledger[0])
True 1
5
print(lab.coupon()) print(lab.db.execute("SELECT coupon,COUNT(*) FROM redeemed GROUP BY coupon").fetchall())
['granted', 'used']
[('coupon-1', 1)]
6
with lab.db: first = lab.db.execute("UPDATE wallet SET balance=30,version=version+1 WHERE id=1 AND version=1").rowcount second = lab.db.execute("UPDATE wallet SET balance=40,version=version+1 WHERE id=1 AND version=1").rowcount print(first,second) print(lab.db.execute("SELECT balance,version FROM wallet").fetchone())
1 0
(30, 2)
7
current = lab.db.execute("SELECT balance,version FROM wallet WHERE id=1").fetchone() print(current) print(lab.db.execute("SELECT COUNT(*) FROM redeemed").fetchone())
(30, 2)
(1,)

演習完了