ASP.NET C#でメッセージキューを使用した高同時接続処理(Xiaomiスマートフォン購入例)

Webアプリケーションが高同時接続を処理する際には、ハードウェアの拡張やプログラムの最適化に加え、並列処理を逐次処理に変換するアプローチも有効です。この手法はデータベースロックやメッセージキューの実装でよく用いられ、処理遅延のデメリットはあるものの、コスト削減という利点があります。

現象の再現

在庫商品テーブルの作成

CREATE TABLE [dbo].[stock_item](
    [id] [int] NOT NULL,--ユニークプライマリキー
    [name] [nvarchar](50) NULL,--商品名
    [status] [int] NULL ,--0在庫あり 1 在庫なし  デフォルト0
    [user_name] [nvarchar](50) NULL--注文ユーザー
 )

レコードの挿入

insert into stock_item(id,name,status,user_name) values(1,'Xiaomiスマートフォン',0,null)

注文処理プログラムの作成

public ContentResult SubmitOrder(string user)
        {
            using (ProductDbContext db = new ProductDbContext())
            {
                    var item = db.stock_item.Where<stock_item>(p => p.status== 0).FirstOrDefault();
                    if (item.status == 1)
                    {
                        return Content("失敗、商品はすでに在庫切れ");
                    }
                    else
                    {
                        //データベース遅延をシミュレーション
                        Thread.Sleep(5000);
                        item.status = 1;<br></br>                        item.user_name= user;<br></br>              db.SaveChanges(); <br></br>              return Content("購入成功");<br></br>             } <br></br>      } <br></br>    }

5秒以内に以下の2つのURLを同時にアクセスすると、両方とも「購入成功」と返され、データベース内のuser_nameは「lisi」になる。

/controller/SubmitOrder?user=zhangsan

/controller/SubmitOrder?user=lisi

これが同時接続による問題です。

第1段階:ロックによる単純な対応

Webアプリケーションはマルチスレッド構造を持つため、競合が発生しうる箇所にロックを適用する方法が考えられます。

        private static object _lock = new object();

        public ContentResult SubmitOrder(string user)
        {
            using (ProductDbContext db = new ProductDbContext())
            {
                lock (_lock)
                {
                    var item = db.stock_item.Where<stock_item>(p => p.status == 0).FirstOrDefault();
                    if (item.status == 1)
                    {
                        return Content("失敗、商品はすでに在庫切れ");
                    }
                    else
                    {
                        //データベース遅延をシミュレーション
                        Thread.Sleep(5000);
                        item.status = 1;
                        item.user_name = user;
                        db.SaveChanges();
                        return Content("購入成功");
                    }
                }
            }
        }

これにより各リクエストは順次処理され、競合問題は解消されます。

利点:競合問題を解決。

欠点:処理が遅く、ユーザー体験が悪く、大量データ処理には不向き。

第2段階:メッセージキューによる生産者-消費者モデルの導入

  1. 注文受付エントリ(生産者)の作成
public class OrderController : Controller
    {

        /// <summary>
        /// 注文受付(生産者)
        /// </summary>
        /// <returns></returns>
        public ContentResult SubmitOrderQueue(string user)
        {
            //注文をキューに直接追加
            OrderProcessor.OrderRequests.Enqueue(user);
            return Content("待機中");
        }

        /// <summary>
        /// 注文結果の確認
        /// </summary>
        /// <returns></returns>
        public ContentResult CheckOrderResult(string user)
        {
            var result = OrderProcessor.ProcessedOrders.Where(p => p.userName == user).FirstOrDefault();
            if (result == null)
            {
                return Content("処理待ち");
            }
            else
            {
                return Content(result.Status.ToString());
            }
        }
}
  1. 注文処理モジュール(消費者)の作成
/// <summary>
    /// 注文処理モジュール(消費者)
    /// </summary>
    public class OrderProcessor
    {
        /// <summary>
        /// 注文キュー
        /// </summary>
        public static ConcurrentQueue<string> OrderRequests = new ConcurrentQueue<string>();
        /// <summary>
        /// 処理結果リスト
        /// </summary>
        public static List<OrderStatus> ProcessedOrders = new List<OrderStatus>();
        /// <summary>
        /// 注文処理の実行
        /// </summary>
        public static void StartProcessing()
        {
            string user = null;
            while (true)
            {
                //キューが空の場合1秒間待機
                if (!OrderRequests.TryDequeue(out user))
                {
                    Thread.Sleep(1000);
                    continue;
                }
                //実際の業務処理(例:データベース挿入)
                bool status = new OrderExecutor().ProcessOrder(user);
                //処理結果を保存
                ProcessedOrders.Add(new OrderStatus() { Status = status, userName = user });
            }
        }
    }
  1. 実際の業務処理モジュールの作成
/// <summary>
    /// 実際の注文処理モジュール
    /// </summary>
    public class OrderExecutor
    {
        /// <summary>
        /// 在庫状態フラグ
        /// </summary>
        private bool availableStock = true;
        /// <summary>
        /// 注文処理の実行
        /// </summary>
        /// <returns></returns>
        public bool ProcessOrder(string user)
        {
            //在庫がない場合は即座にfalseを返す
            if (!availableStock)
            {
                return availableStock;
            }
            using (ProductDbContext db = new ProductDbContext())
            {
                var item = db.stock_item.Where(p => p.status == 0).FirstOrDefault();
                if (item == null)
                {
                    availableStock = false;
                    return false;
                }
                else
                {
                    Thread.Sleep(10000);//データベース処理を遅延シミュレーション
                    item.status = 1;
                    item.user_name = user;
                    db.SaveChanges();
                    return true;
                }
            }
        }
    }
    /// <summary>
    /// 処理結果エンティティ
    /// </summary>
    public class OrderStatus
    {
        public string userName { get; set; }
        public bool Status { get; set; }
    }
  1. コンシューマースレッドの起動
protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            GlobalConfiguration.Configure(WebApiConfig.Register);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            //Application_Startイベント内でコンシューマースレッドを起動
            Task.Run(OrderProcessor.StartProcessing);
        }

この構成では、ユーザーからのリクエストはキューに追加され、順次処理されます(複数アイテムの同時処理も可能)。

利点:前段階よりも処理速度が向上。

欠点:注文後、別インターフェースでの結果確認が必要。

第3段階:生産者-消費者ロールの逆転、在庫商品を事前にキューに配置

  1. 生産者モジュールと初期化処理の作成
public class StockManager
    {
        /// <summary>
        /// 在庫商品キュー
        /// </summary>
        public static ConcurrentQueue<int> AvailableItems = new ConcurrentQueue<int>();
        /// <summary>
        /// 在庫商品キューの初期化
        /// </summary>
        public static void Initialize()
        {
            using (ProductDbContext db = new ProductDbContext())
            {
                db.stock_item.Where(p => p.status == 0).Select(p => p.id).ToList().ForEach(p =>
                {
                    AvailableItems.Enqueue(p);
                });
            }
        }
    }
 public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            GlobalConfiguration.Configure(WebApiConfig.Register);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            //アプリケーション起動時に在庫キューを初期化
            StockManager.Initialize();
        }
    }
  1. 消費者モジュールの作成
public class OrderController : Controller
    {
        /// <summary>
        /// 注文処理
        /// </summary>
        /// <param name="user">注文者</param>
        /// <returns></returns>
        public async Task<ContentResult> SubmitOrder(string user)
        {
            if (StockManager.AvailableItems.TryDequeue(out int itemId))
            {
                await new OrderExecutor2().ProcessOrder(user, itemId);
                return Content($"注文成功、商品ID:{itemId}");
            }
            else
            {
                await Task.CompletedTask;
                return Content($"商品はすでに在庫切れ");
            }
        }
    }
  1. 実際の業務処理モジュール
/// <summary>
    /// 実際の注文処理モジュール
    /// </summary>
    public class OrderExecutor2
    {
        /// <summary>
        /// 複雑な注文処理(例:データベース操作)
        /// </summary>
        /// <param name="user">注文者</param>
        /// <param name="itemId">商品ID</param>
        /// <returns></returns>
        public async Task ProcessOrder(string user, int itemId)
        {
            using (ProductDbContext db = new ProductDbContext())
            {
                var item = db.stock_item.Where(p => p.id == itemId).FirstOrDefault();
                if (item != null)
                {
                    item.status = 1;
                    item.user_name = user;
                    await db.SaveChangesAsync();
                }
            }
        }
    }

この構成で以下の3つのURLに同時にアクセスすると、データベースに2つの商品しか存在しない場合、1つのリクエストが「商品はすでに在庫切れ」と返される。

http://localhost:88/Order/SubmitOrder?user=zhangsan

http://localhost:88/Order/SubmitOrder?user=lisi

http://localhost:88/Order/SubmitOrder?user=wangwu

この方法の利点:処理速度が速く、第2段階に比べて結果確認用インターフェースが不要。

欠点:現時点では思いつかない。ご意見歓迎。

注:この方法は個人的な仮説であり、実際のプロジェクト経験に基づくものではない。参考としてのみ使用し、プロジェクトでの導入は慎重に検討することを推奨します。ご指摘・ご意見をお待ちしています。

タグ: ASP.NET C# メッセージキュー ConcurrentQueue Entity Framework

6月7日 16:25 投稿