我有一个 .NET 7 应用程序,每天处理几千个用户。在高峰时段,应用程序会消耗其运行的 IIS 服务器上的所有资源。
我知道下一步是容器化和所有这些好东西,但现在我想充分利用我所拥有的东西。我对开发还很陌生,而且我没有计算机科学背景,我不能自称完全理解异步或多线程。
举个例子,我有一个端点,每秒可能被调用大约 300 次。数据库后端是 SQL Server 2019(标准),我使用的是 Entity Framework Code First。它使用 .NET Identity 在这些端点上进行身份验证。
关于如何使这两个片段绝对最快,并在不破坏我的服务器的情况下处理最多数量的并发用户的任何建议,以及一个简短的解释,以便我可以在我的应用程序中采用类似的范例。
获取结果
[Route("/mobile/result/{matchtype}/{referenceid}/{lastchecked}")]
public async Task<IActionResult> GetResult(MatchType matchType, long referenceid, string lastchecked)
{
DateTime datetocheck = DateTime.Parse(HttpUtility.UrlDecode(lastchecked));
Result response = new Result();
if (matchType == MatchType.League)
{
response = _context.Results
.Where(i => i.Fixture.Id == referenceid && i.LastUpdated >= datetocheck)
.Select(i => new Result()
{
ReferenceId = referenceid,
MatchType = matchType,
ResultId = i.Id,
HomeFrameScore = i.HomeScore,
AwayFrameScore = i.AwayScore,
HomeHandicap = i.HomeTeam.Handicap,
AwayHandicap = i.AwayTeam.Handicap,
MatchStatus = i.Status
//Set scores for comps
})
.FirstOrDefault();
if (response == null)
return NoContent();
response.Frames = new List<Frame>();
response.Frames = _context.ResultFrames
.Where(i => i.Result.Fixture.Id == referenceid)
.Select(i => new Frame()
{
ResultFrameId = i.Id,
FrameNumber = i.FrameNumber,
HomeBreak = i.HomeBreak,
HomeDish = i.HomeDish,
HomeForfeit = i.HomeForfeit,
HomeLockedIn = i.HomeLockedIn,
HomeScore = i.HomeScore,
AwayBreak = i.AwayBreak,
AwayDish = i.AwayDish,
AwayForfeit = i.AwayForfeit,
AwayLockedIn = i.AwayLockedIn,
AwayScore = i.AwayScore,
Status = i.FrameStatus,
MatchFormatSectionId = i.MatchFormatSection.Id
}).ToList();
var resultframeplayers = _context.ResultFramePlayers
.Where(i => i.Result.Fixture.Id == referenceid)
.Select(i => new FramePlayer()
{
ResultFramePlayerId = i.Id,
Number = i.Number,
ResultFrameId = i.ResultFrame.Id,
Joker = i.Joker,
HomeAway = i.HomeAway,
PlayerId = i.PlayerSeasonTeam != null ? i.PlayerSeasonTeam.Player.Id : 0
}).ToList();
foreach( var rf in response.Frames)
{
rf.HomeFramePlayers = resultframeplayers.Where(i => i.ResultFrameId == rf.ResultFrameId && i.HomeAway == HomeAway.Home).ToList();
rf.AwayFramePlayers = resultframeplayers.Where(i => i.ResultFrameId == rf.ResultFrameId && i.HomeAway == HomeAway.Away).ToList();
}
}
return Json(response);
}
锁定部分
[Route("/mobile/lockinsection")]
[HttpPost]
public async Task<IActionResult> LockInSection([FromBody] FrameResultData data)
{
var user = await _userManager.GetUserAsync(User);
if (data.MatchType == MatchType.League)
{
var checkcaptain = ValidCaptain(data.MatchType, data.ReferenceId);
if (checkcaptain == false)
return BadRequest("Not a valid captain for this match");
var captainhomeaway = CaptainHomeOrAway(data.MatchType, data.ReferenceId);
if (data.LockIn.HomeAway != captainhomeaway)
return BadRequest("Captain locking in for the wrong team");
foreach(var frame in data.LockIn.Frames)
{
var resultframe = _context.ResultFrames
.Include(i => i.Result)
.FirstOrDefault(i => i.Id == frame.ResultFrameId);
if (data.LockIn.HomeAway == HomeAway.Home)
resultframe.HomeLockedIn = LockedIn.True;
if (data.LockIn.HomeAway == HomeAway.Away)
resultframe.AwayLockedIn = LockedIn.True;
_context.Update(resultframe);
}
var result = _context.Results.FirstOrDefault(i => i.Fixture.Id == data.ReferenceId);
result.LastUpdated = DateTime.UtcNow;
_context.Update(result);
_context.SaveChanges();
return Ok();
}
else
{
//Competition
}
return BadRequest();
}
关于性能,有两个相关但独立的问题需要考虑。执行一段代码需要时间,然后还有等待该响应的代码可以做什么的问题。
对于性能,我可以建议进行一些调整。在阅读中,您可以通过利用导航属性引用来简化对单个查询的调用:
response = _context.Results
.Where(i => i.Fixture.Id == referenceid && i.LastUpdated >= datetocheck)
.Select(i => new { Result = new Result()
{
ReferenceId = referenceid,
MatchType = matchType,
ResultId = i.Id,
HomeFrameScore = i.HomeScore,
AwayFrameScore = i.AwayScore,
HomeHandicap = i.HomeTeam.Handicap,
AwayHandicap = i.AwayTeam.Handicap,
MatchStatus = i.Status
Frames = i.Frames.Select(f => new new Frame
{
ResultFrameId = f.Id,
FrameNumber = f.FrameNumber,
HomeBreak = f.HomeBreak,
HomeDish = f.HomeDish,
HomeForfeit = f.HomeForfeit,
HomeLockedIn = f.HomeLockedIn,
HomeScore = f.HomeScore,
AwayBreak = f.AwayBreak,
AwayDish = f.AwayDish,
AwayForfeit = f.AwayForfeit,
AwayLockedIn = f.AwayLockedIn,
AwayScore = f.AwayScore,
Status = f.FrameStatus,
MatchFormatSectionId = f.MatchFormatSection.Id,
}).ToList();
},
MatchPlayers = i.Players.Select(p => new FramePlayer()
{
ResultFramePlayerId = p.Id,
Number = p.Number,
ResultFrameId = p.ResultFrame.Id,
Joker = p.Joker,
HomeAway = p.HomeAway,
PlayerId = p.PlayerSeasonTeam != null ? p.PlayerSeasonTeam.Player.Id : 0
}).ToList();
}).FirstOrDefault();
它的作用是在单个查询中读取您与框架的匹配,然后根据我称为“玩家”的预期导航属性单独加载所有玩家作为示例。因此,生成的匿名对象包含结果及其关联帧,以及结果的所有玩家的集合。从那里您可以浏览框架来过滤适当的主客场设置,而不是再次访问数据库。您可以在填充帧时在内部查询结果帧播放器,但一次性加载它们并进行过滤可能更有效。这使得数据库无需多次往返即可完成大部分工作。
对于更新...我只需加载结果及其关联的帧一次,然后更新字段:所以而不是这样:
foreach(var frame in data.LockIn.Frames)
{
var resultframe = _context.ResultFrames
.Include(i => i.Result)
.FirstOrDefault(i => i.Id == frame.ResultFrameId);
if (data.LockIn.HomeAway == HomeAway.Home)
resultframe.HomeLockedIn = LockedIn.True;
if (data.LockIn.HomeAway == HomeAway.Away)
resultframe.AwayLockedIn = LockedIn.True;
_context.Update(resultframe);
}
var result = _context.Results.FirstOrDefault(i => i.Fixture.Id == data.ReferenceId);
result.LastUpdated = DateTime.UtcNow;
如果您更新 3 个帧,将执行 4 个查询,这可以改进为仅使用单个查询:
var result = _context.Results
.Include(r => r.Frames)
.FirstOrDefault(i => i.Fixture.Id == data.ReferenceId);
然后在结果中找到适用的帧并更改这些值以及 Result.last 修改等。或者,如果结果有很多帧而您只需要特定的一组:
var frameIds = data.Login.Frames.Select(i => i.ResultFrameId).ToList();
var result = _context.Results
.Include(r => r.Frames.Where(f => frameIds.Contains(f.Id))
.FirstOrDefault(i => i.Fixture.Id == data.ReferenceId);
因此,如果结果包含数十个帧,而我们只关心更新一小部分,那么仅加载我们关心的帧而不是全部可能会更有效。
接下来的事情是在加载跟踪实体时不使用
Update()
,只需更新值并调用SaveChanges()
。 Update()
是一种更新分离实体的方法。 Update()
的问题在于,无论是否发生任何更改,它都会为所有列生成更新 SQL 语句。对于跟踪的实体,EF 将仅针对已更改的列生成更新 SQL 语句,并且仅在实际发生更改时才生成。
您的 API 方法设置为异步执行,尽管其中的所有代码大部分都是同步的。这意味着调用将阻塞并且在代码完成之前不执行任何操作。异步不会使代码执行得更快,如果有的话,它会使代码执行得稍微慢一些,但它确实可以让调用者在代码执行时可以做其他事情。对于 Web API,等待代码是处理针对服务器的 Web 请求的线程。在同步世界中,如果您有 100 个工作线程,您可以接收 100 个请求,那么请求 101 必须等待其中一个工作线程完成。提高响应能力的唯一方法是使代码更快或增加资源,以便服务器一次可以处理 200 个请求等。这可以是垂直扩展(更大的硬件)或水平扩展(具有负载平衡的更多侦听器)。数据库连接等瓶颈。一旦代码尽可能快地执行,并且您平均每个请求 150 毫秒,但希望确保 Web 服务器能够响应,这就是异步可以提供帮助的地方。这可能意味着代码需要 160 毫秒而不是 150 毫秒才能运行,但会在执行时释放 Web 请求线程。第 101 个请求不会等待 150 毫秒才开始,它可能只等待 45 毫秒。最好情况下,对每个请求的响应可能需要 160 毫秒或更长时间,但每个服务器实例通常可以处理更多请求。由于您的方法已设置为
async
,因此您可以通过使用 FirstOrDefaultAsync()
而不是 FirstOrDefault()
以及类似 SaveChangesAsync()
的方法等待读取来提高响应能力。请务必await
他们。
希望这能给您一些值得思考和尝试的事情,请随时用您认为可能需要改进的特定问题领域来更新问题。