Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Outboxパターンの素振りをした
Search
Kazuki Nitta
October 16, 2026
330
2
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Outboxパターンの素振りをした
会社の勉強会で喋る小ネタ
https://github.com/ntk221/outbox-suburi-cf
Kazuki Nitta
October 16, 2026
More Decks by Kazuki Nitta
See All by Kazuki Nitta
なぜ、SREはロールであるべきか
ntk221
1
670
AIを相棒としてみなさない
ntk221
0
49
Featured
See All Featured
The MySQL Ecosystem @ GitHub 2015
samlambert
251
13k
More Than Pixels: Becoming A User Experience Designer
marktimemedia
3
560
4 Signs Your Business is Dying
shpigford
187
23k
The Success of Rails: Ensuring Growth for the Next 100 Years
eileencodes
47
8.4k
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
49
3.6k
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
9
1.6k
The Language of Interfaces
destraynor
162
27k
The State of eCommerce SEO: How to Win in Today's Products SERPs - #SEOweek
aleyda
2
12k
How to make the Groovebox
asonas
2
2.5k
Efficient Content Optimization with Google Search Console & Apps Script
katarinadahlin
PRO
1
930
Speed Design
sergeychernyshev
33
2.2k
Code Review Best Practice
trishagee
74
20k
Transcript
2026.10.16 Outboxパターンの素振りを した DBの更新とイベントの送信を、ちゃんとそろえる nitta
今日の流れ 01 何が困るの DB とイベント、両方に書くといずれずれる 02 Outbox 同じトランザクションで書いて、あとで送る 03 素振り
Durable Objects + alarm + Queues で作る 04 気をつけること 重複・順序・溜まる 02 / 15
DBの変更を、ほかにも伝えたい どうやって届ける? 検索 Elasticsearch に反映したい キャッシュ 古くなったら消したい 注文DB INSERT /
UPDATE / DELETE 分析基盤 DWH に取り込みたい ほかのサービス 在庫・通知に知らせたい 03 / 15
間にメッセージブローカーを置く メール送信 メッセージブローカー 注文サービス 送る 「注文が入った」という イベントを送る お客さんに通知 在庫 在庫を引き当てる
いったん預かる 検索 検索に反映 送る側は相手を知らなくていい 受け手が止まっていても大丈夫 製品はいろいろ ブローカーに渡すだけ。受け手が増えて も、送る側は変わらない ブローカーが預かっておき、復旧したら 届ける Kafka、RabbitMQ、Amazon SQS、 Cloudflare Queues など 04 / 15
素朴なやり方の課題 例:注文を保存して、「注文が入った」イベントをブローカーに送る // トランザクションの中で送る await db.transaction(async (tx) => { await
tx.insert("orders", order); await broker.send(orderCreated(order)); }); // ← 1. COMMIT に失敗したら? 1. 送ったのに、注文がない 送ったあとで COMMIT に失敗すると、取り消された注 文の「受け付けました」メールが届く 2. 注文はあるのに、届かない // じゃあ、コミットしてから送る? await db.transaction(async (tx) => { await tx.insert("orders", order); }); // ← 2. ここで落ちたら? await broker.send(orderCreated(order)); コミット後、送る前に落ちるとイベントが消える 3. 2PC はたいてい使えない DB とブローカーをまたぐトランザクションに、多くの ブローカーは対応していない 05 / 15
Transactional Outbox Pattern イベントも DB に同じトランザクションで書く。 このイベントをあとから、送る。
Outboxパターン 二重書き込み問題を解く、定番のやり方 1つのトランザクションで書く orders outbox 中継役(relay) メッセージ基盤 注文を更新 イベントを1行追加 outbox
を読んで送る 「注文作成」などを 他サービスが購読 片方だけ成功、が起きない イベントの形を設計できる 送り終えたら消す 書くのはDBだけ。コミットされればイベ ントも必ず出る テーブル構造をそのまま晒さず、意味の ある形で出せる outbox は放っておくと溜まる。送った分 は掃除する 07 / 15
素振りの構成 OrdersDB(Durable Object) transactionSync() … 同じトランザクションで両方書く orders 業務テーブル + outbox
中継役(relay)は alarm。 outbox を定期的に読んで送る 外に出すイベント alarm() Queues MailSender outbox を読んで送る 送った位置を保存 順序の保証なし メール送信役(お客さんへ) 重複は送らない・順番を待つ アプリから PUT 08 / 15
なんで Durable Objects? 特に理由はない
イベントは同じトランザクションで書く async upsert(id, status) { this.ctx.storage.transactionSync(() => { const prev
= this.findOrder(id); this.saveOrder(id, status); // 業務の書き込み this.addEvent( // イベントも書く prev ? "OrderStatusChanged" : "OrderCreated", id, { id, status }); }); await this.scheduleRelay(); } 片方だけコミットされることはない 途中で例外が出たら、orders も outbox もロール バック イベントの形はアプリが決める 行の差分ではなく「注文作成」「ステータス変更」 // alarm をセット 1つ前の seq も書く addEvent の中で記録。受け側が注文ごとの順番 を守るのに使う 10 / 15
流れてくるイベント { "seq": 2, "type": "OrderStatusChanged", seq 通し番号。送った位置の目印 type イベントの種類。アプリが決める
aggregate_id どの注文のイベントか prev_seq この注文の1つ前のイベント。順番待ちに 使う payload 中身。テーブルの形とは切り離せる "aggregate_id": 42, "prev_seq": 1, "payload": { "id": 42, "status": "shipped" }, "ts_ms": 1792076127301 } 行の差分ではなく、意味のあるイベントを流す 11 / 15
リレーとして alarm で Queue に送る async alarm() { const sent
= this.loadOffset(); // どこまで送ったか const rows = this.sql.exec( `SELECT * FROM outbox WHERE seq > ? ORDER BY seq`, sent ).toArray(); if (rows.length === 0) return; await this.env.EVENTS.sendBatch( rows.map((r) => ({ body: toEvent(r) }))); // ← ここで落ちたら、次の alarm で同じ seq を再送 失敗したら自動で再試行 alarm は例外を投げると、間隔を空けて再実行さ れる 送る → 位置を保存 この順なので、間で落ちると重複する。中継役は どれもこう 送り終えた分は消す const last = rows.at(-1).seq; this.saveOffset(last); // 送った位置を保存 this.sql.exec(`DELETE FROM outbox WHERE seq <= ?`, last); 消さないと outbox は増え続ける } 12 / 15
気をつけること 例:注文イベントを受けて、お客さんにメールを送る場合 1. 同じメールが2通届く relay がイベントを送ったあとに落ちてイベントが再送 される場合がある。メールを送った seq を consumer
側で記録し、2回目は送らないようにする 2. 順番が入れ替わる Queues は順序を保証しない。注文ごとに、前のメール を送ってから次を送るようにする 3. まとめて遅れて届く relay や Queue の不具合でイベントを送れない間は outbox が溜まる。未送信の件数を監視する [mail] send seq=3 注文43「ご注文を受け付けました」 [relay] send seq=3..3 ← 再送 [mail] skip seq=3 注文43(送信済み) [mail] hold seq=5 注文44(seq=4 待ち) [mail] send seq=4 注文44「ご注文を受け付けました」 [mail] send seq=5 注文44「お支払いを確認しました」 [relay] Queue に送れない(未送信 3 件)retry=1 (復旧) [relay] send seq=6..8 13 / 15
思ったこと consumer 側で重複をはじくには、結局「処理済み」を記録しないといけない。それ、Outbox でやりたいことができ 自分 てなくない? そうでもない。Outbox は業務の記録とイベントの記録をアトミックにするテクニックで、配信されるイベントの重複 は at-least-once
を選んだ時点で残る。consumer 側の副作用が DB なら、処理済み seq を業務の書き込みと同じトラ Claude ンザクションで書けば済む。 じゃあメール送信のような外部システムへの副作用のときは、わざわざ Outbox にするメリット薄い? 自分 薄くない。比較相手は「コミット後に送る」。幻のメール・届かない・2通届く、のうち一番マシな「2通」だけが残 る。また、外部サービスによっては冪等キーを渡すと重複を弾いてくれる API があるので、seq をそのまま渡せる。 Claude 14 / 15
おわり 今日のコード https://github.com/ntk221/outbox-suburi-cf Pattern: Transactional outbox microservices.io › patterns Durable
Objects:Alarms developers.cloudflare.com › durable-objects Durable Objects:SQLite Storage API Q&A 質問・ツッコミ、どうぞ developers.cloudflare.com › durable-objects Queues:Delivery guarantees developers.cloudflare.com › queues 15 / 15