前言
這一節翻譯一篇有關Session State性能問題的文章,非一字一句翻譯。
話題
不知道我們在真實環境中是否用到了Session State特性,它主要用來當在同一瀏覽器發出多個請求時來存儲數據,在現在我們更多的為了避免整個頁面刷新,Web應用程序更多傾向於利用高擴展性的Ajax,但是不知道我們是否注意到當我們使用Session數據多次請求MVC上的Action方法時產生的性能問題呢?
將Session放入上下文中(Put Session into the Context)
在進行代碼演示時我們首先得知道Session的工作原理:當一個新請求第一次到達服務器時,顯然在此之前Cookie中沒有SessionId的,此時服務器將創建一個新的Session標識,通過如下:
System.Web.HttpContext.Current.Session.SessionID
但是並不意味着當有多個請求發送到服務器上時,服務器都會保存一個Session Cookie。只是在Session中保存具體請求的數據,換言之,ASP.NET Framework會首先會添加Session Cookie到響應流中,此時需要保存的數據將被保存在Session中。
因此,說到這里好像和我們要講的主題半毛錢關系都沒有,那跟我們的性能有什么關系呢?ASP.NET能夠處理來自同一瀏覽器的多個請求,如下:
在上述圖片中,當瀏覽器未發出請求時顯然在服務器上不會存儲任何Session數據,如果服務器存儲了一些數據在Session中,此時將會添加一個Session Cookie到響應流中,接下來所有的子請求使用相同的Session Cookie,同時會以隊列的形式依次等待被處理。
我們能夠想象到一種很常見的場景,要是多個請求同時去讀取或修改相同的Session值,此時則會造成不一致的數據。接下來進入我們話題演示時間。
代碼演示
我們在控制器中給出如下代碼:
[OutputCache(NoStore = true, Duration = 0)] //不緩存數據 public class HomeController : Controller { public List<string> boxes = new List<string>() { "red", "green", "blue", "black", "gray", "yellow", "orange" }; // GET: Home public ActionResult Index() { return View(); } public string GetBox() //隨機獲取集合中顏色 { System.Threading.Thread.Sleep(10); Random rnd = new Random(); int index = rnd.Next(0, boxes.Count); return boxes[index]; } public ActionResult StartSession() //啟動Session並存值 { System.Web.HttpContext.Current.Session["Name"] = "Chris"; return RedirectToAction("Index"); } }
接下來我們利用AngularJS在視圖中發出Ajax請求以及其他操作,我們看看視圖中代碼:
<body ng-controller="asyncCtrl" ng-init="getBoxes()"> <nav role="navigation" class="navbar navbar-default navbar-fixed-top"> <div class="container-fluid"> <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header"> <button type="button" data-target="#navbarCollapse" data-toggle="collapse" class="navbar-toggle"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> </div> <!-- Collection of nav links and other content for toggling --> <div id="navbarCollapse" class="collapse navbar-collapse"> <ul class="nav navbar-nav"> <li class="active"><a href="#">Performace testing</a></li> <li> @Html.ActionLink("Start Session", "StartSession") </li> <li> <a class="links" ng-click="getBoxes()">Not resolved</a> </li> <li> <a class="links" ng-click="getBoxes(true)">Resolved</a> </li> <li> <form class="navbar-form"> <label class="checkbox" style="margin-top:5px"> @Html.CheckBox("isSessionNewChk", Session.IsNewSession, new { @disabled = "disabled" }) Is Session new </label> </form> </li> </ul> <ul class="nav navbar-nav navbar-right"> <li><a href="#">{{boxes.length}} Boxes</a></li> </ul> </div> </div> </nav> <br /><br /><br /> <div class="container"> <div class="row"> <div id="boxesContainer" ng-repeat="color in boxes track by $index"> <div class="box" ng-class="color" /> </div> </div> <br /> <div class="row"> <div id="timeOccured" ng-show="showResults" class="alert" ng-class="isResolved()" ng-bind="timeElapsed"></div> </div> </div> <script src="~/Scripts/app.js"></script> </body>
接下來我們看看 app.js
angular.module('asyncApp', []) .value('mvcuri', 'http://localhost:49588/home/getbox') .value('mvcurisessionresolved', 'http://localhost:49588/SessionResolved/getbox') .controller('asyncCtrl', function ($http, $scope, mvcuri, mvcurisessionresolved) { $scope.boxes = []; $scope.showResults = false; var uri; $scope.getBoxes = function (resolved) { var start = new Date(); var counter = 300; if (resolved) uri = mvcurisessionresolved; else uri = mvcuri; // Init variables $scope.boxes = []; $scope.showResults = false; $scope.timeElapsed = ''; for (var i = 0; i < 300; i++) { $http.get(uri) .success(function (data, status, headers, config) { $scope.boxes.push(data); counter--; if (counter == 0) { var time = new Date().getTime() - start.getTime(); $scope.timeElapsed = 'Time elapsed (ms): ' + time; $scope.showResults = true; } }) .error(function (error) { $scope.timeElapsed = error.Message; }).finally(function () { }); } }; $scope.isResolved = function () { return uri == mvcuri ? 'alert-danger' : 'alert-success'; } });
上述AngularJS腳本比較簡單就不敘述。接下來再創建一個控制器 SessionResolvedController 來進行比較。
[OutputCache(NoStore = true, Duration = 0)] public class SessionResolvedController : Controller { public List<string> boxes = new List<string>() { "red", "green", "blue", "black", "gray", "yellow", "orange" }; public string GetBox() { try { System.Threading.Thread.Sleep(10); Random rnd = new Random(); int index = rnd.Next(0, boxes.Count); return boxes[index]; } catch(Exception ex) { return "red"; } } }
此時我們看看運行效果:
當我們運行程序時,此時復選框是勾上的,說明此時還未有添加數據到Session中,接下來我們點擊 Start Session 看看效果:
上述我們是啟動Start Session並控制整發出300個Ajax請求並返回隨機顏色。我們看到花了8722毫秒。
接下來我們點擊 Not resolved 看看耗時多少,如下:
耗時8166毫秒,看來和啟動Session沒什么區別可言。因為每個到服務器的請求都有一個Session Cookie,此時所有的請求將會被依次處理正如我們之前所描述的那樣,所以接下來我們進行如下操作:
[SessionState(SessionStateBehavior.Disabled)] public class SessionResolvedController : Controller {.....}
我們禁用SessionState看看效果:
注意:上述程序運行建議在Release模式下進行演示,可能這樣的效果更加明顯。
參考
ASP.NET MVC Session state Performance Issue
結語
當有多個請求發送到服務器時此時若進行Session操作將會對性能產生一定影響。我們通過設置 [SessionState(SessionStateBehavior.Disabled)] 特性最終驗證了這一觀點。但是這樣設置后我們將無法獲取Session中的值。所以在請求數量較多的情況下,建議對於Ajax請求使用Web APi來完成,我們通過Web APi來接收Ajax請求,可以使用Session中的數據來渲染視圖或者提交到數據或者參數到MVC控制器的Action方法上。