后端工程能力 ASP.NET Core 实战

📅 2026/7/11 2:37:37
后端工程能力 ASP.NET Core 实战
后端工程能力解决的是接口在真实环境里的稳定性问题重复提交怎么办、部分成功怎么办、并发冲突怎么办、异常怎么统一返回、日志怎么定位、谁能访问接口、请求太多怎么办、外部服务失败要不要重试、多实例部署时怎么保证同一资源只有一个执行者。这一类能力和 ORM / SQL 优化不同。SQL 优化重点是“查得快不快”后端工程能力重点是“接口在错误、并发、重复请求、权限、流量压力下是否仍然可控”。业务背景继续使用简化电商订单系统用户创建订单。用户支付订单。用户取消订单。系统扣减库存。后台查询订单和处理异常订单。这个背景能覆盖真实工作中最常见的后端工程问题用户重复点击“提交订单”。支付回调重复通知。扣库存时多个请求同时进来。数据库写入成功但后续步骤失败。接口抛异常后前端不知道发生了什么。线上接口慢但日志无法定位。未登录用户访问了需要授权的接口。突然大量请求打到下单接口。调用第三方支付接口失败要不要重试。多台服务实例同时处理同一个任务。请求链路一个典型 ASP.NET Core 订单接口可以这样理解HTTP 请求 - 鉴权/授权中间件 - 限流中间件 - TraceId / 日志作用域 - Controller / Minimal API endpoint - 参数校验 - 应用服务 - 幂等检查 - 事务 - EF Core / 数据库 - 调用外部服务或消息队列 - 统一响应 - 结构化日志记录结果学习时不要只看接口能不能返回200 OK。更重要的是回答重复请求会不会造成重复数据 异常时数据会不会部分成功 并发时库存会不会扣成负数 日志能不能定位到具体请求 未授权请求能不能被挡住 请求过多时系统会不会被打垮 重试会不会把副作用执行多次 多实例部署时锁是否还有效分步实战路线第 0 步理解后端工程能力解决什么问题当前问题很多接口一开始只是“能跑”但生产环境真正的问题来自异常、重复、并发、权限、流量和外部依赖。问题写法app.MapPost(/orders, async (CreateOrderRequest request, AppDbContext db) { var order new Order { CustomerId request.CustomerId, Status Pending }; db.Orders.Add(order); await db.SaveChangesAsync(); return Results.Ok(order.Id); });这段代码能创建订单但没有处理参数是否合法 重复提交 事务边界 并发库存 异常返回 日志定位 权限控制 限流保护推荐理解后端工程能力不是“让接口看起来更复杂”而是让接口在不理想条件下仍然可预测。完成判断能说清楚“能跑的接口”和“生产级接口”的差异。下一步先处理最基础的异常问题。第 1 步异常处理当前问题如果每个接口自己写try/catch很容易返回格式不统一、日志遗漏、异常被吞掉。问题写法app.MapPost(/orders, async (CreateOrderRequest request, AppDbContext db) { try { var order new Order { CustomerId request.CustomerId }; db.Orders.Add(order); await db.SaveChangesAsync(); return Results.Ok(order.Id); } catch (Exception ex) { return Results.Problem(ex.Message); } });问题在哪里每个接口重复 try/catch 异常响应格式不统一 可能把内部异常信息暴露给前端 日志上下文不完整推荐写法app.UseExceptionHandler(errorApp { errorApp.Run(async context { var exception context.Features .GetIExceptionHandlerFeature() ?.Error; ​ var traceId context.TraceIdentifier; context.Response.StatusCode StatusCodes.Status500InternalServerError; ​ await context.Response.WriteAsJsonAsync(new { code InternalServerError, message 服务器内部错误, traceId }); }); });知识点统一异常处理应该放在中间件层。业务异常可以转成明确错误码未知异常统一返回安全提示并记录完整日志。观察点接口是否返回统一格式 响应里是否有 traceId 日志里是否记录完整异常堆栈 前端是否看不到数据库连接串、SQL、堆栈等敏感信息常见坑catch 后不记录日志 把 ex.Message 直接返回给用户 业务异常和系统异常不区分完成判断任意接口抛异常时都能得到统一响应并能用traceId在日志里找到完整错误。下一步让日志能稳定定位一次请求。第 2 步日志当前问题线上接口慢或报错时如果日志只是几行普通字符串很难知道是哪个用户、哪个接口、哪次请求、哪条 SQL 出了问题。问题写法Console.WriteLine(create order failed);问题在哪里没有 TraceId 没有结构化字段 不能按订单号、用户、接口、耗时检索 无法关联 SQL、异常和请求推荐写法logger.LogInformation( Create order started. CustomerId{CustomerId}, IdempotencyKey{IdempotencyKey}, request.CustomerId, idempotencyKey);再配合请求作用域using (logger.BeginScope(new Dictionarystring, object { [TraceId] context.TraceIdentifier, [Path] context.Request.Path.Value ?? })) { await next(context); }知识点大型项目日志不是为了“多打印”而是为了“能检索、能关联、能复盘”。关键字段通常包括TraceId、接口路径、用户 ID、订单号、耗时、错误码、SQL 标签。观察点一次请求的入口日志、业务日志、异常日志、SQL 日志是否能用 TraceId 串起来 日志是否能按 OrderId / CustomerId / IdempotencyKey 查询 是否记录耗时常见坑只记录中文句子不记录结构化字段 日志里输出密码、Token、身份证等敏感信息 成功日志太多错误日志太少 没有 TraceId无法跨服务定位完成判断给你一个接口报错响应里的traceId你能在日志里找到这次请求执行过哪些关键步骤。下一步处理多步写入时的数据一致性。第 3 步事务当前问题创建订单通常不只插入一张表还可能写订单、订单明细、库存流水、支付记录。如果中间一步失败不能只成功一半。问题写法db.Orders.Add(order); await db.SaveChangesAsync(); ​ db.OrderItems.AddRange(items); await db.SaveChangesAsync(); ​ db.InventoryLogs.Add(log); await db.SaveChangesAsync();问题在哪里订单已创建但明细失败 明细已创建但库存流水失败 多个 SaveChanges 之间没有统一事务边界推荐写法await using var transaction await db.Database.BeginTransactionAsync(); ​ db.Orders.Add(order); db.OrderItems.AddRange(items); db.InventoryLogs.Add(log); ​ await db.SaveChangesAsync(); await transaction.CommitAsync();知识点事务保证一组数据库操作要么全部成功要么全部回滚。单个SaveChanges默认有事务但多个SaveChanges或混合多资源操作时要明确事务边界。观察点故意让中间步骤抛异常 检查 Orders / OrderItems / InventoryLogs 是否都没有脏数据 检查日志里是否记录事务失败原因常见坑事务范围太大长时间占锁 事务里调用慢外部接口 数据库事务和消息发送不能天然保证一致完成判断任一步失败时数据库不会留下半成功订单。下一步处理用户重复提交的问题。第 4 步幂等当前问题用户可能重复点击提交订单支付平台也可能重复回调。如果接口没有幂等保护同一个业务动作可能执行多次。问题写法app.MapPost(/orders, async (CreateOrderRequest request, AppDbContext db) { var order new Order { CustomerId request.CustomerId }; db.Orders.Add(order); await db.SaveChangesAsync(); return Results.Ok(order.Id); });问题在哪里同一个请求发两次就可能创建两笔订单 网络超时后客户端重试服务端可能重复执行 支付回调重复通知可能重复更新状态或重复发货推荐写法客户端或调用方传入Idempotency-Key: create-order-20260710-0001服务端记录幂等键var existing await db.IdempotencyRecords .FirstOrDefaultAsync(x x.Key idempotencyKey); ​ if (existing is not null) { return Results.Ok(existing.ResponseBody); } ​ // 执行业务成功后保存幂等结果知识点幂等不是“不允许重复请求”而是“重复请求得到同一个业务结果不重复产生副作用”。适合创建订单、支付回调、发券、发货、扣库存等场景。观察点同一个 Idempotency-Key 连续请求两次 数据库是否只生成一笔订单 第二次响应是否复用第一次结果 日志是否能看到幂等命中常见坑只靠前端按钮置灰 幂等 Key 没有唯一索引 失败请求是否记录幂等结果没有定义清楚 Key 永不过期导致表无限增长完成判断重复调用同一个创建订单请求不会重复创建订单。下一步处理多个请求同时修改同一资源的问题。第 5 步并发控制当前问题多个用户同时购买同一个库存很少的商品如果只是先查库存再扣减可能出现超卖。问题写法var product await db.Products.FindAsync(productId); ​ if (product.StockQuantity 0) { return Results.BadRequest(库存不足); } ​ product.StockQuantity - 1; await db.SaveChangesAsync();问题在哪里两个请求同时读到库存 1 两个请求都判断库存足够 两个请求都扣减成功 最终可能超卖推荐写法一条件更新。var affectedRows await db.Products .Where(p p.ProductId productId p.StockQuantity 0) .ExecuteUpdateAsync(setters setters .SetProperty(p p.StockQuantity, p p.StockQuantity - 1)); ​ if (affectedRows 0) { return Results.BadRequest(库存不足); }推荐写法二乐观并发。public byte[] RowVersion { get; set; } Array.Emptybyte(); modelBuilder.EntityProduct() .Property(p p.RowVersion) .IsRowVersion();知识点并发控制的核心是避免“先查再改”的时间窗口。条件更新适合库存扣减这类简单原子操作乐观并发适合编辑订单、资料修改等冲突检测场景。观察点并发请求同一个商品 最终库存是否会小于 0 失败请求是否明确返回库存不足或并发冲突 日志是否能看到 affectedRows 0常见坑只在 C# 里 lock部署多实例后失效 事务隔离级别乱升导致锁范围过大 没有唯一约束或条件更新兜底完成判断多个请求同时扣减同一库存时不会超卖。下一步处理谁能访问接口的问题。第 6 步鉴权当前问题不是所有接口都能匿名访问。比如查询自己的订单、取消订单、后台修改订单状态都需要确认用户身份和权限。问题写法app.MapPost(/admin/orders/{id}/close, async (int id, AppDbContext db) { // 任何人都能调用 });问题在哪里未登录用户能访问敏感接口 普通用户能调用后台接口 用户 A 可能操作用户 B 的订单推荐写法builder.Services .AddAuthentication(Bearer) .AddJwtBearer(Bearer, options { options.Authority https://identity.example.com; options.Audience order-api; }); ​ builder.Services.AddAuthorization(options { options.AddPolicy(AdminOnly, policy policy.RequireRole(Admin)); });接口上声明权限app.MapPost(/admin/orders/{id}/close, CloseOrder) .RequireAuthorization(AdminOnly);知识点认证解决“你是谁”授权解决“你能做什么”。业务里还要检查资源归属比如用户只能操作自己的订单。观察点无 Token 调用是否返回 401 普通用户调用后台接口是否返回 403 用户 A 操作用户 B 订单是否被拒绝 日志是否记录 userId、role、path、traceId常见坑只在前端隐藏按钮后端不校验 只校验登录不校验资源归属 Token 过期、签名、Audience、Issuer 校验不严格完成判断不同身份调用同一接口时能得到正确的 401、403 或业务拒绝。下一步处理请求过多的问题。第 7 步限流当前问题下单、登录、发送验证码、查询报表等接口如果没有限流突发请求会拖垮服务或数据库。问题写法app.MapPost(/orders, CreateOrder);问题在哪里同一个用户可以疯狂请求 恶意 IP 可以打爆接口 数据库连接池可能被耗尽推荐写法builder.Services.AddRateLimiter(options { options.AddFixedWindowLimiter(CreateOrder, limiterOptions { limiterOptions.Window TimeSpan.FromSeconds(10); limiterOptions.PermitLimit 5; limiterOptions.QueueLimit 0; }); }); ​ app.UseRateLimiter(); ​ app.MapPost(/orders, CreateOrder) .RequireRateLimiting(CreateOrder);知识点限流是保护系统不是解决业务正确性。常见维度包括 IP、用户 ID、接口、租户、客户端。生产中常结合网关、Redis 或云厂商限流能力。观察点连续快速请求是否返回 429 日志是否记录被限流的 userId / IP / path 限流是否只影响目标接口常见坑只做单机内存限流多实例下总量不准 限流策略太粗误伤正常用户 没有给前端返回 Retry-After 或明确错误码完成判断短时间重复调用接口时超过阈值的请求会被拒绝并返回明确状态。下一步处理外部服务瞬时失败的问题。第 8 步重试当前问题调用支付、短信、库存、第三方 API 时可能遇到网络抖动或超时。完全不重试会降低成功率但无脑重试会放大故障或重复副作用。问题写法await paymentClient.PayAsync(orderId, amount);或者更危险for (var i 0; i 3; i) { await paymentClient.PayAsync(orderId, amount); }问题在哪里不知道哪些错误可重试 重试没有退避瞬间打爆下游 有副作用的操作重复执行 没有幂等 Key支付可能重复推荐写法var retryPolicy Policy .HandleHttpRequestException() .OrTimeoutException() .WaitAndRetryAsync(3, attempt TimeSpan.FromMilliseconds(100 * attempt)); ​ await retryPolicy.ExecuteAsync(() paymentClient.PayAsync(orderId, amount, idempotencyKey));知识点重试只适合瞬时故障比如网络抖动、超时、临时 503。业务失败、参数错误、余额不足不能重试。有副作用的重试必须配合幂等。观察点日志里是否能看到第几次重试 每次重试是否带同一个 Idempotency-Key 不可重试错误是否立即失败 重试总耗时是否可控常见坑对所有异常都重试 没有超时控制 多个层级都重试导致重试风暴 重试写操作但没有幂等保护完成判断瞬时失败会有限次数重试业务失败不会重试有副作用操作不会重复执行。下一步处理多实例同时执行同一任务的问题。第 9 步分布式锁当前问题单机lock只能保护当前进程。服务部署多实例后多个实例可能同时处理同一订单、同一库存、同一后台任务。问题写法private static readonly object LockObj new(); ​ lock (LockObj) { // 只保护当前进程 }问题在哪里多台服务器各有自己的内存锁 实例 A 和实例 B 仍然会同时执行 进程崩溃后锁状态不可控推荐写法一Redis 分布式锁。var lockKey $order:pay:{orderId}; var token Guid.NewGuid().ToString(N); ​ var acquired await redis.StringSetAsync( lockKey, token, expiry: TimeSpan.FromSeconds(30), when: When.NotExists); ​ if (!acquired) { return Results.Conflict(订单正在处理中); }推荐写法二数据库唯一约束或状态机兜底。CREATE UNIQUE INDEX UX_Payment_OrderId ON Payments(OrderId);知识点分布式锁解决多实例互斥问题但它不是万能一致性方案。关键业务仍然要靠数据库唯一约束、状态条件更新、幂等记录兜底。观察点两个实例同时处理同一订单时是否只有一个拿到锁 锁是否有过期时间 释放锁时是否校验 token避免误删别人的锁 数据库是否有唯一约束兜底常见坑锁没有过期时间进程崩溃后死锁 释放锁不校验 token 只依赖锁没有数据库约束兜底 业务执行时间超过锁过期时间完成判断多实例同时处理同一资源时只有一个执行成功其他请求明确失败或等待并且数据库不会产生重复副作用。下一步整理生产级接口检查清单。第 10 步整理生产级接口检查清单当前问题后端工程能力学完后不能只记住概念要能在写接口时逐项检查。检查清单能力写接口时要问的问题异常处理失败时是否统一返回日志能否定位日志是否有 TraceId、用户、接口、耗时、关键业务 ID事务多表写入是否在同一事务失败是否回滚幂等重复提交或回调是否只产生一次副作用并发控制同一资源被同时修改时是否安全鉴权用户是谁是否有权限是否能操作该资源限流请求过多时是否保护接口和数据库重试是否只对瞬时故障重试是否配合幂等分布式锁多实例下是否仍然互斥是否有数据库兜底学习记录模板实验主题幂等防重复提交 当前问题重复点击提交订单会创建多笔订单 问题写法直接插入 Orders 推荐写法Idempotency-Key 唯一约束 保存首次响应 观察点同一个 Key 连续请求两次只生成一笔订单 我的理解幂等不是拒绝重复请求而是重复请求得到同一结果且不重复副作用完成判断拿到一个新接口需求时能主动判断它是否需要异常、日志、事务、幂等、并发、鉴权、限流、重试或分布式锁。总结后端工程能力不是堆技术名词而是把接口放到真实环境里思考会不会重复、会不会部分成功、会不会并发冲突、能不能定位、谁能访问、流量大时怎么办、外部服务失败怎么办、多实例部署是否仍然安全。对 .NET 后端来说建议按这个顺序掌握异常处理 - 日志 - 事务 - 幂等 - 并发控制 - 鉴权 - 限流 - 重试 - 分布式锁这条线学明白后再继续学缓存、消息队列、可观测性、部署和可靠性会更容易把各类技术放到正确位置。