ASP.NET Core中服务注入的地方
1.在ASP.NET Core项目中一般不需要自己创建ServiceCollection、IServiceProvider。在Program.cs的builder.Build()之前向builder.Services中注入。
2.在Controller中可以通过构造方法注入服务。
定义一个MyService类
1 2 3 4 5
| public class MyService{ public IEnumerable<string> GetName(){ return new string[] {"you","xian","yu"}; } }
|
在ASP.NET Core项目的Program.cs文件中的var app=builder.Build()代码之前通过AddScoped方法来注册MyService服务。
1
| builder.Services.AddScoped<MyService>();
|
在控制器中,我们通过构造方法来注入服务。
1 2 3 4 5 6 7 8 9 10 11
| public class TestController:ControllerBase{ private readonly MyService myService; public TestController(MyService myService){ this.myService=myService; } [HttpGet] public string Test(){ var names=myService.GetNames(); return string.Join(",",names); } }
|
低使用频率服务的另类注入方式
1.把Action用到的服务通过Action的参数注入,在这个参数上标注[FromServices]。和Action的其他参数不冲突。
2.一般不需要,只有调用频率不高并且资源的创建比较消耗资源的服务才[FromServices]。
3.只有Action方法才能用[FromServices],普通的类默认不支持。
1 2 3 4
| public string Test([FromServices]MyService myService,string name){ var names=myService.GetName(); return string.Join(",",names)+",Hello:"+name; }
|
开发模块化的服务注册框架
一个软件通常有多个项目组成,这些项目都会直接或者间接被主ASP.NET Core项目引用。这些项目中通常都会用到若干个被注入的服务,因此我们需要在主ASP.NET Core项目的Program.cs中注册这些服。这样不仅增加了Program.cs管理的复杂度,而且增加了项目的耦合度。
Scrutor是 Kristian Hellang 大神写的一个基于Microsoft.Extensions.DependencyInjection的一个扩展库,主要是为了简化我们对DI的操作。
Scrutor提供Scan这个方法。
1.选择程序集:指定从哪个程序集中扫描类型。
1
| builder.Services.Scan(scan => scan.FromAssemblyOf<Class1>());
|
2.添加类:过滤需要注册的类。
1 2 3
| builder.Services.Scan(scan => scan.FromAssemblyOf<Class1>() .AddClasses(cl => cl.Where(t => t.Name.EndsWith("Service"))) );
|
3.指定注册方式:指定应该注册为哪些接口。
1 2 3 4
| builder.Services.Scan(scan => scan.FromAssemblyOf<Class1>() .AddClasses(cl => cl.Where(t => t.Name.EndsWith("Service"))) .AsImplementedInterfaces() );
|
4.设置生命周期:为注册的服务设置生命周期
1 2 3 4 5
| builder.Services.Scan(scan => scan.FromAssemblyOf<Class1>() .AddClasses(cl => cl.Where(t => t.Name.EndsWith("Service"))) .AsImplementedInterfaces() .WithScopedLifetime() );
|
性能优化“万金油”:缓存
缓存(caching)是系统优化中简单又有效的工具,只要简单几行代码或几个简单的配置,我们就可利用缓存的性能得到极大的提升。
1.什么是缓存
缓存是一个用来保存数据的区域,从缓存区域中读取数据的速度比从数据源读取数据的速度快很多。在从数据源(如数据库)获取数据之后,我们可以把数据保存到缓存中。

客户端响应缓存
1.RFC7324是HTTP协议中对缓存进行控制的规范,其中重要的是cache-control这个响应报文头。服务器如果返回cache-control:max-age=60,则表示服务器指示浏览器端“可以缓存这个响应内容60秒”。
2.我们只要给需要进行缓存控制的控制器的操作方法添加ResponseCacheAttribute这个Attribute,ASP.NET Core会自动添加cache-control报文头。
3.验证:编写一个返回当前时间的Action方法。分别加和不加ResponseCacheAttribute看区别。
1 2 3 4 5 6 7 8 9
| public class TestController : ControllerBase { [HttpGet] [ResponseCache(Duration =20)] public DateTime Now() { return DateTime.Now; } }
|
第一次访问,服务器返回当前时间

第二次访问,响应是直接从浏览器缓存中获取的。

第三次访问

服务器端响应缓存
1.如果ASP.NET Core中安装了“相应缓存中间件”,那么ASP.NET Core不仅会继续根据[ResponseCache]设置来生成cache-control响应报文头来设置客户端缓存,而且服务器也会按照[ResponseCache]的设置来对应响应服务器端缓存。和客户端缓存的区别?来子多个不同客户端的相同请求。
2.“响应缓存中间件”的好处:对于来自不同客户端的相同请求或者不支持客户端缓存的客户端,能降低服务器端的压力。
3.用法:在程序集Program.cs中app.MapControllers()之前加上app.UseResponseCaching()。请确保app.UseCors()写到app.UseResponseCaching()之前。
1 2
| app.UseResponseCaching(); app.MapControllers();
|
服务器端响应缓存很鸡肋
1.无法解决恶意请求给服务器带来的压力。
2.服务器端响应缓存还有很多限制,包括但不限于:响应状态码为200的GET或者HEAD响应才可能被缓存;报文头中不能含有Authorization、Set-Cookie等。
3.建议采用ASP.NET Core提供的内存缓存、分布式缓存机制来编写程序,以更灵活地进行自定义缓存处理。
内存缓存
1.把缓存数据放在应用程序的内存。内存缓存中保存的是一个系列的键值对,就像Dictionary类型一样。
2.内存缓存的数据保存在当前运行的网站程序的内存中,是和进程相关的。因为在Web服务器中,多个不同网站重启后,内存缓存中的所有数据也就都被清空了。
内存缓存用法
1.启用:
1
| builder.Services.AddMemoryCache();
|
2.注入IMemoryCache接口,查看接口的方法:TryGetValue、Remove、Set、GetOrCreate、GetOrCreateAsync
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| private readonly IMemoryCache memoryCache; private readonly ILogger<TestController> logger;
public TestController(IMemoryCache memoryCache, ILogger<TestController> logger) { this.memoryCache = memoryCache; this.logger = logger; } [HttpGet] public async Task<ActionResult<Book?>> GetBookById(long id) { logger.LogInformation($"GetBookById id={id}"); Book? b= await memoryCache.GetOrCreateAsync("Book" + id, async (e) => { logger.LogInformation($"GetBookById id={id} 进入缓存"); return await MyDbContext.GetBookByIdAsync(id); }); logger.LogInformation($"GetBookById id={id} 结束缓存"); if (b == null) { return NotFound($"找不到id={id}的书"); } else { return b; } }
|
缓存的过期时间策略
1.上面的例子中的缓存不会过期,除非重启服务器。
2.解决方法:在数据改变的时候调用Remove或者Set来删除或者修改缓存(优点:及时);过期时间(只要过期时间比较短,缓存数据不一致的情况也不会持续很长时间。)
3.两种过期时间策略:绝对过期时间、滑过过期时间。
缓存的绝对过期时间
1.GetOrCreateAsync()方法的回调方法中有一个ICacheEntry类型的参数,通过ICacheEntry对当前的缓存项做设置。
2.AbsoluteExpirationRelativeToNow用来设定缓存项的绝对过期时间。
1 2 3 4 5 6 7
| logger.LogInformation($"GetBookById id={id}"); Book? b= await memoryCache.GetOrCreateAsync("Book" + id, async (e) => { e.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10); logger.LogInformation($"GetBookById id={id} 进入缓存"); return await MyDbContext.GetBookByIdAsync(id); });
|
缓存的滑动过期时间
1.ICacheEntry的SlidingExpiration属性用来设定缓存项的滑动过期时间。
1 2 3 4 5 6 7 8
| logger.LogInformation($"GetBookById id={id}"); Book? b= await memoryCache.GetOrCreateAsync("Book" + id, async (e) => { e.SlidingExpiration = TimeSpan.FromSeconds(10); logger.LogInformation($"GetBookById id={id} 进入缓存"); return await MyDbContext.GetBookByIdAsync(id); });
|
两种过期时间混用
使用滑动过期时间策略,如果一个缓存项一直被频繁访问,那么这个缓存项就会一直被续期而不过期。可以对一个缓存项同时设定滑动过期时间和绝对过期时间长,这样缓存想的内容会在绝对过期时间内随着访问被滑动续期,但是一旦超过了绝对过期时间,缓存像就会被删除。
缓存穿透问题的规避
在使用内存缓存的时候,如果处理不当,我们容易遇到“缓存穿透”的问题。我们注意到,IMemoryCache接口中有一个Get(object key)方法,它用来根据缓存键key查找缓存值,如果找不到缓存项,则方法会返回null等默认值。
缓存穿透的解决方案
1.解决方法:把“查不到”也当成一个数据放入缓存。
2.我们用GetOrCreateAsync方法即可,因为它会把null值也当成合法的缓存值。
1 2 3 4 5 6 7 8 9 10
| logger.LogInformation($"GetBookById id={id}"); Book? b= await memoryCache.GetOrCreateAsync("Book" + id, async (e) => { Book? d= await MyDbContext.GetBookByIdAsync(id); Console.WriteLine($"GetBookById id={id} 进入缓存"); logger.LogInformation($"GetBookById id={id} 进入缓存"); return await MyDbContext.GetBookByIdAsync(id); });
|
缓存雪崩
1.缓存项集中过期引起缓存雪崩。
2.解决方法:在基础过期时间之上,再加一个随机的过期时间。
1 2 3 4 5 6 7 8 9 10 11
| logger.LogInformation($"GetBookById id={id}"); Book? b= await memoryCache.GetOrCreateAsync("Book" + id, async (e) => { e.AbsoluteExpirationRelativeToNow=TimeSpan.FromSeconds(Random.Shared.Next(5, 10)); Book? d= await MyDbContext.GetBookByIdAsync(id); Console.WriteLine($"GetBookById id={id} 进入缓存"); logger.LogInformation($"GetBookById id={id} 进入缓存"); return await MyDbContext.GetBookByIdAsync(id); });
|
封装内存缓存操作的帮助类
1.IQueryable、IEnumerable等类型可能存在着延迟加载的问题,如果把这两种类型的变量指向的对象保存到缓存中,在我们把它们取出来再去执行的时候,如果它们延迟加载时候需要的对象已经被释放的话,就会执行失败。因此缓存禁止这两种类型。
2.实现随机缓存过期时间。
使用NuGet包:Zack.ASPNETCore
1
| builder.Services.AddScoped<IMemoryCacheHelper, MemoryCacheHelper>();
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| [HttpGet] public async Task<ActionResult<Book?>> Test3(long id) { var book = await memoryCache.GetOrCreateAsync("Book" + id, async (e) => { e.SlidingExpiration = TimeSpan.FromSeconds(5); var book = await MyDbContext.GetBookByIdAsync(id); return book; },10);
if (book == null) { return NotFound("不存在"); } else { return book; } }
|
分布式缓存
分布式系统中的内存缓存

如果集群节点的数量非常多的话,这样的重复查询也同样可能会把数据库压垮。
1.常用的分布式缓存服务器有Redis、Memcached等。
2..NET Core中提供了统一的分布式缓存服务器的操作接口IDistributedCache,用法和内存缓存类似。
3.分布式缓存和内存缓存的区别:缓存值的类型为byte[],需要我们进行类型转换,也提供了一些按照string类型存取缓存值的扩展方法。

用什么做缓存服务器
1.Memcached是缓存专用,性能非常高,但是集群、高可用等方面比较弱,而且有“缓存键的最大长度为250字节”等限制。可以安装EnyimMemcachedCore这个第三NuGet包。
2.Redis不局限于缓存,Redis做缓存服务器比Memcached性能稍差,但是Redis的高可用、集群等方面非常强大,适合在数据量大、高可用性等场合使用。
使用Redis
1.NuGet安装
1
| Microsoft.Extensions.Caching.StackExchangeRedis
|
配置redis
1 2 3 4 5
| builder.Services.AddStackExchangeRedisCache(options => { options.Configuration= "127.0.0.1:6379"; options.InstanceName = "XIAN_"; });
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| private readonly IMemoryCache memoryCache; private readonly ILogger<TestController> logger; private readonly IMemoryCacheHelper memoryCacheHelper; private readonly IDistributedCache distCache;
public TestController(IMemoryCache memoryCache, ILogger<TestController> logger, IMemoryCacheHelper memoryCacheHelper, IDistributedCache distCache) { this.memoryCache = memoryCache; this.logger = logger; this.memoryCacheHelper = memoryCacheHelper; this.distCache = distCache; }
[HttpGet ] public async Task<ActionResult<Book?>> Test2(long id) { Book? book; string? s= await distCache.GetStringAsync("Book" + id); if(s == null) { book = await MyDbContext.GetBookByIdAsync(id); await distCache.SetStringAsync("Book" + id,JsonSerializer.Serialize(book)); } else { book = JsonSerializer.Deserialize<Book?>(s); }
if (book == null) { return NotFound("不存在"); } else { return book; } }
|
运行

使用Redisdesktop查看,转换json格式

封装分布式缓存操作的帮助类
解决雪崩缓存问题
1
| builder.Services.AddScoped<IDistributedCacheHelper, DistributedCacheHelper>();
|
注入服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| private readonly IMemoryCache memoryCache; private readonly ILogger<TestController> logger; private readonly IMemoryCacheHelper memoryCacheHelper; private readonly IDistributedCache distCache; private readonly IDistributedCacheHelper distributedCacheHelper;
public TestController(IMemoryCache memoryCache, ILogger<TestController> logger, IMemoryCacheHelper memoryCacheHelper, IDistributedCache distCache, IDistributedCacheHelper distributedCacheHelper) { this.memoryCache = memoryCache; this.logger = logger; this.memoryCacheHelper = memoryCacheHelper; this.distCache = distCache; this.distributedCacheHelper = distributedCacheHelper; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| [HttpGet] public async Task<ActionResult<Book?>> Test3(long id) { var book = await distributedCacheHelper.GetOrCreateAsync("Book" + id, async (e) => { e.SlidingExpiration = TimeSpan.FromSeconds(5); var book = await MyDbContext.GetBookByIdAsync(id); return book; },10);
if (book == null) { return NotFound("不存在"); } else { return book; } }
|