杨中科B站视频链接:.NET 6教程,.Net Core 2022视频教程,杨中科主讲_哔哩哔哩_bilibili
缓存(Caching)是系统优化中简单又有效的工具,投入小收效大。数据库中的索引等简单有效的优化功能本质上都是缓存
1、缓存命中
2、缓存命中率
3、缓存数据不一致
cache-control
1、RFC7324是HTTP协议中对缓存进行控制的规范,其中重要的是cache-control这个响应报文头。服务器如果cache-control:max-age=60,则表示服务器指示浏览器端"可以缓存这个响应内容60秒"
2、我们只要给需要进行缓存控制的控制器的操作方法添加ResponseCacheAttribute这个Attribute,ASP.NET Core会自动添加cache-control报文头
3、验证:编写一个返回当前时间的Action方法。分别加和不加ResponseCacheAttribute看区别。也F12看看Network
4、缺点:各个浏览器缓存是相互独立的,每个浏览器都得请求一遍服务器端,再进行缓存,缓存不共享
Response Caching Middleware
1、如果ASP.NET Core中安装了“响应缓存中间件”,那么ASP.NET Core不仅会继续根据[ResponseCache]设置来生成cache-control响应报文头来设置客户端缓存,而且服务器端也会按照[ResponseCache]的设置来对响应进行服务器端缓存。和客户端缓存的区别?来自多个不同客户端的相同请求。
2、“响应缓存中间件”的好处:对于来自不同客户端的相同请求或者不支持客户端缓存的客户端,能降低服务器端的压力。
3、用法:app.MapControllers()之前加上app.UseResponseCaching()。请确保app.UseCors()写到app.UseResponseCacheing()之前
1、大部分浏览器都是支持RFC7324规范的,所以不方便用来测试服务端响应缓存。用默认忽略RFC7324规范的PostMan测试。试一下请求服务器端
2、可以在浏览器的“开发人员工具”中禁用缓存,但是和PostMan中不一致,为什么?“cache-control:no-cache”
3、也可以让PostMan在请求报文头中加如“cache-control:no-cache”,只要在Postman的设置中开启【Send no-cache headers】
1、无法解决恶意请求给服务器带来的压力
2、服务器端响应缓存还有很多限制,包括但不限于:响应状态码为200的Get或者HEAD请求才可能被缓存;报文头中不能含有Authorization、Set-Cookie等。
Authorization为什么?:把一个浏览器响应的具体用户数据的缓存下来,给到另外一个用户浏览器不现实
3、不怪他,honor RFC7234.It's a feature,not a bug
4、怎么办呢?采用内存缓存、分布式缓存等
内存缓存
1、把缓存数据放到应用程序的内存。内存缓存中保存的是一系列的键值对,就像Dictionaty类型一样
2、内存缓存的数据保存再当前运行的网站程序的内存中,是和进程相关的。因为再Web服务器中,多个不同的网站时运行在不同的进程中的,因此不同网站的内存缓存是不会互相干扰的,而且网站重启后,内存缓存中所有数据也就都被清空了。
内存缓存的用法
1、启用:builder.Service.AddMemoryCache()
2、注入IMemoryCache接口,查看接口的方法:TryGetValue、Remove、Set、GetOrCreate、GetOrCreateAsync
3、用GetOrCreateAsync讲解
public async Task GetBooks()
{
logger.LogInformation("开始执行GetBooks");
var items = await memCache.GetOrCreateAsync("AllBooks", async (e) =>
{
logger.LogInformation("从数据库中读取数据");
return await dbCtx.Books.ToArrayAsync();
});
logger.LogInformation("把数据返回给调用者");
return items;
}
缓存的过期时间
1、上面的例子中的缓存不会过期,除非重启服务器。
2、解决方法:在数据改变的时候调用Remove或者Set来删除或者修改缓存(优点:及时);过期时间(只要过期时间比较短,缓存数据不一致的情况也不会持续很长时间)
3、两种过期时间策略:绝对过期时间、滑动过期时间。它们分别是什么?
缓存的绝对过期时间
1、GetOrCreaeteAsync()方法的回调方法中有一个ICacheEntry类型的参数,通过ICacheEntry对当前的缓存项做设置
2、AbsoluteExpirationRelativeToNow用来设定缓存项的绝对过期时间
缓存的滑动过期时间
1、ICacheEntry的SlidingExpiration属性用来设定缓存项的滑动过期时间
两种过期时间混用
使用滑动过期时间策略,如果一个缓存项一直被频繁访问,那么这个缓存项就会一直被续期而不过期。可以对一个缓存项同时设定滑动过期时间和绝对过期时间,并且把绝对过期时间设定的比滑动过期时间长,这样缓存项的内容会在绝对过期时间内随着访问被滑动续期,但是一旦超过了绝对过期时间,缓存项就会被删除
内存缓存的是与非
1、无论用哪种过期时间策略,程序中都会存在缓存数据不一致的情况。部分系统(博客等)无所谓,部分系统不能忍受(比如金融)
2、可以通过其他机制获取数据源改变的信息,再通过代码调用IMemoryCache的Set方法更新缓存
什么是缓存穿透
string cacheKey = "Book" + id;//缓存键
Book? b = memCache.Get(cacheKey);
if(b==null)//如果缓存中没有数据
{
//查询数据库,然后写入缓存
b = await dbCtx.Books.FindAsync(id);
memCache.Set(cacheKey, b);
}
缓存穿透的解决方案
1、解决方法:把"查不到"也当成一个数据放入缓存
2、我们用GetOrCreateAsync方法即可,因为它会把null值也当成合法的缓存值
string cacheKey = "Book" + id;
var book = await memCache.GetOrCreateAsync(cacheKey, async (e) => {
var b = await dbCtx.Books.FindAsync(id);
logger.LogInformation("数据库查询:{0}",b==null?"为空":"不为空");
return b;
});
logger.LogInformation("Demo5执行结束:{0}", book == null ? "为空" : "不为空");
缓存雪崩
1、缓存项集中过期引起缓存雪崩
2、解决方法:再基础过期时间之上,再加一个随机的过期时间
缓存数据混乱
public User GetUserInfo()
{
Guid userId=...;//获取当前用户Id
return memCache.GetOrCreate("UserInfo", (e) => {
return ctx.User.Find(userId);
});
}
解决方法:合理设置key
封装内存缓存操作的帮助类
1、IQueryable、IEnumerable等类型可能存在着延迟加载的问题,如果把这两种类型的变量指向的对象保存到缓存中,在我们把它们取出来再去执行的时候,如果它们延迟加载时候需要的对象已经被释放的话,就会执行失败。因此缓存禁止这两种类型
2、实现随机缓存过期时间
public interface IMemoryCacheHelper
{
TResult? GetOrCreate(string cacheKey,
Func valueFactory, int expireSeconds);
Task GetOrCreateAsync(string cacheKey,
Func> valueFactory, int expireSeconds);
void Remove(string cacheKey);
}
查看实现代码
NETBookMaterials/MemoryCacheHelper.cs at main · yangzhongke/NETBookMaterials · GitHub
分布式系统中的内存缓存
如果集群节点的数量非常多的话,这样的重复查询也同样可能会把数据库压垮
分布式缓存服务器
用户什么做缓存服务器
1、用SQLServer做缓存性能并不好
2、Memcached是缓存专用,性能非常高,但是集群、高可用等方面比较弱,而且有“缓存键的最大长度为250字节”等限制。可以安装EnyimMemcachedCore这个第三方NuGet包
3、Redis不局限于缓存,Redis做缓存服务器比Memcached性能稍差,但是Redis的高可用、集群等方便非常强大,合适在数据量大、高可用性等场合使用
分布式缓存用法
1、NuGet安装:Microsoft.Extensions.Caching.StackExchangeRedis
2、
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost";
options.InstanceName = “yzk_”;//避免混乱
});
3、用时间显示测试用法
封装分布式缓存操作的帮助类
需求
1、解决缓存穿透、缓存雪崩等问题
2、自动地进行其他类型的转换
public interface IDistributedCacheHelper
{
TResult? GetOrCreate(string cacheKey, Func valueFactory, int expireSeconds);
Task GetOrCreateAsync(string cacheKey,
Func> valueFactory, int expireSeconds);
void Remove(string cacheKey);
Task RemoveAsync(string cacheKey);
}
3、实现类 NETBookMaterials/DistributedCacheHelper.cs at main · yangzhongke/NETBookMaterials · GitHub
1)加载现有的IConfiguration
2)加载项目根目录下的appsettings.json
3)加载项目根目录下的appsettings.{Environment}.json
4)当程序运行在开发环境下,程序会加载“用户机密”配置
5)加载环境变量中的配置
6)加载命令行中的配置
1、Why?开发环境、测试环境、生产环境需要进行不同的配置
2、运行环境:ASP.NET Core会从环境变量中读取名字为ASPNETCORE_ENVIRONMENT的值。推荐值:Developmen(开发环境)、Staging(测试环境)、Production(生产环境)
3、读取方法:app.Environment.EnvironmentName、app.Environment.IsDevelopment()
4、在WinDows和VS(推荐开发时用)中设置环境变量的方法
1、把不方便放到appsetting.json中的机密信息放到一个不在项目中的json文件中
2、在ASP.NET Core项目上单击右键,选择【管理用户机密】
3、secrets.json文件到底保存在哪里?如何和项目建立关系?csproj文件中的
1、供开发人员使用的,不适合在生产环境中使用
2、仍然是明文存储。不想别人看到怎么办?Azure Key Vault、Zack.AnyDBConfigProvider等。无法完全避免。加强安全防控更重要
3、如果因为重装、新员工等原因导致secrets.json重建,就要重新配置,麻烦。如果影响大的话,还是用集中式配置服务器
功能需求
1)系统的主要配置(Redis、Smtp)放到配置专用的数据库中。Zack.AnyDBConfigProvider
2)连接配置数据库的连接字符串配置在“用户机密”中。“Data Sourse=.;Initial Catalog=demo1;Integrated Security=SSPI;”
3)把Smtp的配置显示到界面上
4)程序启动的时候就连接Redis,并且把Redis连接对象注册到依赖注入系统中
builder.Services.AddSingleton(sp => {
string connStr = builder.Configuration.GetValue("Redis:ConnStr");
return ConnectionMultiplexer.Connect(connStr);
});
1、为什么要项目分层?带来什么问题?
2、创建一个.NET类库项目BooksEFCore,放实体等类。NuGet:Microsoft.EntityFrameworkCore.Relational
3、BooksEFCore中增加实体类Book和配置类
1、上下文类MyDbContext:为什么正式项目中最好不要在MyDbContext写数据库配置(连接不同的DB甚至不同类型的DB)。尽量数据库配置的代码写到ASP.NET Core项目中。不重写OnConfiguring方法,而是为MyDbContext类的构造方法增加DbContextOptions
2、创建ASP.NET Core项目,添加对“BookEFCore”项目的引用。NuGet安装Microsoft.EntityFrameworkCore.SqlServer
3、配置文件、配置代码等放到ASP.NET Core项目中。
builder.Services.AddDbContext(opt => {
string connStr = builder.Configuration.GetConnectionString("Default");
opt.UseSqlServer(connStr);
});
4、在Controller中注入MyDbContext,编写测试代码
5、生成实体类的迁移脚本。多项目的环境执行Add-Migration的时候可能会出现这个错误,原理是什么?
6、不用研究项目中Add-Migration的细节。实用的方案:编写实现IDesignTimeDbContextFactory接口的类,把配置放到里面反正是开发环境而已
7、可以把连接字符串配置到环境变量中,不过MyDbDesignTimeDbContextFactory中来读取配置系统,可以直接用Environment.GetEnvironmentVariable()读取环境变量
8、数据库迁移脚本要生成到BooksEFCore中,因此为这个项目安装Microsoft.EntityFrameworkCore.Tools、Microsoft.EntityFrameworkCore.SqlServer。然后把BooksEFCore设置为启动项目,并且在【程序包管理控制台】中也选中BookEFCore项目后,执行Add-Migration和Update-Database
1、建类库项目,方式实体类、DbContext、配置类等DbContext中不配置数据库连接,而是为DbContext增加一个DbContextOptions类型的构造函数
2、EFCore项目安装对应数据库的EFCore Provider
3、asp.net core项目引用EFCore项目,并且通过AddDbContext来注入DbContext及对DbContext进行配置
4、Controller中就可以注入DbContext类使用了
5、让开发环境的Add-Migration知道连接哪个数据库,在EFCore项目中创建一个实现了IDesignTimeDbContextFactory的类。并且在CreateDbContext返回一个连接开发数据库的DbContext
public MyDbContext CreateDbContext(string[] args)
{
DbContextOptionsBuilder builder = new DbContextOptionsBuilder();
string connStr = "Data Source=.;Initial Catalog=demo666;Integrated Security=SSPI;";
builder.UseSqlServer(connStr);
MyDbContext ctx = new MyDbContext(builder.Options);
return ctx;
}
如果不在乎连接字符串被上传到Git,就可以把连接字符串直接写死到CreateDbContext;如果在乎,那么CreateDbContext里面很难读取到VS中通过简单的方法设置的环境变量,所以必须把连接字符串配置到Windows的正式的环境变量中,然后再Environment.GetEnvironmentVariable读取 6、正常执行Add-Migration、Update-Database迁就就行了。需要把EFCore项目设置为启动项目,并且子啊【程序包管理器控制台】中也要选中EFCore项目,并且安装Microsoft.EntityFrameworkCore SqlServer、Microsoft.EntityFrameworkCore.Tools
填用AddDbContextPool
1、用AddDbContextPool代替AddDbContext可以实现“DbContext池”
2、AddDbContextPool的问题:
1)用AddDbContextPool注册的DbContext无法注入其他服务?而AddDbContext可以。为啥要为DbContext注册服务?为什么AddDbContextPool不行?
2)很多数据库的ADO.NET提供都实现了数据库连接池机制,可能会有冲突,实用的时候需要自己调节。
3、AddDbContextPool意义不大:
1)“小上下文”策略
2)有数据库连接池
4、DbContextPool中的每一个DbContext都对应一个数据库连接,DbContextPool中每多一个DbContext,数据库连接池中就会少一个数据库连接。当这两个池的大小不一样且DbContextPool大于数据库连接池,问题来了,DbContextPool根据自家池(假设128)子的大小畅快地向池中填DbContext,浑然不顾数据库连接池的大小(假设是100),当填到第101个DbContext时就会出现上面的错误
案例:复杂.NET Core项目中批量注册上下文
1、项目采用“小上下文”策略,在项目中可能存在多个上下文类,如果手动AddDbContext就太麻烦了
2、反射扫描程序集中所有的上下文类,然后逐个调用AddDbContext注册,Install-Package Zack.Infrastructure GitHub - yangzhongke/NETBookMaterials
3、使用演示
1、切面编程机制,在ASP.NET Core特定的位置执行我们自定义的代码
2、ASP.NET Core中的Filter的五种类型: Authorization Filter、Resource Filter、Action Filter、Exception Filter、Result Filter。本书中重点讲解Exception Filter和Action Filter
3、所有筛选器一般有同步和异步两个版本,比如IActionFilter、IAsyncActionFilter接口
1、当系统中出现未经处理的异常的时候,异常筛选器就会执行
1、当系统中出现未处理异常的时候,我们需要统一给客户端返回如下格式的响应报文:{“code”:"500","message":"异常信息"}。对于开发环境中message是异常堆栈,对于其他环境message用一个general的报错信息
2、实现IAsyncExceptionFilter接口。注入IHostEnvironment得知运行环境
ObjectResult result = new ObjectResult(new { code = 500, message = message });
result.StatusCode = 500;
context.Result = result;
context.ExceptionHandled = true;
3、
builder.Services.Configure(options =>
{
options.Filters.Add();
});
Action Filter
1、IAsyncAction接口
2、多个ActionFilter的链式执行,栈(先进后出)
测试代码
public class MyActionFilter1 : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context,
ActionExecutionDelegate next)
{
Console.WriteLine("MyActionFilter 1:开始执行");
ActionExecutedContext r = await next();
if (r.Exception != null)
Console.WriteLine("MyActionFilter 1:执行失败");
else
Console.WriteLine("MyActionFilter 1:执行成功");
}
}
再编写MyActionFilter2
builder.Services.Configure(options =>
{
options.Filters.Add();
options.Filters.Add();
});
无论是同步还是异步Action方法,都能用IAsyncActionFilter处理
需求
1、数据库事务:要么全部成功、要么全部失败
2、自动化:启动、提交以及回滚事务
3、当一般使用EF Core进行数据库操作的代码放到TransactionScope声明的范围中的时候,这段代码就会自动被标记为“支持事务”
4、TransactionScope实现了IDisposable接口,如果一个TransactionScope的对象没有调用Complete()就执行了Dispose()方法,则事务会被回滚,否则事务就会被提交。
5、TransactionScope还支持嵌套式事务
6、.NET Core中的TransactionScope不像.NET FX一样有MSDTC分布式事务提升的问题。请使用最终一致性事务
1、对于强制不进行事务控制的Action方法,请标注NotTransactionalAttribute
2、开发筛选器TransactionScopeFilter;把TransactionScopeFilter注册到Program.cs中
3、编写两个插入数据的代码,测试
需求
1、Action Filter可以在满足条件的时候终止操作方法的执行
2、在Action Filter中,如果我们不调用await next(),就可以终止Action方法的执行了
3、为了避免恶意客户端频繁发送大量请求消耗服务器资源,我们要实现“一秒钟内只允许最多有一个来自同一个IP地址的请求”
中间件是ASP.NET Core的核心组件,MVC框架、响应缓存、身份验证、CORS、Swagger等都是内置中间件
1、广义上来讲:Tomcat、WebLogic、Redis、IIS;狭义上来讲,ASP.NET Core中的中间件指ASP.NET Core中的一个组件。
2、中间件有前逻辑、next、后逻辑3部分组成,前逻辑为第一段要执行的逻辑代码、next为指向下一个中间件的调用、后逻辑为从下一个中间件执行返回所执行的逻辑代码。每个HTTP请求都要经历一系列中间件的处理,每个中间件对于请求进行特定的处理后,在转到下一个中间件,最终的业务逻辑代码执行完成后,响应的内容也会按照处理的相反顺序进行处理,然后形成HTTP响应报文返回给客户端。
3、中间件组成一个管道,整个ASP.NET Core的执行过程就是HTTP请求和响应按照中间件组装的顺序在中间件之间流转的过程。开发人员可以对组成管道的中间件按照需要进行自由组合
Map、Use和Run。Map用来定义一个管道可以处理哪些请求。Use和Run用来定义管道,一个管道由若干个Use和一个Run组成,每个Use引入一个中间件,而Run是用来执行最终的核心应用逻辑
简单的自定义中间件
为了能够更清晰地了解中间件,我们创建一个空的ASP.NET Core的项目,然后手动添加中间件。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Map("/test", async appbuilder => {
appbuilder.Use(async (context, next) => {
context.Response.ContentType = "text/html";
await context.Response.WriteAsync("1 Start
");
await next.Invoke();
await context.Response.WriteAsync("1 End
");
});
appbuilder.Use(async (context, next) => {
await context.Response.WriteAsync("2 Start
");
await next.Invoke();
await context.Response.WriteAsync("2 End
");
});
appbuilder.Run(async ctx => {
await ctx.Response.WriteAsync("hello middleware
");
});
});
app.Run();
封装简单的自定义中间件:
1、如果中间件的代码比较复杂,或者我们需要重复使用一个中间件的话,我们最好把中间件的代码放到一个单独的“中间件类”中
2、中间件类是一个普通的.NET类,它不需要集成任何父类或者实现任何接口,但是这个类需要有一个构造方法,构造方法至少要有一个RequestDelegate类型的参数,这个参数用来指向下一个中间件。这个类还需要定义一个名字为Invoke或InvokeAsync的方法,方法至少有一个HttpContext类型的参数,方法的返回值必须是Task类型。中间件类的构造方法和Invoke(或InvokeAsync)方法还可以定义其他的参数,其他参数的值会通过依赖注入自动赋值
检查请求中是否有password=123的查询字符串,而且会把请求报文体按照Json格式尝试解析为dynamic类型的对象,并且把dynamic对象放入context.items中供后续的中间件或者Run使用
public class CheckAndParsingMiddleware
{
private readonly RequestDelegate next;
public CheckAndParsingMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task InvokeAsync(HttpContext context)
{
string pwd = context.Request.Query["password"];
if (pwd == "123")
{
if (context.Request.HasJsonContentType())
{
var reqStream = context.Request.BodyReader.AsStream();
dynamic? jsonObj = DJson.Parse(reqStream);
context.Items["BodyJson"] = jsonObj;
}
await next(context);
}
else context.Response.StatusCode = 401;
}
}
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Map("/test", async appbuilder => {
appbuilder.UseMiddleware();
appbuilder.Run(async ctx => {
ctx.Response.ContentType = "text/html";
ctx.Response.StatusCode = 200;
dynamic? jsonObj = ctx.Items["BodyJson"];
int i = jsonObj.i;
int j = jsonObj.j;
await ctx.Response.WriteAsync($"{i}+{j}={i+j}");
});
});
app.Run();
自己动手模仿Web API中间件
代码解读
框架由MyStaticFilterMiddleware、MyWebAPIMiddleware、NotFoundMiddleware这3个中间件组成 运行查看效果,解读源代码
需求
1、什么是Markdown;不被浏览器支持;所以,编写一个在服务器端把Markdown装换为HTML的中间件
2、我们开发的中间件是构建在ASP.NET Core内置的StaticFiles中间件之上,并且在它之前运行,所有的*.md文件都被放到wwwroot文件夹下,当我们请求wwwroot下其他的静态文件的时候,StaticFiles中间件会把它们返回给浏览器,而当我们请求wwwroot下的*.md文件的时候,我们编写的中间件会读取对应的*.md文件并且把它们转换为HTML格式返回给浏览器
本文编码测试
调用Ude.NetStandard这个NuGet包的CharsetDetector类来探测文件的编码。
private static string DetectCharset(Stream stream)
{
CharsetDetector charDetector = new();
charDetector.Feed(stream);
charDetector.DataEnd();
string charset = charDetector.Charset ?? "UTF-8";
stream.Position = 0;
return charset;
}
MD-->HTML
NuGet包:MarkdownSharp
Markdown markdown = new Markdown();
string html= markdown.Transform(text);
整合
app.UseMiddleware();
app.UseStaticFiles();
Filter与Middleware的区别
关系
中间件是ASP.NET Core这个基础提供的功能,而Filter是ASP.NET Core MVC中提供的功能。ASP.NET Core MVC是由MVC中间件提供的框架,而Filter属于MVC中间件提供的功能
区别
1、中间件可以处理所有的请求,而Filter只能处理对控制器的请求;中间件运行在一个更底层、更抽象的级别,因此在中间件中无法处理MVC中间件特有的概念。
2、中间件和Filter可以完成很多相似的功能。“未处理异常中间件”和“未处理异常Filter”;“请求限流中间件”和“请求限流Filter”的区别
3、优先选择使用中间件;但是如果这个组件只针对MVC或者需要调用MVC相关的类的时候,我们就只能选择Filter