1. 请求管道与中间件面试官真正想考察什么1.1 中间件执行顺序为什么是面试必考题中间件在 ASP.NET Core 里本质是一个委托链。形象点说你可以把管道想象成一条流水线HttpContext 是一个不断被传递的工作对象每个中间件是一个工位。一个工位要么自己处理完直接产出短路返回要么调用 next 把工作交给下一个工位。这听起来简单但面试时最常暴露的一个问题是候选人能把 Use、Map、Run 背得滚瓜烂熟却讲不清为什么执行顺序会成为线上事故的根源。如果让我来出题我会先给一个场景一个请求进来需要记录耗时、处理异常、校验访问令牌这三个中间件应该按什么顺序注册普通人往往先答“异常最外层”然后卡住。其实背后逻辑是异常中间件必须最先注册它包住后面所有委托才能 catch 到管道中任何位置抛出的异常耗时中间件次之这样计时范围涵盖了后续全部逻辑令牌校验要放在耗时中间件内部、业务处理之前这样校验不通过的请求也会被计时且不会影响后续业务。如果不按这个顺序比如把异常中间件放到最后前面的中间件抛异常就只能被框架默认处理无法统一返回友好错误。面试官接着通常会追问一个概念管道是洋葱模型请求从外层进入响应从内层退出。实际沿着 Use 的注册顺序执行但响应阶段是逆序返回。这意味着你在 next 前写的代码先执行在 next 后写的代码在响应返回时才执行。我要提醒准备面试的朋友不要只记住“先注册先执行”要能举出“响应日志”的例子下一个中间件返回后当前中间件在 await next() 之后打印的信息记录的是响应阶段的状态码和耗时而不是请求阶段。这个细节能答出来说明你真的调试过请求管道而不是背答案。1.2 自定义中间件的正确姿势与常见误区自定义中间件有三种常见写法直接用 lambda 写在 app.Use 中、定义类式中间件通过 app.UseMiddleware 注册、自己封装工厂方法动态创建。很多候选人只会第一种遇到需要在中间件里使用依赖注入的场景就卡住。类式中间件通常在构造函数里接收 RequestDelegate next也可以注入一些 Singleton 或 Transient 服务但要注意构造函数注入的服务生命周期不能是 Scoped因为中间件是启动时创建的单例对象从根容器解析 Scoped 会报错。如果要注入 Scoped 服务正确做法是在 InvokeAsync 方法的参数里声明它。中间件的 Invoke 方法由容器动态调用支持按请求作用域解析服务所以这样既能使用 DbContext 或业务服务又不会出现捕捉作用域的问题。我见过很多候选人不知道这一点在构造函数里注入了 IRepository 就再也跑不起来。实际上 ASP.NET Core 内置的一些中间件例如 UseDbContext采用的就是这种参数注入模式。还有一个常见误区只记得 try-catch 包裹 next却不考虑响应头是否已经发出。如果后续中间件已经写入了响应体前面的 catch 再去修改状态码就会抛异常。正确做法是先判断 response.HasStarted如果还没开始可以安全地改写状态码和响应体如果已经开始则只能选择终止连接或者记录日志。另外需要读取响应体的时候一定要做缓冲——用 MemoryStream 替换 Response.Body处理完再把内容拷贝回去否则日志工具无法看到响应内容还会导致大响应占额外内存。面试时主动讲到这几个点面试官基本就会把你归到“有实战经验”那类。1.3 分支管道与短路Map/UseWhen/When 的适用边界Map、MapWhen、UseWhen 这几个 API 经常被混为一谈。Map 是按路径前缀直接分叉进入分支后原管道后续的中间件不会再执行而且分支里可以继续注册自己的中间件。MapWhen 和 Map 的区别在于它不只看路径前缀而是接受一个自定义判断委托比如按 Header、QueryString、Host 来分叉。UseWhen 则完全不同它只是条件性进入分支管道分支执行完之后还会回到主管道继续往下走相当于在主管道里临时插入了一个子流程。面试题“如何让 /health 端点不受业务认证中间件影响”答案思路是 Map 一个健康检查分支把需要跳过的中间件注册在分支之前、或者用分支替换整体管道。使用 UseWhen 就不合适因为它分支执行后还会回到主管道令牌校验依然会被执行。另一个常见场景是静态文件加速希望通过 UseWhen 先检查文件是否存在存在就直接短路不存在再走后续管道处理。这种用 UseWhen 合理。有一个细节很多人不清楚Map 创建分支管道时并不是从空管道开始而是继承了调用 Map 之前已经注册的中间件状态。因为 Map 本质上是在当前管道里插入一个分支节点分支内部后续处理逻辑是新的中间件集合但之前的中间件已经包裹在外层。理解这一点可以解释为什么分支里异常还是会被外层异常中间件捕获。面试答到这个层面说明你不是只照着文档敲过代码。2. 依赖注入生命周期选错线上事故离你不远2.1 Singleton、Scoped、Transient 背后的容器原理ASP.NET Core 依赖注入容器的核心职责是管理对象的创建和释放三个生命周期不是随便定的它们对应不同的复用语义。Singleton 是在根容器里只创建一个实例所有地方共享Scoped 是在每个作用域内创建一个实例不同作用域各自独立Transient 则每次解析都创建一个新实例。默认情况下每个 HTTP 请求会创建一个 IServiceScope所以注册为 Scoped 的服务在同一个请求内始终是同一个实例跨请求则是不同实例。面试中考得最多的一个问题是“Singleton 能不能注入 Scoped”。答案是不能因为单例服务是从根容器创建的如果它依赖了一个 Scoped 服务容器会尝试从根容器解析这个 Scoped从而把它提升为单例造成所谓“捕获作用域”。一旦被捕获这个 Scoped 就失去了按请求隔离的语义多个请求会共用同一个实例导致数据串台或线程安全问题。反过来Scoped 可以注入 SingletonSingleton 可以注入 TransientTransient 不要注入 Scoped。简单的记忆方式是依赖的生命周期不能比被依赖方更短。更细一层容器创建对象时会自动处理构造函数的依赖链。如果构造函数里同时存在值类型、字符串、接口值类型和字符串必须有明确注册否则会在启动或请求时抛异常。面试官如果问“容器都做了哪些事情”可以把“依赖图的构建、实例的缓存、IDisposable 的自动释放”三个点都答全。特别提到释放因为很多人不知道容器默认会跟踪它创建的所有 IDisposable 对象并在作用域结束或容器释放时逐一调用 Dispose但如果对象不是容器创建的容器不会负责释放。2.2 捕获依赖与作用域泄漏的经典陷阱实际开发中最容易踩的坑是在后台任务里使用 Scoped 服务。很多同事在写 BackgroundService 时把某个 Scoped 的仓储服务直接注入到单例后台服务的构造函数中看起来启动没问题一旦调用就会抛异常Cannot resolve scoped service from root provider。更隐蔽的情况是通过 ScopeFactory.CreateScope() 创建了作用域但忘记释放导致 DbContext 实例不被释放底层连接也不会归还连接池时间一长连接池就被耗尽。正确做法是用 IServiceScopeFactory 每次创建独立的 scope在 scope 内解析需要的 Scoped 服务处理完立即释放。通常我们会在 BackgroundService 的主循环里使用 try-finally 或 using 块来确保 scope 被释放。我印象很深的一个线上问题某个订单扫描服务把 ApplicationDbContext 注册成了 Singleton结果多个 worker 线程并发操作同一个上下文时不时抛出“DbContext 不是线程安全”的异常。后来查依赖图才发现某个人为了图省事把 DbContext 直接注入了单例服务。这个问题的排查思路也可以用来说给面试官听查看启动日志中容器构建是否报错、检查服务注册的类型、画出依赖链重点看有没有从根容器解析 Scoped 的情况。如果候选人能主动提到“Captive Dependency”这个概念以及在 OWIN/Kestrel 时代和现在的差异面试官大概率会觉得你踩过坑也总结过。2.3 面试加分项验证范围与容器销毁顺序ASP.NET Core 默认只在开发环境开启 DI 验证生产环境默认不会在 Build 时报错很多生命周期问题要到运行期才炸。实际上 Host Builder 提供 ConfigureServices 时我们可以在 serviceCollection 上配置 ValidateOnBuild 和 ValidateScopes。开发环境下框架已经默认开启但最好在 Program 里显式写入并把它作为团队规范固定下来。再往深了讲容器销毁顺序也值得掌握。当 scope 结束时容器会按创建顺序的反序释放它创建的对象这意味着后创建的对象先释放。假设 A 依赖 B 和 C容器创建 A 时先创建 B 再创建 C 最后创建 A释放时先 A再 C再 B。如果 B 的 Dispose 中要使用 A 的资源就会出问题。这个顺序问题不常见但在日志组件、消息消费者、DbContext 组合场景里会翻车。另一个面试加分项是手动作用域的生命周期控制。比如在使用 EF Core 的 Disconnected 场景为了确保上下文在同一个请求内复用可以在管道中手动创建 scope。如果在中间件里 new 了一个 service scope 却交给其他组件释放这段生命周期变得不可控。因此最佳实践是谁创建谁释放作用域必须显式 using。能把这些细节讲清楚不仅面试加分线上排查能力也会提升不少。3. 配置体系从 appsettings 到强类型配置的演进3.1 配置源与优先级为什么说配置是分层的ASP.NET Core 的配置系统是一个可插拔 provider 集合。默认构建器会依次加载 appsettings.json、appsettings.{Environment}.json、环境变量、命令行参数用户机密在开发环境也会参与。后面的 provider 会覆盖前面的 provider这就是为什么生产连接字符串通过环境变量设置之后会覆盖文件里的默认值。理解这一点比背顺序强因为只要你需要自己实现配置源就必须明白 provider 的加载顺序决定了最终值。很多候选人不知道 JSON 配置的 key 是大小写不敏感的而环境变量映射时使用双下划线作为层级分隔符。例如 ConnectionStrings__DefaultConnection 会被解析成 ConnectionStrings:DefaultConnection。在 Linux 部署时环境变量的名称规则和 Windows 不同大小写约定也可能不一致所以尽量用双下划线格式并避免特殊字符。面试官经常从“怎么在不同环境使用不同连接字符串”切入能答出环境变量优先级和 provider 顺序的候选人通常是真的部署过项目的。另一个容易忽略的点是IConfiguration 不是强类型。直接通过 configuration[Section:Key] 拿值需要处理类型转换、默认值和空字符串。更推荐把配置绑定到强类型 POCO做法是 services.Configure (configuration.GetSection(SectionName))然后在构造函数里注入 IOptions 等接口读取。这样既可以享受配置热更新又避免了到处都是字符串 key 的魔法值重构时编译器也能帮上忙。3.2 IOptions、IOptionsSnapshot、IOptionsMonitor 该怎么选这三个选项接口是配置绑定之后读取数据的关键入口面试几乎必问。IOptions 是 Singleton 形式容器第一次解析后缓存实例之后配置变化不会自动更新。IOptionsSnapshot 是 Scoped 形式每个作用域第一次解析时从 IConfiguration 重新读取所以同一个请求内保持一致不同请求可能拿到新值。IOptionsMonitor 是 Singleton 形式但内部监听了配置 reload 事件配置变化时重新构造 T并且可以通过 OnChange 注册回调。在实际项目里普通 Web 请求中用 IOptionsSnapshot 最顺手因为每个请求都是一次新的作用域拿到的配置和当前配置一致又不会创建过多对象。如果是后台长时间运行的服务比如消息消费者或者 WorkerIOptionsSnapshot 就不合适因为它的作用域可能长时间不释放永远拿不到更新值。这时候应该用 IOptionsMonitor或者通过监听配置变更事件主动刷新自己的字段。我记得自己踩过一个坑做一个灰度开关服务刚开始在单例的配置服务里注入 IOptionsSnapshot 结果所有开关都只在进程启动时生效上线后改配置不生效排查半天才意识到后台任务的作用域不随 HTTP 请求变化。换成 IOptionsMonitor 之后开关功能一切正常。面试时如果能把这个“后台任务用错接口导致配置不更新”的经历讲出来比单纯背概念有说服力得多。3.3 敏感数据保护与配置热更新实践连接字符串、API Key、证书密钥这些东西放在 appsettings.json 里提交到代码库是个坏习惯。开发环境可以使用 dotnet user-secrets 把敏感配置放在用户目录生产环境用环境变量、密钥管理平台、或者容器编排的 secret 对象。ASP.NET Core 的配置源设计允许我们扩展自定义 provider所以企业内部可以写一个从远程配置中心拉取配置的 provider把它加在默认 provider 之后实现中心化配置管理。热更新默认由 FileConfigurationProvider 通过 reloadOnChange 支持。appsettings.json 变化时配置系统会重新加载文件并触发 ChangeToken。但配置绑定的对象不会自动重建除非你使用 IOptionsMonitor 或 IOptionsSnapshot。如果自己写配置中心一般做法是后台轮询远端接口获取最新配置写入 ConcurrentDictionary再触发一次 reload让所有订阅了 ChangeToken 的组件重新读取。这个机制理解之后面试官问“配置中心怎么实现”你就可以从 IConfigurationBuilder、ConfigurationProvider、ChangeToken 三个角度展开而不是只会说“连数据库读配置”。同时要提示一点不要把所有配置都热更新像数据库连接字符串这种如果被频繁换掉可能导致连接池不稳定。通常做法是配置内容分为“静态启动项”和“动态变更项”动态项走配置中心静态项保留在环境变量或启动参数中。做架构设计时提前区分好能减少很多上线事故。4. 认证与授权从 Cookie 到 JWT 的完整心智模型4.1 认证与授权分离的设计逻辑认证解决“你是谁”授权解决“你能做什么”。ASP.NET Core 中 AuthenticationMiddleware 在请求管道中把凭据解析成 ClaimsPrincipal 并放到 HttpContext.UserAuthorizationMiddleware 再通过授权策略判断当前用户是否满足条件。两个中间件是独立的授权需要认证但认证不一定要授权。这个分离很多人讲不清甚至有人以为加了 [Authorize] 就是认证其实它管的是授权认证方案是另一个注册配置。AddAuthentication 和 AddAuthorization 是两个独立的扩展方法。AddAuthentication 注册认证方案的 handler例如 CookieHandler、JwtBearerHandlerAddAuthorization 注册授权策略和 handler。请求进入时认证中间件会根据既定方案尝试认证如果失败不会直接拒绝而是让 User 保持未认证状态授权中间件到了之后发现未认证才会引导跳转或返回 401。这个机制意味着如果你想实现“某些接口允许匿名某些必须登录”只需要在控制器或端点上加 [AllowAnonymous] 或 [Authorize]不需要写大量分支代码。面试高频追问是“多个认证方案并存怎么处理”。答案是可以为 [Authorize] 传 AuthenticationSchemes 属性或者在 AddAuthentication 里设置 DefaultAuthenticateScheme也可以在策略里指定。如果同一个接口既支持 JWT 又支持 Cookie需要明确指定方案否则授权中间件只会用默认方案去认证导致一种方案永远无法通过。能讲到这里说明你不是只会按模板搭一个 Swagger 加 JWT 的 Demo。4.2 JWT 与 Cookie 方案的适用场景对比JWT 适合无状态 API、移动端、跨域调用Cookie 适合传统浏览器 MVC 应用。很多人默认 JWT 更现代更安全其实要看风险模型。JWT 的 token 一旦发到前端常见做法是存在 localStorage 或内存中如果 XSS 攻击者能执行脚本就可以偷走 token 模拟请求。Cookie 配合 HttpOnly、SameSite、Secure 属性浏览器会限制 JavaScript 读取 Cookie从而降低这类风险。另一方面Cookie Session 由服务端保存会话状态可以随时登出失效JWT 的 token 本身难以做到服务端主动失效。但这不代表 JWT 不能用。在无状态 API 场景JWT 避免了下游服务每次都要查询 Session 存储适合水平扩展。真正要解决的是泄露与失效问题通常引入短期 accessToken 加长期 refreshToken服务端保存 refreshToken 的白名单或黑名单实现可控失效。面试时如果问“JWT 泄露了怎么处理”不要只说“等它过期”要提出刷新令牌轮换、异常检测、设备指纹、黑名单机制等组合方案。我一般建议的原则是面向浏览器的应用优先 Cookie纯前后端分离且对 XSS 防护有信心的团队可以用 JWT但必须做好刷新和黑名单。面试官往往不是要一个标准答案而是看你会不会针对不同场景取舍。你把 Cookie Secure 和 HttpOnly 的特性讲清楚再把 JWT 的无状态特点讲清楚就是一个很好的对比式回答。4.3 基于策略与自定义授权过滤器的常见扩展角色授权最简单但有时不够用。比如管理员和运营都算“可编辑”但运营不能删除或者权限需要按租户区分同一角色的人在不同租户下拥有不同的权限范围。这时候就需要自定义授权策略。ASP.NET Core 提供 IAuthorizationRequirement 和 IAuthorizationHandler可以定义 HasPermissionRequirement并实现一个 handler 根据用户的 Claim 或数据库中的权限集合来判断。策略注册时可以把 requirements 组合起来多个 requirement 之间是 AND 关系都满足才通过。面试中如果被问“按钮级权限怎么做”不要陷入过滤器或中间件的思路。最正统的方式是授权策略前端根据接口返回的权限码决定显示哪些按钮后端通过 [Authorize(Policy CanEditOrder)] 校验按钮对应的行为是否被允许。如果需要在多个地方复用权限逻辑策略模式比在每个控制器里手写 if 判断要干净得多。同时要把授权过滤器与授权中间件的区别讲清楚AuthorizationFilter 运行在 MVC 管道中是对已经通过授权中间件的用户做更面向资源的校验属于应用层逻辑而授权中间件是做全局策略判断。一个加分细节是 IAuthorizationPolicyProvider 的用途。默认 policy 只能通过名称查找但如果你有几千个权限点把每个权限点都注册成 policy 不现实。可以通过自定义 policy provider把“权限名:操作”字符串动态解析成 requirement。我见过有团队这么做后扩展新权限点只需要改权限名不用动 Startup。这个设计非常值得在面试里展开。5. 异步编程与性能攻坚Task、线程池与连接池5.1 async/await 只是语法糖面试官追问的底层原理说 async/await 是语法糖没有错但没有回答本质。它编译后是一个异步状态机编译器会把方法拆成若干状态每个 await 是一个挂起点。当 await 的操作完成后状态机会在合适的线程上继续执行。这个模型保证了不阻塞调用线程但对于 I/O 操作来说真正快不是因为谁同时处理多个操作而是因为线程在等待 I/O 时可以去做别的请求。所以异步的本质是资源释放不是并行。对 Web 服务来说把控制器方法写成 async 能显著提高吞吐因为线程池线程不需要在数据库查询期间被白白占着。但 CPU 密集型操作使用 async 没有收益反而增加状态机开销。这时候可以用 Task.Run 把它丢到线程池但也不需要每个 CPU 任务都包一层因为请求本身就由线程池线程处理直接同步执行也占用线程。面试官如果问“你知道线程池饥饿吗”可以这样答线程池线程处理请求时如果同步阻塞等待异步操作的完成回调却又要从线程池中取出新线程才能继续线程池为了快速补充线程会短暂注入新线程高并发下会导致吞吐骤降和延迟上升。在代码里一个常见的反模式是 async void 事件处理器只用于 UI 事件不要用于后端服务。另一个是使用 .Result 或 .Wait() 阻塞并等待异步方法这不但浪费线程还可能死锁。回答时要强调从控制器到数据库访问整个调用链尽量保持 async避免任何 Sync-over-Async 的转折。5.2 阻塞与死锁ConfigureAwait 什么时候必须用很多人一说异步死锁就提 ConfigureAwait(false)但在 ASP.NET Core 请求管道中默认没有传统的 SynchronizationContext所以不写也不会因上下文缺失而死锁。实际上是桌面应用里因为有 UI SynchronizationContext.Result 会导致死锁。如果类库需要被 WinForms、WPF、或旧 ASP.NET Framework 使用必须留意捕获上下文的行为用 ConfigureAwait(false) 可以让后续代码不需要回到特定上下文。面试官经常会设置一个陷阱让你讲 ConfigureAwait(false) 在 ASP.NET Core 里是不是必须。如果回答“所有 await 后面都要加”说明你没理解机制。正确回答是先说上下文捕获的基本原理再区分平台ASP.NET Core 请求中不必加类库为了健壮可以考虑但加了之后反而会影响需要 HttpContext 的场景。这个细致区分会让面试官刮目相看。还有一个相关概念是 TaskCompletionSource 和 ValueTask。ValueTask 能减少无等待完成时的分配适合高性能路径但约束是不能多次 await。如果候选人能自动提到 ValueTask 的使用场景通常说明对异步性能比较敏感。不过面试中不必展开太深把核心模型讲对最重要。5.3 性能排查从 ThreadPool 饥饿到连接池耗尽线上高并发时请求大量超时往往不一定是代码逻辑慢而可能是线程池或连接池被耗尽。ThreadPool 饥饿的表现是 CPU 不高但请求延迟越来越长因为大量线程阻塞等待异步完成而完成回调又需要线程池分配线程。检查时可以看 ThreadPool 队列长度、线程注入速率以及有没有大量同步阻塞调用。缓解方式包括避免 .Result、合理调大最小线程数但根本还是依赖代码保持全异步。连接池耗尽在数据库或 Redis 场景也很常见。如果一个操作获取连接后没有及时关闭池内连接被耗尽后续操作就会排队或超时。排查时看活动连接数和池上限重点关注有没有未释放的 DbContext、Connection 或 Scope。连接字符串里 Max Pool Size 可以调但不能掩盖泄漏问题。我自己的排查套路是先拉取性能计数器看 CPU、线程数、内存、连接数再抓取两个不同时间点的 dump 对比线程栈找出大量阻塞在哪个调用上最后根据栈信息回到代码定位。面试时不必把工具说得太细但要展示这种“先看指标再抓证据”的思维。如果还能说出“用 dotnet-counters 看 ThreadPool Queue Length用 dotnet-dump 分析阻塞线程”就更有实战感。6. 缓存与分布式场景本地缓存、分布式缓存怎么选6.1 IMemoryCache 的过期策略与缓存穿透IMemoryCache 是进程内缓存速度快、延迟低适合高频读取且基本不变的配置数据、字典数据和热点搜索结果。但它有几个众所周知的问题内存有限需要设计合理的过期策略多实例部署时每个实例缓存独立数据可能不一致。面试常问的是绝对过期和滑动过期的区别。绝对过期是到时间就删滑动过期是每次访问后重置过期时间但为了防止极端情况通常两者结合使用设置一个滑动上限。缓存穿透是查询不存在的数据缓存和数据库都没有请求每次都穿透到数据库。解决办法有缓存空值并设置较短过期时间在请求入口做参数校验使用布隆过滤器快速判断数据是否可能存在。缓存击穿是某个热点 key 失效的瞬间大量请求同时打到数据库。可以用锁或 GetOrCreateAsync 的原子操作但要注意 IMemoryCache 的 GetOrCreateAsync 并发时 factory 可能会执行多次不同版本行为不完全一致最好自己加 Double-Check 锁。能把这个细节说出来说明你不仅用过缓存还读过一些源码。缓存雪崩则是指大量 key 同时过期或者缓存节点整体宕机导致大量请求打进数据库。方案包括key 过期时间加随机抖动、多级缓存、服务降级、数据库限流。回答缓存问题时不建议只背三个术语最好结合场景说明取舍。比如某些业务能接受短暂空窗就不需要过度设计。6.2 Redis 分布式缓存实战要点分布式缓存通常用 Redis通过 IDistributedCache 接口集成。StackExchange.Redis 默认会复用连接但要注意连接超时、断线重连、命令超时这些参数。生产环境一般要设置一个合理的 connectTimeout 和 syncTimeout并实现对缓存故障的降级——不能因为 Redis 抖动导致主业务流程失败。Redis 本身是单线程处理命令所以千万别存大对象、大列表避免 O(N) 命令阻塞所有请求。key 的命名要规范最好带业务前缀、版本号比如 order:cache:v1:12345。分布式缓存存储的是字节数组通常会配一个序列化器如果使用 JSON模型字段变更后反序列化可能出错建议在存储对象中加版本字段或者保留旧类型兼容。除了缓存Redis 还常做分布式锁和计数器。在分布式缓存面试题中如果被追问“缓存和数据库一致性怎么保证”可以自然过渡到下面的章节。能区分本地缓存与分布式缓存的适用边界也很重要本地缓存减小网络开销但数据不一致分布式缓存一致性更好但延迟高实际系统往往是两级缓存组合使用把最热的数据放本地其余放 Redis。6.3 缓存一致性更新数据库后缓存怎么失效最常被问到的经典问题先更新数据库还是先删缓存先删缓存后更新数据库读请求可能把旧数据重新写回缓存先更新数据库再删缓存在删除完成前有短暂旧缓存窗口。这两种都会造成短时间不一致只是概率和窗口不同。业界常见的做法是 Cache-Aside读时先查缓存未命中查库回填写时更新成功后删除缓存。对于不一致窗口可以通过延迟双删更新后延迟几百毫秒再删一次缓解。更可靠但更重的方案是订阅数据库变更日志如 binlog异步删除缓存或者使用本地消息表保证最终一致性。这不一定适合所有项目但面试时如果能从“简单的 Cache-Aside”到“延迟双删/消息驱动”分层介绍会让面试官觉得你有完整的知识体系。同时要强调绝对一致性很难做到也不一定需要做到。如果业务要求强一致最简单的方案是不缓存或串行化读写而不是引入复杂的一致性协议。在实践层面我建议在缓存值里塞一个时间戳或版本当数据库更新后缓存删除失败时至少可以通过版本对比拒绝旧数据。面试官如果听说你处理过删除缓存失败的消息重试基本就能确认你有真实项目经验。7. 常见综合连环题与答题框架7.1 “你的项目遇到性能瓶颈怎么排查”这类开放题的答法这是一道综合性很高的开放题没有标准答案面试官要看你的排查思路是否系统。我的答题框架是先确定问题现象和影响范围QPS、延迟、错误率、成功率然后沿着请求链路逐层定位最后针对性优化并回归。不要一上来就说“加缓存、加机器”那是没有分析过程的暴力方案。具体链路可以从入口开始负载均衡、中间件耗时、控制器/业务层、数据库访问、下游调用。每个环节都可能成为瓶颈。通过日志或 trace 把各环节耗时百分比算出来就能确定大头在哪。比如数据库查询占总耗时 80%那就先看索引、执行计划、缓存命中如果是下游调用超时占大头就要看超时配置、重试策略、连接池设置。另一个维度是资源分类CPU 瓶颈、内存瓶颈、IO 瓶颈、锁竞争。CPU 高抓 dump 找热点内存高看 GC 堆大小和对象存活IO 高看磁盘队列和网络带宽锁竞争看线程上下文切换和等待时间。面试时只要能把这些思路分层次说出来再附上一个自己遇到过的简单例子就是不错的表现。7.2 涉及微服务时 ASP.NET Core 的定位与边界在微服务架构里ASP.NET Core 更多是提供服务宿主能力。它通过中间件来承载网关、服务发现、配置刷新、认证、日志追踪等横切关注点。比如每个服务启动时从配置中心拉取配置请求进入时从请求头透传 trace id调用下游时把当前 trace id 注入到 HttpClient。这些功能在 ASP.NET Core 里通常实现为一个中间件或 DelegatingHandler而不是在业务代码里到处复制。很多候选人对“服务间调用要不要走 API 网关”这类问题没有明确观点。我的建议是外部流量进业务集群走网关做统一认证、限流、路由内部服务调用可以直接走服务发现或内部负载均衡避免所有流量都绕经网关增加延迟。ASP.NET Core 提供了内置的健康检查端点、gRPC、SignalR 等多种能力在微服务中分别适合不同场景健康检查用于探针gRPC 适合低延迟高吞吐的内部通信SignalR 适合实时推送。能把这些组件串起来说明你不是只把微服务理解成“拆几个 Web API 项目”。还要知道 ASP.NET Core 配置、DI、日志是跨项目共享的基础设施。微服务团队可以创建公共基础包封装统一的中间件管道和配置约定既保证各服务行为一致又不必重复造轮子。面试时说清楚这一点比单纯背微服务八股更有价值。7.3 高频进阶问题清单速查为了让读者在面试前一天快速过一遍我把本文涉及的高频问题整理成速查表。每一个问题都可以用“结论原理实战案例”三段式回答时间控制在一分钟以内。请求管道的执行顺序、Map/UseWhen/Use 的区别是什么实际项目中如何选择Singleton、Scoped、Transient 的生命周期边界在哪里为什么 Singleton 不能注入 ScopedIOptions、IOptionsSnapshot、IOptionsMonitor 分别适合什么场景后台服务中该怎么选JWT 与 Cookie 的优缺点是什么JWT 如何应对泄露和失效异步方法为什么能提升吞吐.Result 会造成什么后果缓存穿透、击穿、雪崩分别怎么解决本地缓存和分布式缓存怎么搭配更新数据库后缓存怎么保持一致性这些问题的答案在文中都有展开。如果面试官继续深挖就往源码层面走看一下中间件源码里 next 是如何调用的、DefaultServiceProvider 是如何解析生命周期的、认证 handler 的内部流程。把这些源码阅读经验写进博客是我觉得最有效的方式。最后再分享一点我自己的体会面试中遇到没准备过的问题不要慌。ASP.NET Core 的很多设计都围绕着“请求管道、容器、配置、中间件”这几个抽象即使不知道具体 API也可以从这几个抽象推出合理答案。我在准备面试时习惯把每篇文章里的知识点都投影到这几个抽象上形成一个自己能推导的知识网络。你能把这里面的逻辑讲通技术深度自然就体现出来了。