同時実行と二重処理の防止
事前チェックだけに頼らず、確定する更新の条件や制約で二重利用を止める方法を確認します。
説明
二つの要求が同じ残高を読むと、どちらも利用可能だと判断する場合があります。読んだ値で後から上書きするだけでは、台帳と残高が一致しなくなることがあります。
別々の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 / 固定の公開試料
from sd12 import *
lab = Race()
print(lab.state())(100, (0, 0))
print(lab.run(atomic=False))
print(lab.state())[True, True] (20, (2, 160))
lab.reset()
print(lab.run(atomic=True))
print(lab.state())[False, True] (20, (1, 80))
balance,ledger = lab.state()
print(balance+ledger[1]==100,ledger[0])True 1
print(lab.coupon())
print(lab.db.execute("SELECT coupon,COUNT(*) FROM redeemed GROUP BY coupon").fetchall())['granted', 'used']
[('coupon-1', 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)
print(lab.db.execute("SELECT balance,version FROM wallet").fetchone())1 0 (30, 2)
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,)
演習完了