本系列专栏基于杨中科老师的《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.AddScoped<IBookService, BookService>(); // 作用域(每次请求创建一个实例)
builder.Services.AddTransient<ILoggerHelper, LoggerHelper>(); // 瞬时(每次获取创建新实例)
builder.Services.AddSingleton<ICacheHelper, 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);
}
}
(3)Action 参数注入
使用[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.AddScoped<IBookService, BookService>(); services.AddScoped<IBookRepository, BookRepository>(); } } // 订单模块的注册类(OrderModule/ModuleInitializer.cs) public class OrderModuleInitializer : IModuleInitializer { public void Initialize(IServiceCollection services) { services.AddScoped<IOrderService, 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-age=60;Location:缓存位置(Client/Any/Never);NoStore:是否禁止缓存(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] // Duration>0时,服务器+客户端均缓存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 ILogger<BookService> _logger;
public BookService(IMemoryCache memCache, AppDbContext dbCtx, ILogger<BookService> logger)
{
_memCache = memCache;
_dbCtx = dbCtx;
_logger = logger;
}
public async Task<Book[]> 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 Task<Book?> 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 name="TResult">缓存类型(禁止IQueryable/IEnumerable)</typeparam>
/// <param name="cacheKey">缓存键</param>
/// <param name="valueFactory">缓存工厂方法</param>
/// <param name="expireSeconds">基础过期秒数(会添加随机偏移)</param>
/// <returns>缓存值</returns>
TResult? GetOrCreate<TResult>(string cacheKey, Func<ICacheEntry, TResult?> valueFactory, int expireSeconds)
where TResult : class;
/// <summary>
/// 获取/创建缓存(异步)
/// </summary>
Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<ICacheEntry, Task<TResult?>> 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? GetOrCreate<TResult>(string cacheKey, Func<ICacheEntry, 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 Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<ICacheEntry, Task<TResult?>> 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.AddScoped<IMemoryCacheHelper, MemoryCacheHelper>();
// 业务层使用
public async Task<Book[]> 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 | 分布式系统、高可用要求高的场景 |
(2)Redis 分布式缓存使用(推荐)
- 安装 NuGet 包:
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis - 注册服务(Program.cs):
builder.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
{
Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<DistributedCacheEntryOptions, Task<TResult?>> 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 Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<DistributedCacheEntryOptions, Task<TResult?>> valueFactory, int expireSeconds)
where TResult : class
{
// 1. 尝试获取缓存
var cacheValue = await _distributedCache.GetStringAsync(cacheKey);
if (!string.IsNullOrEmpty(cacheValue))
{
return JsonSerializer.Deserialize<TResult>(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 响应)→ 内存缓存(进程内)→ 分布式缓存(跨进程 / 服务器),按需选择; 缓存问题:穿透(缓存空值)、雪崩(随机过期时间)、一致性(主动更新 / 短过期)是缓存使用的核心关注点; 封装缓存帮助类:统一处理过期策略、类型限制、序列化等问题,提升代码复用性和可维护性。
855



被折叠的 条评论
为什么被折叠?



