2023.07.10

AWSのSQSでアクセス集中の際に注文システムを考慮し、設計してみる

こんにちは。次世代システム研究室のK.X.Dです。

実際にオンライン注文システムで、よく人気で数が限定された新商品を発売する際には、大量の購入リクエストに対応するために注文処理サーバーが負荷になってしまいます。また、同時に集中的なリクエストがあると、在庫チェックや購入順序の処理がややこしくなるという課題もあります。

上記の課題を解決するために、どのように注文処理を分散し、在庫チェックを確実に行うかについて、AWSのSQSを活用する方法を紹介します。

SQSの前提知識について、こちらに参照いただけます。

同時に大量リクエストがあったの際に、SQSのキューに順序保証の検証について、こちらに参照いただけます。

1.要件、設計発想

■要件:
冒頭で差し上げた課題として、要件を2類で分けさせていただきます。
  • 仕様要件

+ 集中リクエストで在庫チェックがしっかりできること。

+ 購入順次が担保できること。


  • システム要件

+ 注文処理の際にエラー発生であった場合、十分にリトライできること。

+ 注文の際に処理を分散できること。

+ 万が一注文のステータスがおかしくなっても、決済が二重が発生しないこと。

■設計発想:
普段の注文受付プロセスでは、
  1. 在庫チェック
  2. 決済確定
  3. 注文確定
普段の注文受付プロセスでは、在庫チェック、決済確定、注文確定という手順で処理を進めます。通常、プログラムではこれらの手順を順次実行しますが、同時に大量の注文リクエストがある場合、在庫チェックと注文受付を同時に行うと、最新の在庫状態が確認できず、在庫超過による受付エラーが発生する可能性があります。また、決済確定の後には決済システムとの通信待ちの無駄な時間が生じます。

しかし、人気商品の場合は即座に注文確定する必要はなく、注文を受け付けた後で注文結果を通知することも可能です。ユーザーにとっては、注文受付成功の通知が届けば十分なユーザーエクスペリエンスを提供できます。

この考え方に基づいて、以下のような分割処理をプログラムに導入することができます。
  1. 注文受付処理
    1.1 オーソリを行って注文決済を確認する
    1.2 注文情報を保存する
  2. 在庫確認、決済確定
    2.1 在庫を確認する
    2.2 決済を確定する
  3. ユーザーに注文受付の通知を行う:
    3.1 SNSなどサービスで通知配信
非同期的に処理を進めるため、2と3の処理が完了を待たずに1の処理が完了したら、すぐにユーザーにレスポンスを返すことができます。そのため、受付処理は非常に軽量になります。

在庫確認は順次実行され、落ち着いて最新の在庫状況を確認してから注文確定ができます。このような非同期の仕組みを実現するために、AWSのキューイングサービスを使用し、以下のような設計を考えています。分散させたい各処理ごとに対応するSQSキューを設け、受付時に該当するキューにメッセージを追加して在庫確認やユーザーへの通知を依頼することができます。


SQSはクラウドサービスとして提供されており、高い信頼性と可用性を備えています。また、エラーハンドリングやメッセージの順序性を保証する機能も持っています。

以上の利点から、今回SQSを活用することで実装してみます。

2.実装

2.1 環境:

2.1.1 EC2サービス:2サーバ(https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/EC2_GetStarted.html#ec2-launch-instance)

2.1.2 SQSサービス:
  • デッドレターキューを作成します。
  • 「在庫確認、決済確定」、「ユーザーに注文受付の通知」のために、2SQS 標準キューを作成します。
以下のスクリンキャプチャのように、「在庫確認、決済確定」、「アプリ通知でユーザへ注文受付知らせ」SQS標準キューを作成します。

+ メッセージ受信待機時間が20秒にして、ロングポーリングにしておきます。
ショートポーリングよりロングポーリングがコストを99%削減できる記事を目にしました。(参照リンク

+ アクセスポリシーがEC2サービスにアタッチロールを設定しておきます。

+ デッドレターキューを設定し、最大受信数を3にしておきます。
メッセージ3回処理失敗すると、デッドレターキューへ移動されます。
3回のリトライ制限により、受信側でのメッセージ処理失敗時の無駄なスループットを制限できます。

  • デッドレターキューの許可ポリシーの再実行を設定:
    上記の2標準キューをアクセス許可できるように、入れておきます。
  • 必要なキュー作成後の一覧:

2.2 注文受付

2.2.1 メッセージ定義:
  • 在庫確認、決済確定:
    {order_id : $order_id , type : ‘stock_check_and_payment_capture’ , amount : $amount, user_id : $user_id}
  • アプリ通知でユーザへ注文受付知らせ
    {order_id : $order_id , type : ‘application_notification’ , amount : $amount, user_id : $user_id}
    <?php
    // DB接続
    $pdo = new PDO(
    'mysql:host=$mysql_host;dbname=$mysql_db_name;',
    $mysql_user_name,
    '$mysql_password',
    [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
    );
    
    
    $stmt = $pdo->prepare("INSERT INTO orders (id, user_id, product_id, amount, master_order_status_id)
    VALUES (':id', ':product_id', ':amount', :master_order_status_id)";
    
    // 値をセット
    $stmt->bindValue('id', 'order_id_1');
    $stmt->bindValue(':product_id_1', 'product_id_1');
    $stmt->bindValue(':amount', '10000');
    $stmt->bindValue(':master_order_status_id', '1');
    
    // SQL実行
    $stmt->execute();
    
    // Stripeなどの決済サービスで$payment_tokenを利用して、決済オーソリで与信を押さえる
    // https://stripe.com/docs/payments/place-a-hold-on-a-payment-method#authorize-onlyに参照する
    \Stripe\Stripe::setApiKey('$stripe_key');
    
    $response = \Stripe\PaymentIntent::create([
      'amount' => 10000,
      'currency' => 'jpy',
      'payment_method_types' => ['card'],
      'capture_method' => 'manual',
     ]);
    
    $payment_intent_id = $response['payment_intent_id']
    
    $stmt = $pdo->prepare("UPDATE orders SET payment_intent_id = :payment_intent_id WHERE id = ':id");
    $stmt->bindValue('payment_intent_id', $payment_intent_id);
    $stmt->bindValue('id', 'order_id_1');
    
    $stmt->execute();
    
    // AWSのPHP Clientを利用して、SQSへメッセージ送信する
    // https://docs.aws.amazon.com/aws-sdk-php/v3/api/class-Aws.Sqs.SqsClient.html
    require 'vendor/autoload.php';
    
    use Aws\Sqs\SqsClient;
    
    $client = new SqsClient([
        'profile' => 'default',
        'region' => 'ap-northeast-1',
        'version' => '2012-11-05'
    ]); 
    
    // アプリ通知でユーザへ注文受付知らせキューへメッセージ送信
    $params = [
        'MessageBody' => "{order_id : 'order_id_1', type : 'application_notification' , amount : 10000, user_id : 'user_id_1'}",
        'QueueUrl' => $notification_queue_url // AWS SQSのユーザーに注文受付の通知キュー管理画面で取得
    ];
    
    $result = $client->sendMessage($params);
    
    // 在庫確認、決済確定キューへメッセージ送信
    $params = [
        'MessageBody' => "{order_id : 'order_id_1', type : 'stock_check_and_payment_capture' , amount : 10000, user_id : 'user_id_1'}",
        'QueueUrl' => $stock_check_and_payment_capture_queue_url // AWS SQSの在庫確認、決済確定キュー管理画面で取得
    ];
    
    $result = $client->sendMessage($params);
    
    ?>
    QueueUrl : キュー管理画面のURLで確認

2.3 在庫確認、決済確定

EC2 の crontab設定で下記の処理を実装します。
<?php
// DB接続
$pdo = new PDO(
'mysql:host=$mysql_host;dbname=$mysql_db_name;',
$mysql_user_name,
'$mysql_password',
[PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
);

// AWSのPHP Clientを利用して、SQSメッセージ受信
// https://docs.aws.amazon.com/aws-sdk-php/v3/api/class-Aws.Sqs.SqsClient.html
require 'vendor/autoload.php';

use Aws\Sqs\SqsClient;

$client = new SqsClient([
'profile' => 'default',
'region' => 'ap-northeast-1',
'version' => '2012-11-05'
]);

// 在庫確認、決済確定キューからメッセージ受信
$results = $client->receiveMessage([
'MaxNumberOfMessages' => 10
'QueueUrl' => $stock_check_and_payment_capture_queue_url // AWS SQSの在庫確認、決済確定キュー管理画面で取得
]);

foreach (result as results) {
// メッセージBodyが正しく受信できないで、処理を終了て、受信リトライする
if(md5($result['Body']) != $result['MD5OfBody']) {
  return;
}

$message_body = json_decode($result['Body']);

$orders = $pdo->prepare('SELECT * FROM orders WHERE id = :id');
$orders->bindValue(':id', $message_body['order_id']);
// SQL実行
$orders->execute();

$order = $orders[0];

// 在庫確認
$toal_is_paid_order = $pdo->query('SELECT COUNT(*) FROM orders WHERE master_order_status_id = 2')->fetchColumn();

$stocks = $pdo->prepare('SELECT * FROM stocks WHERE product_id = :product_id');
$stocks->bindValue(':product_id',order['product_id']);
// SQL実行
$stocks->execute();

if ($stocks[0][quantity] = $toal_is_paid_order) {
// 在庫が切れでステータスを更新する
$stmt = $pdo->prepare("UPDATE orders SET master_order_status_id = 4 WHERE id = ':id");
$stmt->bindValue(':id', order['id']);

// 決済をキャンセルする
// https://stripe.com/docs/refunds?dashboard-or-api=api#cancel-paymentに参照する
\Stripe\Stripe::setApiKey('$stripe_key');
$intent = \Stripe\PaymentIntent::retrieve($order['payment_intent_id']);
$intent->cancel($order['payment_intent_id'], []);

// キューからメッセージを削除する
$result = $client->deleteMessage([
'QueueUrl' => $stock_check_and_payment_capture_queue_url, // AWS SQSの在庫確認、決済確定キュー管理画面で取得
'ReceiptHandle' => result['ReceiptHandle']
]);
  return;
}


// 決済確定できましたが、決済ステータスが更新できないで、メッセージ受信がもう一回受信できる
// また、標準キューが同じメッセージが1回以上受信可能で、
// 冪等性チェック、2重決済を行わないようにチェックする
if ($orders[0]['master_order_status_id'] == 1 && $orders[0]['master_order_status_id'] == 3) {
// Stripeなどの決済サービスで$payment_tokenを利用して、決済オーソリで与信を押さえる
// https://stripe.com/docs/payments/place-a-hold-on-a-payment-method#capture-fundsに参照する
\Stripe\Stripe::setApiKey('$stripe_key');
$intent = \Stripe\PaymentIntent::retrieve($order['payment_intent_id']);
$intent->capture(['amount_to_capture' => 750]);

$stmt = $pdo->prepare("UPDATE orders SET master_order_status_id = 2 WHERE id = ':id");
$stmt->bindValue(':id', order['id']);
}
}
?>

2.4 ユーザーに注文受付の通知を行う:

CronJob設定で下記の処理を実装します。
<?php 
// AWSのPHP Clientを利用して、SQSメッセージ受信
// https://docs.aws.amazon.com/aws-sdk-php/v3/api/class-Aws.Sqs.SqsClient.html
require 'vendor/autoload.php';

use Aws\Sqs\SqsClient;

$client = new SqsClient([
'profile' => 'default',
'region' => 'ap-northeast-1',
'version' => '2012-11-05'
]);

// アプリ通知でユーザへ注文受付知らせキューからメッセージ受信
$results = $client->receiveMessage([
'MaxNumberOfMessages' => 10
'QueueUrl' => $notification_queue_url // AWS SQSのユーザーに注文受付の通知キュー管理画面で取得
]);

foreach (result as results) {
 // メッセージBodyが正しく受信できないで、処理を終了て、受信リトライする
if(md5($result['Body']) != $result['MD5OfBody']) {
return;
}

// SNSでアプリへ通知メッセージを配信する
require 'vendor/autoload.php';

use Aws\Sns\SnsClient;

/** 
* Sends a message to an Amazon SNS topic. 
**/
$SnSclient = new SnsClient([
'profile' => 'default',
'region' => 'ap-northeast-1',
'version' => '2012-11-05'
]);

$message = "$result['user_id']で購入成功";
$topic = "arn:aws:sns:us-east-1:111122223333:UserTopic";

$result = $SnSclient->publish(
['Message' => $message,
'TopicArn' => $topic,
]);
}

?>

3.まとめ

ご覧いただき、ありがとうございました。
非同期設計がシステムの疎結合を強化して、システムの可用性が高くなります。また、単調にで各分散コンポーネントもスケールアウトしやくなります。
その一方、冪等性とリトライスループットを念入れして実装すべきです。
ぜひAWSのSQSで非同期設計をご活用いただければと思います。

4.最後に

次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。

皆さんのご応募をお待ちしています。

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事