來看一個有趣實驗。
以下是個簡單的 ASP.NET MVC Controller,在 Index View 透過 AJAX 呼叫向 Server 讀取資料,SimuAjaxCall 則模擬 AJAX 呼叫動作,使用 Thread.Delay() 延遲指定秒數後傳回字串結果:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace LabWeb.Controllers
{
publicclass HomeController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult SimuAjaxCall(int seqNo, int delay)
{
System.Threading.Thread.Sleep(delay * 1000);
return Content($"AjaxCall-{seqNo}");
}
}
}
Index.cshtml 網頁內容如下。有個測試按鈕觸發同步發出 7 個 AJAX 呼叫 SimuAjaxCall,並將每次呼叫取得內容顯示在網頁上。先聲明,這並非良好的設計方式,依據 HTTP 規範,瀏覽器對同一網站來源的同時連線數有其上限,預設為 6 條,故第 7 個 AJAX 請求必須等待前 6 個請求有人執行完畢後才會送出,故設計時應盡可能透過合併或其他技巧,減少 AJAX Request 數目。(關於連線數上限議題,可參考這篇文章)網頁上還有另一顆「變蝸牛」按鈕,背後呼叫 /Magic/Snail 取得字串顯示,至於它背後做了什麼事,在此先賣個關子。
@{
Layout = null;
}
<!DOCTYPEhtml>
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>Multiple AJAX Call Test</title>
</head>
<body>
<div>
<buttonid="btnAjax">測試 AJAX 呼叫</button>
<buttonid="btnSnail">變蝸牛</button>
<ul>
</ul>
</div>
<scriptsrc="https://code.jquery.com/jquery-3.2.1.js"></script>
<script>
$("#btnAjax").click(function () {
//發出7個耗時1秒的AJAX呼叫
for (var i = 0; i < 7; i++) {
$.post("/Home/SimuAjaxCall", { seqNo: i, delay: 1 })
.done(function (res) {
$("ul").append("<li>" + res + "</li>");
});
}
});
$("#btnSnail").click(function () {
$.post("/Magic/Snail")
.done(function (res) {
$("ul").append("<li>" + res + "</li>");
});
});
</script>
</body>
</html>
我們的測試方法是先按「測試AJAX呼叫」,用 F12 開發者工具觀察 7 個 AJAX Request 的執行時間,接著使用「變蝸牛」魔法捲軸,之後再按一次「測試AJAX呼叫」觀察結果差異。實測結果如下:
第一次 7 個 AJAX Request 齊發測試(黃色部分)一如預期,前六個同步執行,第七個等了 1 秒才執行(1 秒綠色長條前方有 1 秒的灰色細長條為等待時間),驗證了瀏覽器對同一站台同時連線上限數為 6。
呼叫 /Magic/Snail 後再做一次相同測試,結果卻截然不同,七個 AJAX Request 分別花了 1 到 8 秒才執行完(紅色部分)!若使用者必須等待全部 AJAX Request 完成,等待時間也由 2 秒拉長到 8 秒。
這情境似曾相識,對吧?(感覺陌生的同學可參考 再探ASP.NET大排長龍問題)
是的,揭曉 /Magic/Snail 裡的魔法,就是 Session!
using System.Web.Mvc;
namespace LabWeb.Controllers
{
publicclass MagicController : Controller
{
public ActionResult Snail()
{
Session["A"] = 123;
return Content("變蝸牛!");
}
}
}
有趣的實驗,但發生在真實環境我可笑不出來… (補聲暗)
當時我遇到的狀況是網頁同時發出十多個 AJAX Request,前六個 AJAX 呼叫每個耗時 5-12 秒,但個別執行明明只要 1-3 秒。尤其某個應該瞬間完成的 Action,我在 Action 第一行跟最後一行寫 Log 記錄執行時間,發現 Action 從開始到結束花不到 0.1 秒,但 IIS Log 記錄跟瀏覽器端觀察到執行時間都在 4 秒以上,推測時間耗消在呼叫 MVC Action 之前或 Action 完成之後,卻又無從調查。同事 J 提醒可能與 Session 有關,這才恍然大悟。萬萬沒想到,原本以為不用 WebForm 就再也不用擔心 Session 阻塞交通,但事實不然…
一旦你在網站應用程式的某個角落用了 Session,MVC Action 也會大排長龍,一秒變蝸牛!
追究原因,程式用了某個 WebForm 時代的古老元件,其中使用 Session 保存狀態。傳統 WebForm 以 PostBack 為主,Session 的鎖定行為影響有限,當應用在會同時發出多個 AJAX Request 的場景,便導致了可怕的後遺症。
解決方法很簡單,有同步 AJAX 執行需求且要避免被 Session 摧毁效能的 Controller 上請加註[SessionSate()],設成 Disabled 或 ReadOnly:(前題是這個 Controller 未使用 Session 或對 Session 只讀不寫)
[SessionState(System.Web.SessionState.SessionStateBehavior.Disabled)]
publicclass HomeController : Controller
修正後,Action 同步呼叫不再變蝸牛。
狠狠地被上了一課!如果你的網站採取 AJAX 方式設計,Session 這種活化石,就別再用了。