2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

在庫UPDATEがつらいのでやめた。Aurora DSQLとAuroraでECスパイクトラフィックを殴ってみた

2
Last updated at Posted at 2026-03-17

在庫UPDATEがつらいのでやめた。Aurora DSQLとAuroraでECスパイクトラフィックを殴ってみた

ECサイトのスパイクトラフィックでつらいのは、だいたい在庫だ。

注文が一気に入ると、同じ商品の在庫行に対して UPDATE inventory SET quantity = quantity - 1 が殺到する。アプリケーションはシンプルでも、DBは急に苦しそうな顔をする。

Amazon Aurora DSQL が気になっていたので、よくあるECの注文APIを雑に切り出し、Aurora PostgreSQL と Aurora DSQL を同条件でぶつけてみた。

結論だけ先に書く。

  • 同一行UPDATEは、DBの種類以前に設計で負ける
  • 楽観ロックは高並列で普通に死ぬ
  • Append-Only にすると景色が変わる
  • 1000並列では Aurora DSQL がちゃんと強かった

この記事でやること

  • API Gateway + Lambda + DB だけの最小構成で注文APIを作る
  • 在庫管理方式を3パターン試す
    • 悲観ロック
    • 楽観ロック
    • Append-Only
  • Aurora PostgreSQL と Aurora DSQL を比較する
  • Distributed Load Testing on AWS ではなく、まず同等条件のHTTPベンチマークで基礎性能を見る

検証した構成

C4モデル - System Context

C4モデル - Container

処理フロー

テーブル設計

最初はこの5テーブルで始めた。

  • 商品テーブル products
  • 在庫テーブル inventory
  • 顧客テーブル customers
  • 注文テーブル orders
  • 注文詳細テーブル order_items
CREATE TABLE products (
    product_id    UUID PRIMARY KEY,
    product_name  VARCHAR(255) NOT NULL
);

CREATE TABLE inventory (
    product_id    UUID PRIMARY KEY,
    quantity      INTEGER NOT NULL DEFAULT 0,
    updated_at    TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE customers (
    customer_id   UUID PRIMARY KEY
);

CREATE TABLE orders (
    order_id      UUID PRIMARY KEY,
    ordered_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    customer_id   UUID NOT NULL
);

CREATE TABLE order_items (
    order_item_id UUID PRIMARY KEY,
    order_id      UUID NOT NULL,
    product_id    UUID NOT NULL,
    cancel_flag   BOOLEAN NOT NULL DEFAULT FALSE
);

UUIDは全部 UUID v7 をアプリ側で生成した。DB側の自動生成に寄せるより、Aurora と DSQL の両方で同じ条件を作りやすかったからだ。v7 は先頭 48 bit に Unix ミリ秒タイムスタンプを持つので生成順にソートでき、B-Tree インデックスとの相性が良い(RFC 9562 で標準化済み)。

APIの仕様

エンドポイントは3つだけ。

  • POST /orders — 注文作成
  • GET /products — 商品一覧
  • GET /inventory/{id} — 在庫確認

注文時の本題はここ。

POST /orders

この1リクエストの中で、

  1. 注文レコードを作る
  2. 注文詳細を作る
  3. 在庫判定または在庫減算を行う

をまとめて実行する。

試した3パターン

1. 悲観ロック型

いちばんよく見るやつ。

UPDATE inventory
SET quantity = quantity - 1, updated_at = NOW()
WHERE product_id = :product_id
  AND quantity > 0;

シンプルだが、同じ商品に注文が集中すると同一行UPDATE競合が起きる。

2. 楽観ロック型

UPDATE前に updated_at を読む。

SELECT quantity, updated_at
FROM inventory
WHERE product_id = :product_id;

UPDATE inventory
SET quantity = quantity - 1, updated_at = NOW()
WHERE product_id = :product_id
  AND quantity > 0
  AND updated_at = :updated_at;

一見賢そうだが、高並列だと SELECT -> UPDATE の間に他トランザクションが先に更新し、競合祭りになる。

3. Append-Only型

在庫テーブルを減算しない。

SELECT quantity FROM inventory WHERE product_id = :product_id;
SELECT COUNT(*)
FROM order_items
WHERE product_id = :product_id
  AND cancel_flag = FALSE;

つまり、

在庫あり = 初期在庫数 > キャンセルされていない注文詳細数

という見方に変えた。

UPDATEが消える。注文APIはほぼ INSERT only になる。

この時点で「勝ち筋これでは?」という気配が濃い。

ベンチマーク条件

  • 商品3件
  • 顧客100人
  • API Gateway + Lambda 経由
  • Python Lambda
  • Aurora PostgreSQL Serverless v2
  • Aurora DSQL
  • 比較条件を揃えるため、後半は両方とも us-east-1 に配置

Aurora PostgreSQL の結果

悲観ロック

並列数 リクエスト数 RPS P50 P95 P99 成功率
100 1000 55.5 1685ms 2395ms 2819ms 100%

楽観ロック

並列数 リクエスト数 RPS P50 P95 P99 成功率
100 1000 41.2 1679ms 4017ms 11825ms 34.6%

Append-Only

並列数 リクエスト数 RPS P50 P95 P99 成功率
100 1000 57.9 1640ms 2191ms 2499ms 100%

指標の読み方

  • RPS(Requests Per Second)― 1秒あたりの処理リクエスト数。スループットの指標。
  • P50 / P95 / P99(パーセンタイル)― 全リクエストを速い順に並べたとき、50% / 95% / 99% 目にあたるレスポンスタイム。P50 は「ふつうの体感」、P95・P99 は「たまに遅いやつがどれくらい遅いか」を示す。P99 が跳ねていたら、テールレイテンシに問題がある兆候。

見えたこと

  • 楽観ロックは高並列で普通に厳しい
  • 悲観ロックより Append-Only の方が少し速く、しかも安定する
  • 同一行UPDATEを消しただけでかなりマシになる

同リージョンで Aurora と DSQL を比較する

リージョン差があるとフェアじゃないので、Aurora も us-east-1 に作り直して比較した。

Append-Only / 100並列

指標 Aurora (us-east-1) DSQL (us-east-1)
RPS 56.2 56.0
P50 1486ms 860ms
P95 2623ms 6078ms
P99 4807ms 11871ms
成功率 100% 100%

この段階だと、

  • スループットはほぼ同じ
  • P50は DSQL がかなり速い
  • でも P95/P99 は Aurora の方が安定

という結果になった。

ここでは「DSQL速いじゃん」で終わらない。高並列にするともっと差が出る。

200並列・500並列・1000並列でどうなるか

200並列 / Append-Only

指標 Aurora DSQL
RPS 49.9 60.5
P50 2180ms 962ms
成功 1405 2000
在庫切れ 595 0

ここで DSQL がかなり強くなった。

1000並列 / 10000リクエスト / 在庫8000

指標 Aurora DSQL
RPS 75.3 75.3
P50 3398ms 3694ms
P95 63076ms 42022ms
P99 120850ms 65130ms
成功 9797 10000
エラー 203 0
DB時間 P50 1161ms 229ms

ここで一気に差が出た。

DB時間の中央値が 1161ms vs 229ms。DSQLが約5倍速い。

Aurora はタイムアウトが出た。DSQLは出なかった。

Q. RPS変わってねーじゃん?
A. 「DB を5倍速くしても、パイプラインの他の部分が3000ms以上食っているので、全体のRPSは変わらない」という、典型的な アムダールの法則が出てる。DB時間が全体の34%→6%に縮んでも、残り66%→94%の部分が律速になるので、全体スループットの改善幅は限定的になる。

つまり何が言えるのか

1. まず設計を直せ

DBを変える前に、在庫減算を疑った方がいい。

  • 悲観ロック → 競合で遅い
  • 楽観ロック → 高並列でさらに厳しい
  • Append-Only → UPDATEが消えて、ようやく戦える

一番効いたのは DB の変更ではなく、データの持ち方を変えたことだった。

2. DSQL は「普通に速い」ではなく「高並列で崩れにくい」

100並列くらいだと Aurora でも十分戦える。

でも 200並列を超えたあたりから、DSQL の分散性が見えてくる。

1000並列では、Aurora がタイムアウトを出し始めたのに対して、DSQL は全部返し切った。

この差は大きい。

3. DSQL は魔法ではない

P50は良くても、P95/P99は荒れる場面がある。低並列では Aurora の方が安定するケースもあった。

つまり、

  • 常時安定した OLTP を少人数で処理するなら Aurora で十分
  • スパイクが激しく、同時書き込みが突発的に増えるなら DSQL が効く

という整理がしっくりくる。

どんなシステムで向いていそうか

Aurora DSQL が向いていそうなのは、例えばこういうやつ。

  • チケット争奪
  • 先着販売
  • ECの限定商品販売
  • 同一タイミングに注文が集中するキャンペーン
  • ゲーム内イベントで同一商品に購入が集中するケース

逆に、通常の業務システムでトラフィックが平準化されているなら、Aurora の方が素直で扱いやすい。

今回の結論

在庫UPDATEがつらいなら、まずやるべきは DSQL の導入ではなく 在庫の持ち方を疑うこと だった。

そのうえで、

  • 同一行UPDATEをやめる
  • INSERT中心の Append-Only に寄せる
  • その設計で高並列を受ける

ここまで持っていくと、Aurora DSQL はちゃんと強い。

設計を変える前の DSQL は過大評価しやすい。設計を変えた後の DSQL はかなり頼もしい。

この順番を間違えないのが大事だった。


続編はこちら。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?