本系列专栏基于杨中科老师的《ASP.NET Core技术内幕与项目实战》本人记录梳理的学习笔记有部分的增补和省略。更全面系统的讲解请看杨老师的视频课【.NET教程.Net Core视频教程杨中科主讲】。一、依赖注入依赖注入DI是ASP.NET Core 框架的核心设计思想之一它解决了代码耦合度高、测试困难、扩展性差等问题是实现 “控制反转IoC” 的主流方式。1. 使用方式ASP.NET Core 框架内置了依赖注入容器无需手动管理容器生命周期核心使用方式分为以下三类1服务注册在Program.cs中builder.Build()执行前容器构建完成前通过builder.Services本质是IServiceCollection注册服务框架会自动管理IServiceProvider服务提供器的创建和销毁。// Program.cs 示例 var builder WebApplication.CreateBuilder(args); // 注册服务指定生命周期瞬时/作用域/单例 builder.Services.AddScopedIBookService, BookService(); // 作用域每次请求创建一个实例 builder.Services.AddTransientILoggerHelper, LoggerHelper(); // 瞬时每次获取创建新实例 builder.Services.AddSingletonICacheHelper, CacheHelper(); // 单例全局唯一实例 var app builder.Build();2控制器构造函数注入推荐这是最常用的注入方式框架会自动识别控制器构造函数中的依赖并注入。public class BookController : Controller { private readonly IBookService _bookService; // 构造函数注入框架自动解析IBookService的实现类 public BookController(IBookService bookService) { _bookService bookService; } [HttpGet] public IActionResult GetBooks() { var books _bookService.GetAll(); return Ok(books); } }3Action 参数注入使用[FromServices]仅适用于单个 Action需要的服务且满足 “调用频率低、实例创建成本高” 的场景如大对象、数据库重型连接。注意[FromServices]仅支持 Action 方法参数普通类 / 方法不支持且与 Action 其他参数如 [FromQuery]、[FromBody]不冲突。[HttpGet({id})] public IActionResult GetBookById( int id, [FromServices] IHeavyDataService heavyDataService) // 仅当前Action使用 { var book heavyDataService.GetBookById(id); return Ok(book); }2. 模块化的服务注册框架当项目规模扩大多模块 / 多项目时分散在Program.cs的服务注册会变得混乱杨老师编写的Zack.Commons通过模块化思想解决这一问题。1使用步骤安装 NuGet 包Install-Package Zack.Commons # 或使用CLI dotnet add package Zack.Commons每个模块 / 项目中创建实现IModuleInitializer的类封装当前模块的服务注册逻辑// 图书模块的注册类BookModule/ModuleInitializer.cs public class BookModuleInitializer : IModuleInitializer { public void Initialize(IServiceCollection services) { // 仅注册图书模块相关服务职责单一 services.AddScopedIBookService, BookService(); services.AddScopedIBookRepository, BookRepository(); } } // 订单模块的注册类OrderModule/ModuleInitializer.cs public class OrderModuleInitializer : IModuleInitializer { public void Initialize(IServiceCollection services) { services.AddScopedIOrderService, OrderService(); } }全局统一初始化所有模块// Program.cs var builder WebApplication.CreateBuilder(args); // 获取所有引用的程序集自动扫描IModuleInitializer实现类 var assemblies ReflectionHelper.GetAllReferencedAssemblies(); // 执行所有模块的初始化方法自动注册服务 builder.Services.RunModuleInitializers(assemblies); var app builder.Build();2核心原理反射ReflectionHelper.GetAllReferencedAssemblies()会遍历当前应用所有引用的程序集通过反射查找所有实现了IModuleInitializer接口的类扩展方法RunModuleInitializers是IServiceCollection的扩展方法内部会创建这些实现类的实例并调用Initialize方法完成模块化的服务注册优势将服务注册逻辑分散到各模块符合 “单一职责” 原则降低Program.cs的复杂度便于团队协作和维护。二、缓存缓存是系统性能优化的 “低成本高收益” 手段核心思想是将高频访问、计算 / 查询成本高的数据暂存到快速存储介质中减少重复计算 / 数据库查询降低服务器压力。数据库索引、浏览器缓存本质上都是缓存思想的体现。1. 客户端缓存ResponseCacheAttribute通过[ResponseCache]特性控制 HTTP 响应头的cache-control字段指导浏览器 / 客户端缓存响应内容减少重复请求。// 示例客户端缓存60秒仅GET/HEAD请求生效 [HttpGet] [ResponseCache(Duration 60, Location ResponseCacheLocation.Client)] public IActionResult GetBooks() { var books _bookService.GetAll(); return Ok(books); }核心参数Duration缓存时长秒对应cache-control: max-age60Location缓存位置Client/Any/NeverNoStore是否禁止缓存true 时cache-control: no-store。2. 服务器端响应缓存ResponseCaching 中间件客户端缓存依赖浏览器遵守规范服务器端缓存则是将响应内容暂存到服务器内存对相同请求直接返回缓存内容进一步降低服务器处理压力。1使用步骤注册服务ASP.NET Core 3.x 默认已包含无需额外安装// Program.cs builder.Services.AddResponseCaching();启用中间件注意顺序UseCors需在UseResponseCaching之前var app builder.Build(); app.UseCors(); // 跨域中间件在前 app.UseResponseCaching(); // 响应缓存中间件 app.MapControllers();结合[ResponseCache]特性使用[HttpGet] // Duration0时服务器客户端均缓存60秒 [ResponseCache(Duration 60)] public IActionResult GetBooks() { var books _bookService.GetAll(); return Ok(books); }2服务器端缓存限制仅缓存状态码为 200 的GET/HEAD请求响应头包含Authorization、Set-Cookie、Vary: *等时不缓存大部分浏览器遵守 RFC7324 规范测试时可通过 Postman 禁用客户端缓存验证服务器端缓存效果。3. 内存缓存内存缓存IMemoryCache接口是ASP.NET Core 最常用的缓存方式将数据存储在应用程序进程内存中读写速度极快但数据与进程绑定程序重启 / 回收则缓存丢失且多站点 / 多进程间不共享。1启用内存缓存// Program.cs builder.Services.AddMemoryCache(); // 注册内存缓存服务2核心接口方法 / 属性作用TryGetValue(string key, out T value)尝试获取缓存返回是否存在Remove(string key)删除指定缓存项Set(string key, T value, CacheEntryOptions options)设置缓存指定过期策略GetOrCreate/GetOrCreateAsync核心方法获取缓存不存在则执行工厂方法创建并缓存3核心用法GetOrCreateAsync有则获取没有则创建。public class BookService { private readonly IMemoryCache _memCache; private readonly AppDbContext _dbCtx; private readonly ILoggerBookService _logger; public BookService(IMemoryCache memCache, AppDbContext dbCtx, ILoggerBookService logger) { _memCache memCache; _dbCtx dbCtx; _logger logger; } public async TaskBook[] GetBooksAsync() { _logger.LogInformation(开始执行GetBooksAsync); // 核心逻辑缓存键为AllBooks不存在则从数据库查询并缓存 var books await _memCache.GetOrCreateAsync(AllBooks, async (cacheEntry) { _logger.LogInformation(缓存未命中从数据库读取数据); // 设置缓存过期策略可选 cacheEntry.AbsoluteExpirationRelativeToNow TimeSpan.FromMinutes(5); // 绝对过期5分钟 return await _dbCtx.Books.ToArrayAsync(); }); _logger.LogInformation(返回数据给调用者); return books; } }执行逻辑第一次调用缓存未命中 → 执行工厂方法数据库查询→ 结果存入缓存 → 返回数据后续调用缓存命中 → 直接返回缓存数据跳过数据库查询。4. 缓存过期策略内存缓存默认永久有效直到程序重启需通过过期策略解决 “缓存数据不一致” 问题核心分为两类1绝对过期时间AbsoluteExpiration缓存项在指定时间点 / 时间段后强制过期适用于 “数据更新频率固定” 的场景如每日更新的榜单。var books await _memCache.GetOrCreateAsync(AllBooks, async (cacheEntry) { // 绝对过期5分钟后强制失效 cacheEntry.AbsoluteExpirationRelativeToNow TimeSpan.FromMinutes(5); return await _dbCtx.Books.ToArrayAsync(); });2滑动过期时间SlidingExpiration缓存项在最后一次访问后若超过指定时间未被访问则过期适用于 “访问频率不稳定” 的场景如用户个人数据。var books await _memCache.GetOrCreateAsync(AllBooks, async (cacheEntry) { // 滑动过期3分钟内未访问则失效 cacheEntry.SlidingExpiration TimeSpan.FromMinutes(3); return await _dbCtx.Books.ToArrayAsync(); });3混合过期推荐结合绝对过期 滑动过期既避免缓存长期不失效滑动过期又避免缓存频繁失效绝对过期cacheEntry.SlidingExpiration TimeSpan.FromMinutes(3); // 3分钟未访问则过期 cacheEntry.AbsoluteExpirationRelativeToNow TimeSpan.FromHours(1); // 最多缓存1小时5. 缓存常见问题及解决方案1缓存穿透问题恶意请求不存在的 Key如查询 ID-1 的图书缓存未命中后持续穿透到数据库导致数据库压力过大。解决方案将 “查询结果为空” 也存入缓存设置短过期时间避免每次都查询数据库。public async TaskBook? GetBookByIdAsync(int id) { var cacheKey $Book_{id}; var book await _memCache.GetOrCreateAsync(cacheKey, async (cacheEntry) { // 空结果也缓存设置短过期时间如1分钟 cacheEntry.AbsoluteExpirationRelativeToNow TimeSpan.FromMinutes(1); _logger.LogInformation(查询数据库Book_{0}, id); return await _dbCtx.Books.FindAsync(id); // 可能返回null }); return book; }关键GetOrCreateAsync会将null视为合法值存入缓存避免重复查询不存在的数据。2缓存雪崩问题大量缓存项在同一时间过期导致大量请求瞬间穿透到数据库引发数据库压力骤增甚至宕机。解决方案为缓存项设置随机过期时间核心在基础过期时间上增加随机偏移如 5±1 分钟避免批量过期分级缓存内存缓存 分布式缓存熔断 / 限流如使用 Polly 限制数据库请求量。示例随机过期时间var random new Random(); cacheEntry.AbsoluteExpirationRelativeToNow TimeSpan.FromMinutes(5 random.Next(-1, 2)); // 4-7分钟3缓存数据一致性问题缓存数据与数据库数据不一致如更新了图书信息但缓存未同步。解决方案主动更新 / 删除缓存数据修改后立即调用_memCache.Remove(cacheKey)或重新设置缓存短过期时间对一致性要求不高的场景如博客设置短过期时间降低不一致窗口事件通知分布式系统中通过消息队列如 RabbitMQ通知所有节点更新缓存。6. 内存缓存帮助类直接使用IMemoryCache可能存在以下问题重复编写过期时间、随机偏移等逻辑缓存IQueryable/IEnumerable可能触发延迟加载EF Core 上下文已释放导致查询失败未统一处理缓存穿透 / 雪崩问题。因此需要通过存缓存帮助类进行封装。1接口定义/// summary /// 内存缓存帮助类接口封装通用逻辑 /// /summary public interface IMemoryCacheHelper { /// summary /// 获取/创建缓存同步 /// /summary /// typeparam nameTResult缓存类型禁止IQueryable/IEnumerable/typeparam /// param namecacheKey缓存键/param /// param namevalueFactory缓存工厂方法/param /// param nameexpireSeconds基础过期秒数会添加随机偏移/param /// returns缓存值/returns TResult? GetOrCreateTResult(string cacheKey, FuncICacheEntry, TResult? valueFactory, int expireSeconds) where TResult : class; /// summary /// 获取/创建缓存异步 /// /summary TaskTResult? GetOrCreateAsyncTResult(string cacheKey, FuncICacheEntry, TaskTResult? valueFactory, int expireSeconds) where TResult : class; /// summary /// 删除缓存 /// /summary void Remove(string cacheKey); }2实现类public class MemoryCacheHelper : IMemoryCacheHelper { private readonly IMemoryCache _memoryCache; private readonly Random _random; public MemoryCacheHelper(IMemoryCache memoryCache) { _memoryCache memoryCache; _random new Random(); } public TResult? GetOrCreateTResult(string cacheKey, FuncICacheEntry, TResult? valueFactory, int expireSeconds) where TResult : class { return _memoryCache.GetOrCreate(cacheKey, entry { // 设置随机过期时间解决缓存雪崩 var randomOffset _random.Next(-expireSeconds / 10, expireSeconds / 10 1); // 10%偏移 entry.AbsoluteExpirationRelativeToNow TimeSpan.FromSeconds(expireSeconds randomOffset); // 禁止缓存IQueryable/IEnumerable延迟加载风险 var result valueFactory.Invoke(entry); if (result is IQueryable || result is IEnumerable !(result is string)) { throw new InvalidOperationException(禁止缓存IQueryable/IEnumerable类型请先转换为数组/列表); } return result; }); } public async TaskTResult? GetOrCreateAsyncTResult(string cacheKey, FuncICacheEntry, TaskTResult? valueFactory, int expireSeconds) where TResult : class { return await _memoryCache.GetOrCreateAsync(cacheKey, async entry { var randomOffset _random.Next(-expireSeconds / 10, expireSeconds / 10 1); entry.AbsoluteExpirationRelativeToNow TimeSpan.FromSeconds(expireSeconds randomOffset); var result await valueFactory.Invoke(entry); if (result is IQueryable || result is IEnumerable !(result is string)) { throw new InvalidOperationException(禁止缓存IQueryable/IEnumerable类型请先转换为数组/列表); } return result; }); } public void Remove(string cacheKey) { _memoryCache.Remove(cacheKey); } }3注册与使用// Program.cs 注册 builder.Services.AddScopedIMemoryCacheHelper, MemoryCacheHelper(); // 业务层使用 public async TaskBook[] GetAllBooksAsync() { return await _cacheHelper.GetOrCreateAsync( AllBooks, async (entry) await _dbCtx.Books.ToArrayAsync(), 300); // 基础过期5分钟实际4.5-5.5分钟 }7. 分布式缓存内存缓存的局限仅当前进程可用多服务器部署时缓存不共享且进程重启后丢失。分布式缓存将数据存储在独立的缓存服务器如 Redis、Memcached所有应用节点共享缓存数据适合分布式 / 集群部署场景。1主流分布式缓存对比缓存服务器优点缺点适用场景SQLServer部署简单、无需额外组件性能差、不适合高频访问小型项目、临时测试Memcached纯缓存设计、性能极高集群 / 高可用弱、键长度限制250 字节高并发读、简单缓存场景Redis功能丰富缓存 / 消息 / 持久化、高可用 / 集群成熟性能略低于 Memcached分布式系统、高可用要求高的场景2Redis 分布式缓存使用推荐安装 NuGet 包Install-Package Microsoft.Extensions.Caching.StackExchangeRedis注册服务Program.csbuilder.Services.AddStackExchangeRedisCache(options { options.Configuration localhost:6379; // Redis连接字符串可配置密码、数据库等 options.InstanceName BookApp_; // 缓存键前缀避免多应用冲突 });核心接口IDistributedCache方法作用Get/Set同步获取 / 设置缓存字节数组GetAsync/SetAsync异步获取 / 设置缓存RefreshAsync刷新缓存重置滑动过期RemoveAsync删除缓存注意IDistributedCache默认操作byte[]类型需手动序列化 / 反序列化如 JSON。3分布式缓存帮助类进行封装序列化 / 反序列化。/// summary /// 分布式缓存帮助类自动序列化/反序列化 /// /summary public interface IDistributedCacheHelper { TaskTResult? GetOrCreateAsyncTResult(string cacheKey, FuncDistributedCacheEntryOptions, TaskTResult? valueFactory, int expireSeconds) where TResult : class; Task RemoveAsync(string cacheKey); } // 实现类 public class DistributedCacheHelper : IDistributedCacheHelper { private readonly IDistributedCache _distributedCache; private readonly Random _random; private readonly JsonSerializerOptions _jsonOptions new JsonSerializerOptions { PropertyNameCaseInsensitive true }; public DistributedCacheHelper(IDistributedCache distributedCache) { _distributedCache distributedCache; _random new Random(); } public async TaskTResult? GetOrCreateAsyncTResult(string cacheKey, FuncDistributedCacheEntryOptions, TaskTResult? valueFactory, int expireSeconds) where TResult : class { // 1. 尝试获取缓存 var cacheValue await _distributedCache.GetStringAsync(cacheKey); if (!string.IsNullOrEmpty(cacheValue)) { return JsonSerializer.DeserializeTResult(cacheValue, _jsonOptions); } // 2. 缓存未命中执行工厂方法 var options new DistributedCacheEntryOptions(); var randomOffset _random.Next(-expireSeconds / 10, expireSeconds / 10 1); options.AbsoluteExpirationRelativeToNow TimeSpan.FromSeconds(expireSeconds randomOffset); var result await valueFactory.Invoke(options); // 3. 存入缓存空结果也缓存解决穿透 if (result ! null) { cacheValue JsonSerializer.Serialize(result, _jsonOptions); await _distributedCache.SetStringAsync(cacheKey, cacheValue, options); } else { // 空结果缓存短时间如1分钟 await _distributedCache.SetStringAsync(cacheKey, , new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow TimeSpan.FromMinutes(1) }); } return result; } public async Task RemoveAsync(string cacheKey) { await _distributedCache.RemoveAsync(cacheKey); } }总结依赖注入 Core 内置 DI 容器核心注册方式为builder.Services.AddXxx注入方式优先选择构造函数注入[FromServices]仅用于特殊场景Zack.Commons 通过 “反射 模块化” 实现服务注册的解耦适合大型项目的模块化开发。缓存 缓存分类客户端缓存浏览器→ 服务器端响应缓存HTTP 响应→ 内存缓存进程内→ 分布式缓存跨进程 / 服务器按需选择 缓存问题穿透缓存空值、雪崩随机过期时间、一致性主动更新 / 短过期是缓存使用的核心关注点 封装缓存帮助类统一处理过期策略、类型限制、序列化等问题提升代码复用性和可维护性。