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段階:メッセージキューによる生産者-消費者モデルの導入
- 注文受付エントリ(生産者)の作成
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());
}
}
}
- 注文処理モジュール(消費者)の作成
/// <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 });
}
}
}
- 実際の業務処理モジュールの作成
/// <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; }
}
- コンシューマースレッドの起動
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段階:生産者-消費者ロールの逆転、在庫商品を事前にキューに配置
- 生産者モジュールと初期化処理の作成
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();
}
}
- 消費者モジュールの作成
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($"商品はすでに在庫切れ");
}
}
}
- 実際の業務処理モジュール
/// <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段階に比べて結果確認用インターフェースが不要。
欠点:現時点では思いつかない。ご意見歓迎。
注:この方法は個人的な仮説であり、実際のプロジェクト経験に基づくものではない。参考としてのみ使用し、プロジェクトでの導入は慎重に検討することを推奨します。ご指摘・ご意見をお待ちしています。