添加了 HttpContext.Session 中缺少的键(并发)



我正在使用.NET Core 3.1。我想知道HttpContext.Session.SetString(...)是否线程安全?例如,客户端同时向同一控制器操作(Test)发出两个请求。控制器将字符串添加到会话(请参阅下面的示例)。会话结束时(例如,当我刷新页面时)中会有两个、一个或零个键吗?

public IActionResult Test()
{
HttpContext.Session.SetString(Guid.NewGuid().ToString(), "test");
return Ok();
}

我在使用DevExtreme FileUploader时将一些值保存到会话中。当我一次上传多个文件时,组件同时发出多个请求,最后,会话中通常会缺少一些键。我认为有一些竞争条件正在发生。

新增:客户端代码

我注意到只有当我使用会话密钥时才会丢失method: 'POST'(只有 1 个密钥)。如果我使用method: 'GET',有 3 个键(正确)。

$(document).ready(function () {
var method = 'GET'; // works (3 keys)
//var method = 'POST'; // doesn't work (1 key)
$('#fire').on('click', function () {
$.when(
$.ajax({
url: 'Session/Test',
method: method,
success: function(){
console.log('response', 1);
}
}),
$.ajax({
url: 'Session/Test',
method: method,
success: function(){
console.log('response', 2);
}
}),
$.ajax({
url: 'Session/Test',
method: method,
success: function(){
console.log('response', 3);
}
})
).then(function () {
alert('Done');
});
});
});

假设您使用 ASP.Net Core提供的默认会话实现。

HttpContext.Session方面:

HttpContext.Session返回DistributedSession的实例,该实例在内部使用Dictionary<TKey, TValaue>。字典不是线程安全的,因此如果您从多个线程访问Session(例如Task.Run),可能会导致意外结果。

就不同请求的会话而言:

在 ASP.Net 核心中,Session来自ISessionStore,它具有短暂的寿命。这意味着,Session对象不被请求共享。因此,如果您有并发请求,则每个请求都有自己的Session对象。

在竞争条件方面:

会话的默认实现从 Cookie 读取/写入会话状态.AspNetCore.Session。这可能会导致争用条件。

由于Session是按客户端进行的,因此当您有来自同一客户端的并发请求触及相同 cookie/会话状态中的相同位和片段时,您可能会遇到争用条件。但是,争用条件不是因为服务器端的Session。它实际上是由客户端的 cookie 管理引起的。

会话状态为非锁定。如果两个请求同时尝试修改会话的内容,则最后一个请求将覆盖第一个请求。

请考虑以下示例:

假设您有一个控制器操作,该操作使用提供的value设置Session,另一个控制器操作从Session中检索值并将其返回到正文中:

[HttpGet]
[Route("create")]
public IActionResult CreateSession([FromQuery]string value)
{
HttpContext.Session.SetString("key", value);
return Ok();
}
[HttpGet]
[Route("get")]
public IActionResult ReturnSession([FromQuery] string expected)
{
var actual = HttpContext.Session.GetString("key");
return Ok(new { actual, expected });
}

如果使用HttpClient测试这些操作:

async Task TestSession(HttpClient client, string str)
{
await client.GetAsync($"https://localhost:5001/session/create?value={str}");
var r = await client.GetAsync($"https://localhost:5001/session/get?expected={str}");
var session = await r.Content.ReadAsStringAsync();
Console.WriteLine(session);
}
using (var client = new HttpClient())
{
await TestSession(client, "abc");
}

输出应如下所示:

{"actual":"abc","expected":"abc"}

当您有来自同一客户端的并发请求时,将引发问题:

using (var client = new HttpClient())
{
var tasks = new List<Task>();
for (var i = 0; i < 10; i++)
{
var str = i.ToString();
tasks.Add(Task.Run(() => TestSession(client, str)));
}
await Task.WhenAll(tasks);
}

输出如下所示:

{"actual":"2","expected":"1"}
{"actual":"3","expected":"6"}
{"actual":"4","expected":"5"}
{"actual":"4","expected":"3"}
{"actual":"4","expected":"2"}
{"actual":"8","expected":"4"}
{"actual":"7","expected":"8"}
{"actual":"7","expected":"7"}
{"actual":"9","expected":"0"}
{"actual":"9","expected":"9"}

在上述情况下,请求 3在请求6创建和获取之间更改了会话状态,这意味着请求 6可能无法正确查看其会话状态。

若要避免此问题,可以对每个批次使用不同的HttpClient

不,dotnet 集合不是线程安全的,会话也不是。 如果要在多个线程上共享同一会话,则应将其视为只读

最新更新