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")))//只注册以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() //设置为Scoped生命周期
);

性能优化“万金油”:缓存

缓存(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)
{
//GetOrCreateAsync二合一:1。获取缓存 2.如果没有缓存,则执行GetBookByIdAsync方法,并将结果缓存
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.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10); // 设置缓存过期时间*/
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) =>
{
/*e.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10); // 设置缓存过期时间*/
/*e.SlidingExpiration = TimeSpan.FromSeconds(10); // 设置滑动过期时间*/
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(10); // 设置缓存过期时间*/
/*e.SlidingExpiration = TimeSpan.FromSeconds(10); // 设置滑动过期时间*/
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;
}
}