慣性聚合 関心のあるブログ、ニュース、テクノロジーを効率的に追跡
原文を読む 慣性聚合で開く

おすすめ購読元

博客园 - 司徒正美
V
V2EX
T
Tailwind CSS Blog
有赞技术团队
有赞技术团队
aimingoo的专栏
aimingoo的专栏
Apple Machine Learning Research
Apple Machine Learning Research
IT之家
IT之家
Blog — PlanetScale
Blog — PlanetScale
A
About on SuperTechFans
月光博客
月光博客
T
The Blog of Author Tim Ferriss
宝玉的分享
宝玉的分享
Martin Fowler
Martin Fowler
博客园 - 聂微东
The GitHub Blog
The GitHub Blog
V
Visual Studio Blog
WordPress大学
WordPress大学
酷 壳 – CoolShell
酷 壳 – CoolShell
Engineering at Meta
Engineering at Meta
GbyAI
GbyAI

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python)
私の二重簿記システムでストレステストで見つかったレースコンディションと、それを修正した方法
Do Pham Dinh · 2026-05-24 · via DEV Community

私はledger-serviceを構築しています。これはJava 21 / Spring Boot 3.5 / PostgreSQLを使用した二重簿記型の電子ウォレット帳簿です。はRender上で稼働しています。早い段階で、同じ口座に対して50回の送金を発行するストレステストを書きました。 をすぐに実行し、本が決して損なわないことを主張します。赤くなった——そして赤くなった方法が、このプロジェクトを構築してきた中で学んだ最も役立つことはです.

この投稿では、お金の台帳がバランスを保持する理由 キャッシュを整ったプロセスを説明します。 ではありません。キャッシュが招く読み書き書き換えの競合状態、それをどのように検知したか、修正(楽観的ロック+制限再試行)、楽観的ロックを選択したことを正当化するベンチマーク、および idempotency が再試行とどのように組み合わさる必要があるか(ネットワークの不安定さがダブルスペンドを防ぐ)について.

設定:帳簿、それがキャッシュを持つ理由

真実の源は貸借対照式、追記のみテーブル (ADR-0005)。すべての金銭操作は少なくとも1つのバランスを記録します。DEBIT/CREDITペアでΣ DEBIT == Σ CREDIT、そしてledger_entries挿入のみ — いいえUPDATE、いいえDELETE. 誤りは投稿して修正されます修正エントリーより、歴史を編集することなく。これがStripe、Modern Treasury、Formanceがすべて使用するモデルであり、信頼できる監査トレースを提供するものです。

しかし「口座Xの残高は何ですか?」はそうであってはなりませんSUMすべてのアカウントが過去に持ったエントリーよりも。だから私はキャッシュ:accounts.balance具体化されていますΣその口座のエントリ、更新済みは同じトランザクション内でエントリ自体と同じです(ADR-0006)。エントリは真実です;残高は派生読み取りキャッシュであり、O(1)で保持されます

そのキャッシュが正に並行性が問題を引き起こす場所です

レース

二つのリクエストが同じ口座を同じ時に借方にします

R1: read balance $500 (enough)      R2: read balance $500 (enough)
R1: commit −$300 → $200             R2: commit −$400 → −$200   ← overdraft / lost update

フルスクリーンモードに入る フルスクリーンモードから退出する

両方は$500を読み、両方は十分であると決定し、新しいバランスの自分の考えを返信します。一方は静かに破壊し、他方を:ロストアップデート、そして、下の帳簿エントリと一致しないバランスが残ります。

この罠は、データベースがこれを止めてくれると仮定することです。それはそうではありません。PostgreSQLのデフォルトの隔離レベル、READ COMMITTED、あなたが読まないことを保証するだけです未約束データ — それは同時に同じ行を読み書きする2つのトランザクションに関して何もしない。読み書き変更書き込みレースはそれを通り抜ける。

検出:ストレステスト

テストでバグが表面化したものです。一つの口座に資金を付け、次に解雇N = 50 は同時にそれから転送され、その後帳簿を確認します:

AtomicInteger successes = new AtomicInteger();
CountDownLatch start = new CountDownLatch(1);
ExecutorService pool = Executors.newFixedThreadPool(16);
for (int i = 0; i < N; i++) {
    pool.submit(() -> {
        start.await();                       // line them all up...
        int code = post("/transfers", from, to, AMOUNT);   // ...then fire at once
        if (code == 201) successes.incrementAndGet();
    });
}
start.countDown();
// after all complete:
assertThat(balanceCache(from)).isEqualTo(ledgerBalance(from));   // cache == Σ entries
assertThat(balanceCache(from)).isGreaterThanOrEqualTo(0);        // no overdraft
assertThat(balanceCache(to)).isEqualTo((long) ok * AMOUNT);      // exact accounting

フルスクリーンモードに入る フルスクリーンモードから退出する

断片的な主張は意図的に タイミングに依存しない — それらは どんな場合にも当てはまります は成功と失敗を分けるのではなく、キャッシュを帳簿の真実と比較するのではなく、固定された期待される数と比較するためです。それがテストを安定した回帰ガードにするのです。

私は実験によってバグを確認しました:@Version 列を削除すると、~85% のキャッシュ更新が失われました とこれらの断言が赤くなった — キャッシュされたバランスは、エントリの合計から遠ざかった。キャッシュと真実が一致しなかった、これはお金のシステムにおいては全てのゲームのことである。

修正、第1部:楽観的ロック

accounts にはすでに version BIGINT 列があった、なぜなら Account は集約/ロックの境界であるから (ADR-0010)。JPA の @Version としてマッピングすることで、すべてのバランス書き込みが比較と設定に変換される。

UPDATE accounts SET balance = ?, version = version + 1
 WHERE id = ? AND version = ?

フルスクリーンモードを開始 フルスクリーンモードを終了

同時に二つのライターがバージョン7をロードします。最初にコミットする方はそれを8に設定します。二番目のUPDATE ... WHERE version = 7は今やゼロ行に一致し、Hibernateはコミット時にOptimisticLockingFailureExceptionを引き起こします。失われた更新は今や不可能です:静かに上書きする代わりに、敗者は言ったそれが負けた。

主要のプロパティ:これは検知、ブロックしない読者は決してロックを待ちません。レジスタンス(ledger)では、バランス/履歴の読み込みが書き込みをはるかに上回るため、それは非常に重要です。

修正、第2部:制限再試行

単独での検知だけでは不十分です。それに@Version が設定されているが再試行は行わない、ストレステストはデータの破損を停止したが、大きなデータ転送の一部が今や 失敗 してコンフリクトが発生する——正しいが、非常に悪い経験である。そのため、敗者は再試行する必要がある。

再試行のヘルパーは 外部 @Transactional に位置しており、その配置が全てのポイントである:

public <T> T execute(Supplier<T> operation) {
    for (int attempt = 1; ; attempt++) {
        try {
            return operation.get();           // a FRESH transaction each attempt
        } catch (OptimisticLockingFailureException e) {
            if (attempt >= maxAttempts) throw new ConcurrencyConflictException();
            sleep(backoffWithFullJitter(attempt));   // 25–200 ms, capped
        }
    }
}

フルスクリーンモードに入る フルスクリーンモードから退出する

各試行は新しい取引であり、現在のバージョンで行を再読み込みします — 失敗した取引内で再試行しても古いバージョンに対して再失敗します。デフォルト:5回の試行、指数的バックオフと完全なジッター(雷鳴の群れが別の衝突に再同期しないように)、枯渇時にクリーンな409 Conflict —決して500.

中程度の負荷で証明可能に終了するための小さな論理的な要素があります:k番目のコミット者は、異なる以前のコミット者にしか負けられず、したがって最大限にkの試行が行われます。4つの並行書き込みと5試行の予算で、すべての4つが決定的に成功します—不安定なテストはありません。本物の熱心なアカウント(試行予算よりも多くの並行書き込みがある)は409として現れ、これは隠れた汚染ではなく正直なバックプレッシャーです.

楽観的対悲観的:測定された選択

明らかな代替案は悲観的ロックです—SELECT ... FOR UPDATE触れる前にその行をロックするので、ライター#2はただ待つ。リトライはなし、考えるのが簡単だ。ではなぜ楽観的か?

気持ち悪いと感じたので、ベンチマークを書きましたTransferConcurrencyBenchmark) を運行している同一転送論理両方の戦略において、50人の並行書きエージェント、実際のPostgreSQL1台に対して:

シナリオ 楽観的 + 再試行 悲観的FOR UPDATE
低コンテンション (50個の独立した口座ペア) 34 ms · 50/50成功 · 0 再試行の無駄 31 ms · 50/50成功
高コンテンション (50回の転送→1 熱バイト) 731ミリ秒 · 50/50成功 · 185リトライ無駄 358ミリ秒 · 50/50成功

数字を読む:

  • 低い競合は一般的で、引き分けです (34対31ミリ秒) — しかし楽観的にはゼロリトライを無駄にし、そして重要なことに読み取りをブロックしません読み取り負荷の高い帳簿にとって、それが決定要因です。
  • 単一の高温の行で、悲観的は~2倍高速です(358 vs 731 ms)であり何も無駄にせず、楽観的は衝突とバックオフで185回の追加試行(≈4.7倍の作業量)を燃費する。しかし悲観的はここで正確に「勝つ」シリアライズおよびブロッキング読み取り— 避けようとしているもの — それは実際には解決するホットアカウントなら、ただキューに入れるだけです。

それでは、結論は楽観的+再試行であり、ベンチマークの価値は「楽観的が速い」とは決してありません(競合状態ではありません)—それはそれら185度の失敗した再試行は、閾値を定量化する本当に熱心なアカウント(考える:各トップアップが共有SYSTEM_FUNDING 行)には実際のエスカレーションが必要です:非同期キューイングやサブアカウントシャーディング、全体のシステムを悲観的ロックに切り替えるのではなく。

idempotency で組み合わせる

もう一つの方法として、リトライが実際に 悪化させます もし注意しなければならない。サーバーがコミットした後に接続が切断されるクライアントは、HTTPリクエスト全体を再試行する——そして今や転送を二重に投稿するリスクがある。コンフリクト時再試行とネットワークの小さな問題時再試行は異なる問題であり、片方の修正が他方を壊さないようにしなければならない.

したがって、両方の金銭エンドポイントには Idempotency-Key ヘッダー(ADR-0012、Stripeのパターン)が必要である。それを並行処理安全にする仕組みは権利要求-初:

INSERT INTO idempotency_keys (key, status) VALUES (?, 'PENDING')
ON CONFLICT (key) DO NOTHING;     -- committed immediately, before business logic

全画面モードを開始 全画面モードを終了

その原子挿入がシリアライズポイントです。権利要求で勝った方が操作を実行します。同じキーでの並行リクエストはコミットされたPENDING行を見て409 (飛行中) の代わりに再度実行するのではなく。完了したキーは格納されたレスポンスを再生し、異なる本文で再利用されたキーは 422 (クライアント契約違反、意図的に 409 コンフィリクトコードとは区別される) となります。

この理由は、前回のリトライとこの構成がスムーズに組み合わされるためです:楽観的ロックのリトライは 後ろに位置しています。 その鍵は既に要求されている。それらの内部の試行は既に要求されている idempotency 鍵の下で行われるため、クライアントからは完全に見えず、二重投稿を決して生成することはできない。コンフリクト再試行とリクエスト idempotency スタックで代わりに戦う.

次に手を伸ばすべきもの

キャッシュは速いが、ずれることができる(バグ、部分的な失敗)。だからスケジュールされた調整 はすべてのバランスを不変のエントリから再計算し、不一致があるとアラートを表示します—それは決して自動修正しません;オペレーターが修正エントリを投稿します。追記専用の台帳は、真実が常に回復可能であることを意味します。

そして、185回の再試行が露呈させた熱心アカウントの上限は次の実際のスケール問題です:一つの行が本当に競合している場合、答えは非同期投稿やそのアカウントのシャーディングです。再試行率があなたが限界を越えたときを知らせる信号となります。


本質的な点:お金のシステムにおいて、キャッシュが帳簿と異意を持っている は失敗が重要であり、デフォルト分離データベースはそれを作成するのを止めることはありません。@Version の比較設定は失われた更新を不可能にし、制限付きリトライとジッターで通常の負荷下ではそれが見えなくし、ベンチマークは支払っているコストと上限がどこかを教えてくれます。排他性は各レイヤーでのリトライが重複支出にならないように保証します。

コード: github.com/xidoke/ledger-service — その 並列処理モデルのドキュメントとADR(Architecture Decision Records)の0005000600110012はさらに詳しい内容を提供しています。ライブデモ: レジスターサービス-bjzr.onrender.com (無料インスタンス — 初回リクエストコールドスタート~50秒)。