乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 浅析ASP.NET Core性能设计,使用内存、分布式缓存(Redis)敏捷响应
ASP.NET Core性能优化
避免阻塞调用
ASP.NET Core应用应设计为可同时处理许多请求。异步API允许较小线程池处理数千个并发请求,无需等待阻塞调用。线程可以处理另一个请求,而不是等待长时间运行的同步任务完成。
ASP.NET Core应用中的一个常见性能问题是阻塞可以异步进行的调用。许多同步阻塞调用都会导致线程池饥饿和响应时间降低。
禁止行为
- 通过调用
Task.Wait
或Task<TResult>.Result
. - 获取常见代码路径中的锁。当构建为并行运行代码时,ASP.NET Core应用的性能最高。
- 调用
Task.Run
并立即等待它。ASP.NET Core已经在普通线程池线程上运行应用代码,因此调用Task.Run
只会导致不必要的额外线程池计划。即使计划的代码会阻止某个线程,Task.Run
也不会阻止该线程。
建议做法
- 使热代码路径成为异步。
- 如果有异步API可用,则异步调用数据访问、I/O和长时间运行的操作API。请勿用于
Task.Run
使同步API异步。 - 使控制器/RazorPage操作成为异步。为了获益于
async/await
模式,整个调用堆栈都是异步的。
最大程度减少大型对象分配
.NET Core垃圾回收器在ASP.NET Core应用中自动管理内存分配和释放。自动垃圾回收通常意味着开发人员无需担心如何或何时释放内存。但是,清理未引用的对象会占用CPU时间,因此开发人员应最大限度减少热代码路径中的对象分配。垃圾回收在大型对象(>85K字节)上成本特别高昂。大型对象存储在大型对象堆上,需要完整(第2代)垃圾回收才能清理。与第0代和第1代回收不同,第2代回收需要临时暂停应用执行。频繁分配和取消分配大型对象可能会导致性能不一致。
建议
- 请考虑缓存经常使用的大型对象。缓存大型对象会阻止进行成本高昂的分配。
- 使用
ArrayPool<T>
池缓冲区来存储大型数组。 - 请勿在热代码路径上分配许多生存期较短的大型对象。
可以通过在PerfView中查看垃圾回收(GC)统计信息并检查以下内容来诊断内存问题:
- 垃圾回收暂停时间。
- 花费在垃圾回收上的处理器时间百分比。
- 第0代、第1代和第2代的垃圾回收量。
优化数据访问和I/O
与数据存储和其他远程服务的交互通常是ASP.NET Core应用的最慢部分。高效读取和写入数据对于良好的性能至关重要。
建议
- 请异步调用所有数据访问API。
- 请勿检索不需要的数据。编写查询以便仅返回当前HTTP请求所需的数据。
- 如果可接受稍微过时的数据,请考虑缓存从数据库或远程服务检索的经常访问的数据。根据方案使用
MemoryCache
或DistributedCache
。 - 请尽量缩短网络往返。目标是在单个调用而不是多个调用中检索所需数据。
- 当出于只读目的访问数据时,请在
Entity Framework Core
中使用无跟踪查询。EFCore可以更有效地返回无跟踪查询的结果。 - 请筛选和聚合LINQ查询(例如使用
.Where
、.Select
或.Sum
语句),以便数据库执行筛选。 - 请考虑在EFCore客户端上解析某些查询运算符,这可能会导致查询执行效率低下。
- 请勿对集合使用投影查询,这可能会导致执行“N+1”个SQL查询。
以了解可提高大规模应用性能的方法:
- DbContext池
- 显式编译的查询
建议在提交基本代码之前衡量前面高性能方法的影响。已编译查询的额外复杂性可能无法证明性能改进的合理性。
通过使用Application Insights或分析工具查看访问数据所用的时间,可以检测到查询问题。大多数数据库还提供有关频繁执行的查询的统计信息。
什么是缓存
- 缓存是计算结果的"临时"存储和重复使用
- 缓存本质是用"空间"换取"时间"
现实生活中的缓存机制
- 车站的等候大厅
- 物流仓库
- 家里的冰箱
这些都是现实生活中类似于缓存的场景。
缓存的场景
- 计算结果,如:反射对象缓存
- 请求结果,如:DNS缓存
- 临时共享数据,如:会话存储
- 热点访问内容页:如:商品详情
- 热点变更逻辑数据,如:秒杀的库存数
缓存的策略
- 越接近最终的输出结果(靠前),效果越好
- 缓存命中率越高越好,命中率低就意味着"空间"的浪费,因为缓存实际上是占用空间的,比如内存空间
缓存的位置
基于B/S结构的系统,缓存的位置包括如下
- 浏览器中
- 反向代理服务器中(负载均衡),比如CDN的负载均衡器,它会负责一些缓存的策略,将后端的响应数据缓存在代理服务器中,直接响应给客户端
- 应用进程内存中
- 分布式存储系统中,比如Redis内
- 数据库系统中,在数据库查询的缓存
缓存实现的要点
- 缓存Key生成策略,表示缓存数据的范围、业务含义
- 缓存失败策略,如过期时间机制(比如说固定时间周期缓存30秒)、主动刷新机制(缓存永不失效,但是会有监听缓存背后的数据如果发生变化的时候,主动刷新缓存)
- 缓存更新策略,表示更新缓存数据的时机
缓存使用的问题
设计缓存系统的时候,经常会遇到如下问题
- 缓存失效,导致数据不一致,这个不一致可能会导致一些业务问题,根据不同业务特性,需要去重点关注
- 缓存穿透,查询无数据时,导致缓存不生效,查询都落在数据库,一般建议说,当数据为Null时,在缓存里面强制返回一个默认值,避免缓存穿透的情况
- 缓存击穿,缓存失效瞬间,大量请求访问到数据库,比如说某一个页面的并发请求量比较大,这个时候在缓存失效的瞬间,有请求需要去数据库访问并且获取到新的缓存数据,这时候并发量大意味着大量的并发会访问到数据库,一般建议说,使用二级缓存的策略,当一级缓存失效时,允许一个请求落到数据库上面,帮我们更新缓存数据,重置缓存有效时间,其他的请求仍然通过缓存去响应。
- 缓存雪崩,大量缓存同一时间失效,导致数据库压力,它会周期性的随着缓存失效压力大起来,一般建议说,缓存的失效时间策略应该定义得相对来讲比较均匀的,让我们系统数据库接收到请求相对均匀,不会出现说缓存Key会在同一时间有大量失效的情况,让它们错开,比如说有一部分缓存设计成五分钟、七分钟
缓存涉及的组件
- ResponseCache,中间件
- Microsoft.Extensions.Caching.Memory.IMemoryCache,内置内存缓存
- Microsoft.Extensions.Caching.Distributed.IDistributedCache,内置分布式缓存
- EasyCaching,开源社区中国作品
缓存按位置分类
- 内存缓存,内存中缓存使用服务器内存来存储缓存的数据
- 分布式缓存,当应用托管在云或服务器场中时,使用分布式缓存将数据存储在内存中
内存缓存和分布式缓存的区别
- 内存缓存可以存储任意对象
- 分布式缓存对象需要支持序列化
- 分布式缓存远程请求可能失败,内存缓存不会
当我们使用分布式缓存的时候,可能会用到Memcache
、Redis
。
内存缓存就是在当前进程内的使用内存来存储的缓存。
内存缓存可以存储任意对象,这个对象不需要关心是否需要序列化,只要在内存中可以引用它即可。
分布式缓存则要求对象支持序列化,需要将对象序列化并且通过网络传输存储到分布式缓存系统里面去,比如说二进制的序列化方式或者Json的序列化方式
目前基于Redis的缓存,一般都是使用Json序列化的方式,这就意味着分布式缓存它的应用场景是受到序列化支持的限制的。
分布式缓存的另外一个问题是,它远程请求可能失败,内存缓存则不会有这个问题。
响应缓存中间件
什么是响应缓存
响应缓存可减少客户端或代理对Web服务器发出的请求数。响应缓存还减少了Web服务器为生成响应而执行的工作量。响应缓存在标头中设置。
ResponseCache
属性可设置响应缓存标头。客户端和中间代理应遵循HTTP 1.1缓存规范下缓存响应的标头。
对于遵循HTTP 1.1缓存规范的服务器端缓存,请使用响应缓存中间件。
中间件可以使用ResponseCacheAttribute
属性来影响服务器端缓存行为。
响应缓存中间件:
- 启用基于HTTP缓存头的缓存服务器响应。实现标准HTTP缓存语义。像代理一样基于HTTP缓存标头进行缓存。
- 通过对RazorPages等UI应用没有好处,因为浏览器通常会设置阻止缓存的请求头。正在考虑将输出缓存用于下一版本的ASP.NETCore,这将使UI应用受益。使用输出缓存,配置可决定了应独立于HTTP标头缓存的内容。有关详细信息,请参阅此GitHub问题。
- 对于来自满足缓存条件的客户端的公共GET或HEAD API请求可能有用。
基于HTTP的响应缓存
HTTP 1.1缓存规范介绍了Internet缓存的行为方式。用于缓存的主HTTP标头是Cache-Control
,它用于指定缓存指令。
当请求从客户端到达服务器以及响应从服务器返回客户端时,这些指令控制缓存行为。
请求和响应在代理服务器之间移动,并且代理服务器还必须符合HTTP 1.1缓存规范。
Cache-Control
标头的HTTP 1.1缓存规范要求使用缓存来遵循客户端发送的有效Cache-Control
标头。客户端可以使用no-cache
标头值发出请求,并强制服务器针对每个请求生成新的响应。
如果考虑HTTP缓存的目标,则始终遵循客户端Cache-Control
请求标头是有意义的。根据官方规范,缓存旨在减少在客户端、代理和服务器网络中满足请求的延迟和网络开销。它不一定是控制源服务器上的负载的一种方法。
使用响应缓存中间件时,开发人员无法控制此缓存行为,因为该中间件遵循官方缓存规范。支持使用输出缓存以更好地控制服务器负载是ASP.NET Core未来版本的设计方案。
常用Cache-Control指令
指令 | 操作 |
---|---|
public |
缓存可以存储响应。 |
private |
响应不得由共享缓存存储。专用缓存可以存储和重用响应。 |
max-age |
客户端不接受期限大于指定秒数的响应。示例:max-age=60(60秒),max-age=2592000(1个月) |
no-cache |
请求时:缓存不能使用存储的响应来满足请求。源服务器重新生成客户端的响应,中间件更新其缓存中存储的响应。 响应时:响应不得用于未经源服务器验证的后续请求。 |
no-store |
请求时:缓存不得存储请求。 响应时:缓存不得存储任何部分的响应。 |
在缓存中发挥了作用的其他缓存标头
标头 | 函数 |
---|---|
Age |
在源服务器上生成或成功验证响应以来的估计时间量(以秒为单位)。 |
Expires |
响应被视为过时后的时间。 |
Pragma |
为与用于设置no-cache行为的HTTP/1.0缓存向后兼容而存在。如果Cache-Control标头存在,则将忽略Pragma标头。 |
Vary |
指定除非所有Vary标头字段在缓存响应的原始请求和新请求中都匹配,否则不得发送缓存响应。 |
启用响应缓存中间件
需要在Startup.cs
的ConfigureServices
的方法中注册响应缓存中间件AddResponseCaching
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// 添加响应缓存中间件
services.AddResponseCaching();
}
它还可以配置一些参数,进行更高级的设置
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// 添加响应缓存中间件
services.AddResponseCaching(responseCachingOptions =>
{
// 响应正文的最大可缓存大小,默认值64 * 1024 * 1024(64MB)
responseCachingOptions.MaximumBodySize = 64 * 1024 * 1024;
// 响应缓存中间件的大小限制,默认值100 * 1024 * 1024(100MB)
responseCachingOptions.SizeLimit = 100 * 1024 * 1024;
// 确定是否将响应缓存在区分大小写的路径上,默认值为false
responseCachingOptions.UseCaseSensitivePaths = true;
});
}
同时还需要在Configure
方法中使用响应缓存中间件UseResponseCaching
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseResponseCaching();
app.UseCors();
app.UseAuthorization();
当启用Cors
中间件的时候,ResponseCaching
的启用必须放在它前面。
响应缓存中间件使用的HTTP头
标头 | 详细信息 |
---|---|
Authorization |
如果标头存在,则不会缓存响应。 |
Cache-Control |
中间件仅考虑缓存通过public 缓存指令标记的响应。使用以下参数的控制缓存:max-age max-stale min-fresh must-revalidate no-cache no-store only-if-cached private public s-maxage proxy-revalidate |
Pragma |
请求中的Pragma:no-cache 标头具有与Cache-Control:no-cache 相同的作用。此标头由Cache-Control 标头中的相关指令(若存在)覆盖。考虑提供与HTTP/1.0的向后兼容性。 |
Set-Cookie |
如果标头存在,则不会缓存响应。请求处理管道中的任何中间件都阻止Cookie 响应缓存中间件,例如Cookie 基于TempData 提供程序缓存响应。 |
Vary |
Vary标头用于根据另一个标头更改缓存的响应。例如,通过包含Vary:Accept-Encoding 标头基于编码缓存响应,该标头分别缓存针对带有Accept-Encoding:gzip 和Accept-Encoding:text/plain 标头的请求的响应。永远不会存储标头值为* 的响应。 |
Expires |
不会存储或检索此标头认为过时的响应,除非被其他Cache-Control 标头覆盖。 |
If-None-Match |
如果值不是* 并且响应的ETag 与提供的任何值都不匹配,则从缓存中提供完整响应。否则,会提供304(Not Modified) 响应。 |
If-Modified-Since |
如果If-None-Match 标头不存在,则当缓存的响应日期晚于提供的值时,将从缓存中提供完整响应。否则,会提供304-Not Modified 响应。 |
Date |
从缓存提供服务时,如果原始响应中未提供Date 标头,则中间件会设置该标头。 |
Content-Length |
从缓存提供服务时,如果原始响应中未提供Content-Length 标头,则中间件会设置该标头。 |
Age |
会忽略原始响应中发送的Age 标头。中间件在提供缓存的响应时会计算一个新值。 |
使用内置内存缓存
核心对象
- IMemoryCache
启用内存缓存
依赖包
https://www.nuget.org/packages/Microsoft.Extensions.Caching.Memory
dotnet add package Microsoft.Extensions.Caching.Memory
不过这个包实际上在Microsoft.AspNetCore.App
中已经包含了
在Startup.cs
的ConfigureServices
方法中添加MemoryCache服务:AddMemoryCache
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// 开启内存缓存
services.AddMemoryCache();
// 添加响应缓存中间件
services.AddResponseCaching();
}
我们看下AddMemoryCache
定义
public static class MemoryCacheServiceCollectionExtensions
{
public static IServiceCollection AddMemoryCache(this IServiceCollection services);
public static IServiceCollection AddMemoryCache(this IServiceCollection services, Action<MemoryCacheOptions> setupAction);
}
其中MemoryCacheOptions
定义
public class MemoryCacheOptions : IOptions<MemoryCacheOptions>
{
public MemoryCacheOptions();
public ISystemClock Clock { get; set; }
//
// 摘要:
// Gets or sets the amount to compact the cache by when the maximum size is exceeded.
public double CompactionPercentage { get; set; }
//
// 摘要:
// Gets or sets the minimum length of time between successive scans for expired
// items.
public TimeSpan ExpirationScanFrequency { get; set; }
//
// 摘要:
// Gets or sets the maximum size of the cache.
public long? SizeLimit { get; set; }
}
使用内存缓存
我们来看下IMemoryCache
的定义
public interface IMemoryCache : IDisposable
{
ICacheEntry CreateEntry(object key);
void Remove(object key);
bool TryGetValue(object key, out object value);
}
而ICacheEntry
定义是
public interface ICacheEntry : IDisposable
{
DateTimeOffset? AbsoluteExpiration { get; set; }
TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }
IList<IChangeToken> ExpirationTokens { get; }
object Key { get; }
IList<PostEvictionCallbackRegistration> PostEvictionCallbacks { get; }
CacheItemPriority Priority { get; set; }
long? Size { get; set; }
TimeSpan? SlidingExpiration { get; set; }
object Value { get; set; }
}
它定义了缓存的Key、缓存的值、过期时间、滑动过期时间(多长时间不访问就失效),它还支持设计数据变更的更新、数据变更的监听、过期的监听。
可以通过IMemoryCache
的TryGetValue
来获取对应的Key值,然后通过Set
来设置。
/// <summary>
/// 获取订单
/// </summary>
/// <param name="memoryCache"></param>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> GetOrder([FromServices]IMemoryCache memoryCache, [FromServices] ILogger<OrderController> logger, [FromQuery] string id)
{
OrderModel order;
var key = $"GetOrder-{id ?? ""}";
if(!memoryCache.TryGetValue(key, out order))
{
order = new OrderModel
{
Id = id,
Date = DateTime.Now
};
var cacheEntryOptions = new MemoryCacheEntryOptions
{
// 滑动过期,多长时间不访问才失效,这里30秒,30秒不访问就失效
SlidingExpiration = TimeSpan.FromSeconds(30),
// 绝对到期,在滑动过期间隔内未请求该项时,到了60秒仍然会自动过期
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(60),
};
memoryCache.Set(key, order, cacheEntryOptions);
}
return await Task.FromResult(Ok(order));
}
还可以通过GetOrCreateAsync
来获取并创建,这里我们可以在cacheEntry
逻辑中从数据库去拉取数据。
/// <summary>
/// 获取订单
/// </summary>
/// <param name="memoryCache"></param>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> GetOrderV2([FromServices] IMemoryCache memoryCache, [FromServices] ILogger<OrderController> logger, [FromQuery] string id)
{
var key = $"GetOrder-{id ?? ""}";
var order = await memoryCache.GetOrCreateAsync(key, cacheEntry => {
cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(30);
return Task.FromResult(new OrderModel
{
Id = id,
Date = DateTime.Now
});
});
return await Task.FromResult(Ok(order));
}
我们来看下MemoryCacheEntryOptions
的定义
public class MemoryCacheEntryOptions
{
public MemoryCacheEntryOptions();
public DateTimeOffset? AbsoluteExpiration { get; set; }
public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }
public IList<IChangeToken> ExpirationTokens { get; }
public IList<PostEvictionCallbackRegistration> PostEvictionCallbacks { get; }
public CacheItemPriority Priority { get; set; }
public long? Size { get; set; }
public TimeSpan? SlidingExpiration { get; set; }
}
其中SlidingExpiration
代表滑动过期,多长时间不访问才失效,这里30秒,30秒不访问就失效,但是有可能用户一直来访问,那怎么办,可以结合AbsoluteExpirationRelativeToNow
绝对到期时间来设置,这个缓存在滑动过期间隔内未请求该项时,允许设置提前过期的时间。
需要注意是,一般滑动过期时间应小于绝对到期时间。
运行下看看效果
首次访问,没有缓存,进入创建逻辑
第二次访问,命中缓存,直接返回
通过响应缓存中间件使用
同时我们在Configure
方法中启用ResponseCache中间件: UseResponseCaching
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseResponseCaching();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
ResponseCache中间件机制和身份认证是冲突的,包含了身份认证头的请求,实际上是不支持ResponseCache的。
在Controller的层面,在Action上使用ResponseCache
就行。
[Route("api/[controller]/[action]")]
[ApiController]
public class OrderController : ControllerBase
{
/// <summary>
/// 获取订单
/// 缓存过期时间6000秒,缓存Key的生成策略是基于id的值,不同id的值会缓存为不同的Cache
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[ResponseCache(Duration = 6000, VaryByQueryKeys = new string[] { "id" })]
[HttpGet]
public OrderModel GetOrder([FromQuery] string id)
{
return new OrderModel { Id = id, Date = DateTime.Now };
}
/// <summary>
/// 获取地址
/// 缓存过期时间6000秒,缓存Key的生成策略是名为rpc的Header,这个Header不同的值就会缓存不同的Cache
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[ResponseCache(Duration = 6000, VaryByHeader = "rpc")]
[HttpGet]
public OrderModel GetAddress([FromQuery] string id)
{
return new OrderModel { Id = id, Date = DateTime.Now };
}
}
基于查询参数来生成缓存
有了前面的各种缓存组件加持之后,我们就可以通过响应缓存中间件来首先我们的接口缓存功能了。
[Route("api/[controller]/[action]")]
[ApiController]
public class OrderController : ControllerBase
{
/// <summary>
/// 获取订单
/// 缓存过期时间6000秒,缓存Key的生成策略是基于id的值,不同id的值会缓存为不同的Cache
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[ResponseCache(Duration = 6000, VaryByQueryKeys = new string[] { "id" })]
[HttpGet]
public OrderModel GetOrder([FromQuery] string id)
{
return new OrderModel { Id = id, Date = DateTime.Now };
}
}
这里我们通过Attribute的方式可以直接在Action上面使用ResponseCache
,它可以设置一系列参数,比如缓存过期时间Duration
、缓存Key生成策略(基于查询参数,VaryByQueryKeys
)
我们在Postman里面测试下
第一次会进入响应
这时候它会返回一些头信息
其中关键的是
Cache-Control=public,max-age=6000
这会告诉客户端,这个结果是可以缓存的,并且超时时间是6000秒。
当我们同样的参数第二次请求是,发现它就不进去了,直接返回了同样的内容
基于请求头来生成缓存
我们来看下
ResponseCacheAttribute
定义
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ResponseCacheAttribute : Attribute, IFilterFactory, IFilterMetadata, IOrderedFilter
{
public ResponseCacheAttribute();
public string CacheProfileName { get; set; }
public int Duration { get; set; }
public bool IsReusable { get; }
public ResponseCacheLocation Location { get; set; }
public bool NoStore { get; set; }
public int Order { get; set; }
public string VaryByHeader { get; set; }
public string[] VaryByQueryKeys { get; set; }
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider);
public CacheProfile GetCacheProfile(MvcOptions options);
}
这里面CacheProfileName
意味着我们可以指定缓存配置名称,方便先定义好,再根据配置选择。
其中缓存位置Location
的类型是ResponseCacheLocation
,默认是Any
,代表缓存可以缓存在浏览器、允许缓存在反向代理中,也允许缓存在服务器内存中,如果Client
代表只允许缓存存储在我们浏览器中,如果None
就表示说没有缓存。
public enum ResponseCacheLocation
{
//
// 摘要:
// Cached in both proxies and client. Sets "Cache-control" header to "public".
Any = 0,
//
// 摘要:
// Cached only in the client. Sets "Cache-control" header to "private".
Client = 1,
//
// 摘要:
// "Cache-control" and "Pragma" headers are set to "no-cache".
None = 2
}
参数中还有VaryByHeader
与VaryByQueryKeys
实际上是一样的,它指基于不同的Header的值。
[Route("api/[controller]/[action]")]
[ApiController]
public class OrderController : ControllerBase
{
/// <summary>
/// 获取地址
/// 缓存过期时间6000秒,缓存Key的生成策略是名为rpc的Header,这个Header不同的值就会缓存不同的Cache
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[ResponseCache(Duration = 6000, VaryByHeader = "rpc")]
[HttpGet]
public OrderModel GetAddress([FromQuery] string id)
{
return new OrderModel { Id = id, Date = DateTime.Now };
}
}
使用Redis分布式缓存
什么是Redis
Redis是一种开放源代码内存中数据存储,通常用作分布式缓存。
Redis(Remote Dictionary Server, 远程键值服务)是一个内存数据结构存储,用作分布式内存键值数据库、缓存和消息代理,具有可选的耐久性。
Redis支持不同种类的抽象数据结构,如字符串、列表、地图、集、排序集、HyperLogLogs、位图、流和空间索引。该项目由Salvatore Sanfilippo开发和维护,始于2009年。从2015年到2020年,他带领一个由Redis实验室赞助的项目核心团队。Salvatore Sanfilippo在2020年离开了Redis的维护者。它是在BSD3-clause许可证下发布的开源软件。2021年,在原作者和主要维护者离开后不久,Redis实验室从其名称中删除了实验室,现在只被称为"Redis"。
Redis经常被称为数据结构服务器(data structures server)。这意味着Redis通过一组命令提供对可变数据结构的访问,这些命令使用TCP套接字和一个简单协议的服务器-客户模型发送。因此,不同的进程可以以一种共享的方式查询和修改相同的数据结构。
在Redis中实现的数据结构有一些特殊的属性。
- Redis关心的是将它们存储在磁盘上,即使它们总是被送达和修改到服务器内存中。这意味着Redis是快速的,但它也是非易失性的。
- 数据结构的实现强调内存效率,因此与使用高级编程语言建模的相同数据结构相比,Redis内的数据结构可能会使用更少的内存。
- Redis提供了许多在数据库中自然找到的功能,如复制、可调整的耐久性水平、集群和高可用性。
另一个很好的例子是把Redis看作是一个更复杂的memcached版本,其中的操作不仅仅是SET和GET,而是操作复杂的数据类型,如Lists、Sets、有序数据结构等等。
Redis是用ANSI C
编写的,可以在大多数POSIX系统上工作,如Linux
、*BSD
和Mac OS X
,没有外部依赖性。Linux和OS X是Redis开发和测试最多的两个操作系统,我们推荐使用Linux进行部署。Redis可能在Solaris衍生的系统(如SmartOS)中工作,但支持是尽力而为。对于Windows的构建,没有官方支持。
历史
Redis这个名字的意思是远程字典服务器。Redis项目开始于Salvatore Sanfilippo,绰号antirez,Redis的最初开发者,试图提高他的意大利创业公司的可扩展性,开发一个实时网络日志分析器。在使用传统数据库系统扩展某些类型的工作负载时遇到重大问题后,Sanfilippo于2009年开始在Tcl中制作Redis的第一个概念验证版本的原型。在内部成功使用该项目几周后,Sanfilippo决定将其开源,并在HackerNews上公布了该项目。该项目开始受到关注,尤其是在Ruby社区,GitHub和Instagram是第一批采用该项目的公司。
2010年3月,Sanfilippo被VMware公司聘用。
2013年5月,Redis被Pivotal软件公司(VMware的子公司)赞助。
2015年6月,开发工作由Redis实验室赞助。
2018年10月,Redis5.0发布,引入了RedisStream--一种新的数据结构,允许在一个键上以自动的、基于时间的顺序存储多个字段和字符串值。
2020年6月,Salvatore Sanfilippo卸任Redis维护者。
与其他数据库系统的区别
Redis普及了一个可以同时被认为是存储和缓存的系统的想法。它的设计使数据总是被修改并从计算机主内存中读取,但也以不适合随机数据访问的格式存储在磁盘上。格式化的数据只有在系统重新启动后才会被重建到内存中。
Redis还提供了一个与关系型数据库管理系统(RDBMS)相比非常不寻常的数据模型。用户命令并不描述数据库引擎要执行的查询,而是描述对给定的抽象数据类型进行的具体操作。因此,数据必须以一种适合以后快速检索的方式来存储。检索是在没有数据库系统的帮助下,以二级索引、聚合或其他传统RDBMS的常见功能的形式进行的。Redis的实现大量使用fork系统调用,以复制持有数据的进程,这样父进程继续为客户服务,而子进程在磁盘上创建数据的内存副本。
数据类型
Redis将键映射为值的类型。Redis与其他结构化存储系统的一个重要区别是,Redis不仅支持字符串,而且还支持抽象数据类型。
- 字符串的列表(Lists of strings)
- 字符串集(Sets of strings),不重复的未排序元素的集合
- 经过排序的字符串集(Sorted sets of strings),由非重复元素组成的集合,通过一个名为score的浮点数字排序
- 哈希表(Hash tables),键和值都是字符串
- HyperLogLogs用于近似估计集合的cardinality大小,从2014年4月的Redis2.8.9开始提供。
- 带有消费者组的条目流(Stream of entries with consumer groups),允许你以自动的、基于时间的序列在单个键上存储多个字段和字符串值,自2018年10月的Redis5.0起可用。
- 通过实现geohash技术的地理空间数据(Geospatial data),自Redis3.2起可用。
一个值的类型决定了该值可以进行哪些操作(称为命令)。Redis支持高级的、原子的、服务器端的操作,如集合之间的交叉、联合和差异,以及列表、集合和排序集合的排序。
基于Redis模块的API,支持更多的数据类型:
JSON
- RedisJSON实现ECMA-404(JavaScript对象符号数据交换标准)作为本地数据类型。Graph
- RedisGraph实现了一个可查询的属性图形Time series
- RedisTimeSeries实现了一个时间序列数据结构- Bloom filter,Cuckoo filter,Count-min sketch和Top-K-RedisBloom为Redis实现了一套概率数据结构。
使用者
由于数据库设计的性质,典型的用例是会话缓存、全页面缓存、消息队列应用、排行榜和计数等等。发布-订阅的消息传递模式允许服务器之间的实时通信。
大型公司如Twitter正在使用Redis,亚马逊网络服务提供了一个名为ElastiCache的Redis管理服务,微软在Azure中为Redis提供AzureCache,阿里巴巴在阿里云中为Redis提供ApsaraDB。
使用Docker新建Redis实例
其中一些标签中可能有类似bullseye的名称。这些是Debian版本的套件代码名,并指示映像基于哪个版本
docker run -d --name redis --restart unless-stopped -p 6379:6379 redis:6.2.7 redis-server --requirepass xxxxxxxxx
docker exec -it redis /bin/bash
运行redis-cli
,可以执行常用动作
通过Another Redis DeskTop Manager连接Redis实例
https://github.com/qishibo/AnotherRedisDesktopManager/releases
更快、更好、更稳定的Redis桌面(GUI)管理客户端,兼容Windows、Mac、Linux,性能出众,轻松加载海量键值
Redis DeskTop Manager自从进入了0.9.9版本就开始付费使用或者贡献代码获得免费使用期限,可以转用Another Redis DeskTop Manager
获取Another Redis DeskTop Manager
补一个Redis DeskTop Manager的截图,获取老版本: Redis DeskTop Manager v0.8.8
启用Redis分布式缓存
依赖包
https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
在Startup.cs
的ConfigureServices
方法中添加RedisCache服务:AddStackExchangeRedisCache
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// 添加RedisCache服务
services.AddStackExchangeRedisCache(redisCacheOptions =>
{
Configuration.GetSection("Redis").Bind(redisCacheOptions);
});
// 添加响应缓存中间件
services.AddResponseCaching();
}
这里注意到,还添加了响应Cache服务AddResponseCaching
,它对应传统MVC应用中OutputCache
。
这里我们查看下RedisCacheOptions
的定义,
public class RedisCacheOptions : IOptions<RedisCacheOptions>
{
public string Configuration { get; set; }
public ConfigurationOptions ConfigurationOptions { get; set; }
/// <summary>
/// Gets or sets a delegate to create the ConnectionMultiplexer instance.
/// </summary>
public Func<Task<IConnectionMultiplexer>> ConnectionMultiplexerFactory { get; set; }
public string InstanceName { get; set; }
public Func<ProfilingSession> ProfilingSession { get; set; }
RedisCacheOptions IOptions<RedisCacheOptions>.Value
{
get { return this; }
}
}
它有两个字段,配置地址Configuration
和实例名称InstanceName
所以,我们还需要在配置文件appsettings.json
中配置Redis
的节点内容
{
"Redis": {
"Configuration": "localhost:6379,password=xxxxxxxxxxxxx",
"InstanceName": "CachingRedis31"
}
}
如果Redis设置了密码,记得在
Configuration
中追加,password=xxxxxxxxxxxxx
。
使用Redis分布式缓存
我们看下AddStackExchangeRedisCache
的定义
public static class StackExchangeRedisCacheServiceCollectionExtensions
{
/// <summary>
/// Adds Redis distributed caching services to the specified <see cref="IServiceCollection" />.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
/// <param name="setupAction">An <see cref="Action{RedisCacheOptions}"/> to configure the provided
/// <see cref="RedisCacheOptions"/>.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddStackExchangeRedisCache(this IServiceCollection services, Action<RedisCacheOptions> setupAction)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (setupAction == null)
{
throw new ArgumentNullException(nameof(setupAction));
}
services.AddOptions();
services.Configure(setupAction);
services.Add(ServiceDescriptor.Singleton<IDistributedCache, RedisCache>());
return services;
}
}
最后一句,我们看到,它将RedisCache
注册成了IDistributedCache
接口的实现,也就意味着我们可以通过IDistributedCache
来操作我们的Redis分布式缓存。
我们来看下IDistributedCache
的定义
public interface IDistributedCache
{
byte[] Get(string key);
Task<byte[]> GetAsync(string key, CancellationToken token = default(CancellationToken));
void Set(string key, byte[] value, DistributedCacheEntryOptions options);
Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default(CancellationToken));
void Refresh(string key);
Task RefreshAsync(string key, CancellationToken token = default(CancellationToken));
void Remove(string key);
Task RemoveAsync(string key, CancellationToken token = default(CancellationToken));
}
ASP.NE TCore中内置了IDistributedCache
的两个实现。一个是基于SQLServer,另一个是基于Redis
接下来,我们回到OrderController
来使用它,纯粹为了临时演示,这里的IDistributedCache
、ILogger<OrderController>
都是通过FromServices
的方式来从容器中获取实例。
/// <summary>
/// 获取订单
/// </summary>
/// <param name="distributedCache"></param>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> GetOrder([FromServices]IDistributedCache distributedCache, [FromServices]ILogger<OrderController> logger, [FromQuery] string id)
{
OrderModel order = null;
var key = $"GetOrder-{id ?? ""}";
var value = await distributedCache.GetStringAsync(key);
if (string.IsNullOrEmpty(value))
{
var option = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(60)
};
order = new OrderModel
{
Id = id,
Date = DateTime.Now
};
await distributedCache.SetStringAsync(key, JsonConvert.SerializeObject(order), option);
}
if (!string.IsNullOrEmpty(value))
{
try
{
order = JsonConvert.DeserializeObject<OrderModel>(value);
}
catch (Exception ex)
{
logger.LogError(ex, "反序列化失败");
}
}
return Ok(order);
}
通过IDistributedCache
的GetStringAsync
方法,我们可以轻松的获取到Redis存储的数据值,同时SetStringAsync
也可以很方便的写值,同时通过DistributedCacheEntryOptions
可以设置缓存值的策略。
public class DistributedCacheEntryOptions
{
private DateTimeOffset? _absoluteExpiration;
private TimeSpan? _absoluteExpirationRelativeToNow;
private TimeSpan? _slidingExpiration;
/// <summary>
/// Gets or sets an absolute expiration date for the cache entry.
/// </summary>
public DateTimeOffset? AbsoluteExpiration
{
get
{
return _absoluteExpiration;
}
set
{
_absoluteExpiration = value;
}
}
/// <summary>
/// Gets or sets an absolute expiration time, relative to now.
/// </summary>
public TimeSpan? AbsoluteExpirationRelativeToNow
{
get
{
return _absoluteExpirationRelativeToNow;
}
set
{
if (value <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(
nameof(AbsoluteExpirationRelativeToNow),
value,
"The relative expiration value must be positive.");
}
_absoluteExpirationRelativeToNow = value;
}
}
/// <summary>
/// Gets or sets how long a cache entry can be inactive (e.g. not accessed) before it will be removed.
/// This will not extend the entry lifetime beyond the absolute expiration (if set).
/// </summary>
public TimeSpan? SlidingExpiration
{
get
{
return _slidingExpiration;
}
set
{
if (value <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(
nameof(SlidingExpiration),
value,
"The sliding expiration value must be positive.");
}
_slidingExpiration = value;
}
}
}
实际上运行效果
缓存策略
实际上,我们还可以在这里去设计一些二级缓存来去避免缓存雪崩的情况、缓存穿透的情况。
使用EasyCaching缓存
什么是EasyCaching
EasyCaching是一个开源的缓存库,它包含了缓存的基本用法和一些高级用法,可以帮助我们更容易地处理缓存问题。
启用EasyCaching缓存
依赖包
dotnet add package EasyCaching.Redis
dornet add package EasyCaching.Serialization.Json
dotnet add package Microsoft.Bcl.AsyncInterfaces
在Startup.cs
的ConfigureServices
方法中添加EasyCaching服务: AddEasyCaching
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// 添加EasyCaching服务
services.AddEasyCaching(easyCachingOptions =>
{
easyCachingOptions.UseRedis(Configuration, name: "EasyCaching").WithJson("redis");
});
}
这里需要添加它对应的配置信息
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"EasyCaching": {
"redis": {
"MaxRdSecond": 120,
"EnableLogging": false,
"LockMs": 5000,
"SleepMs": 300,
"SerializerName": "redis",
"dbconfig": {
"Password": "xxxxxxxxxxxx",
"IsSsl": false,
"SslHost": null,
"ConnectionTimeout": 5000,
"AllowAdmin": true,
"Endpoints": [
{
"Host": "localhost",
"Port": 6379
}
],
"Database": 0
}
}
}
}
这里特别注意的是,要设置好
SerializerName
,它的名字要和前面的WithJson
方法指定的一样,不然会引发错误:Can not find the matched serializer instance, serializer name is redis1 #370,报错:Can not find the matched serializer instance
如果继续使用的时候,报错
Could not load file or assembly 'Microsoft.Bcl.AsyncInterfaces
,那么需要补装下这个包。
这里的
MaxRdSecond
作用是预防在同一时刻出现大批量缓存同时失效,为每个key原有的过期时间上面加了一个随机的秒数,尽可能的分散它们的过期时间
接下来,我们可以去OrderController
来添加使用的方法。
[Route("api/[controller]/[action]")]
[ApiController]
public class OrderController : ControllerBase
{
private readonly IEasyCachingProvider _provider;
public OrderController(IEasyCachingProvider provider)
{
this._provider = provider;
}
/// <summary>
/// 获取模型
/// </summary>
/// <param name="easyCachingProvider"></param>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public IActionResult GetModel([FromQuery] string id)
{
var key = $"GetModel-{id ?? ""}";
// 获取这个Key的值,默认值直接返回当前时间,过期时间为60秒
var value = _provider.Get(key, () => DateTime.Now.ToString(), TimeSpan.FromSeconds(60));
return Content(value.Value);
}
}
这里我们设计了一个GetModel
的接口,通过传入的Id+自定义的前缀来生成Key值,通过IEasyCachingProvider
来获取Key值即可。
我们还可以看看其他方法,并且将这里改成异步的
/// <summary>
/// 获取模型
/// </summary>
/// <param name="easyCachingProvider"></param>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> GetModelV2([FromQuery] string id)
{
var key = $"GetModel-{id ?? ""}";
// 获取缓存key的值,没有默认值
var value = await _provider.GetAsync<string>(key);
// 获取缓存key的值,默认值返回当前时间,过期时间一分钟
var value2 = await _provider.GetAsync(key, async () => await Task.FromResult(DateTime.Now.ToString()), TimeSpan.FromMinutes(1));
// 设置缓存Key=demo,value=123,过期时间为一分钟
await _provider.SetAsync("demo", "123", TimeSpan.FromMinutes(1));
// 删除缓存key=demo
await _provider.RemoveAsync("demo");
return Content(value2.Value);
}
如果用的Get方法是带有查询的,它在没有命中缓存的情况下去数据库查询前,会有一个加锁操作,避免一个key在同一时刻去查了n次数据库,这个锁的生存时间和休眠时间是由配置中的LockMs和SleepMs决定的
多实例缓存
当存在多个缓存实例需要支持的时候,我们可以使用IEasyCachingProviderFactory
工程模式来获取IEasyCachingProvider
先从配置文件配置多个来区分,这里配置了两个redis节点:EasyCaching
、DefaultRedis
{
"EasyCaching": {
"redis": {
"MaxRdSecond": 120,
"EnableLogging": false,
"LockMs": 5000,
"SleepMs": 300,
"SerializerName": "redis",
"dbconfig": {
"Password": "xxxx",
"IsSsl": false,
"SslHost": null,
"ConnectionTimeout": 5000,
"AllowAdmin": true,
"Endpoints": [
{
"Host": "localhost",
"Port": 6379
}
],
"Database": 0
}
}
},
"DefaultRedis": {
"redis": {
"MaxRdSecond": 120,
"EnableLogging": false,
"LockMs": 5000,
"SleepMs": 300,
"SerializerName": "redis",
"dbconfig": {
"Password": "xxxxxxxxxx",
"IsSsl": false,
"SslHost": null,
"ConnectionTimeout": 5000,
"AllowAdmin": true,
"Endpoints": [
{
"Host": "localhost",
"Port": 6379
}
],
"Database": 1
}
}
}
}
然后在Controller
中,我们通过IEasyCachingProviderFactory
来根据配置名称获取实例GetCachingProvider
[Route("api/[controller]/[action]")]
[ApiController]
public class OrderController : ControllerBase
{
private readonly IEasyCachingProvider _defaultProvider;
private readonly IEasyCachingProvider _easyProvider;
public OrderController(IEasyCachingProviderFactory factory)
{
this._defaultProvider = factory.GetCachingProvider("DefaultRedis");
this._easyProvider = factory.GetCachingProvider("EasyCaching");
}
/// <summary>
/// 获取模型
/// </summary>
/// <param name="easyCachingProvider"></param>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> GetModelV3([FromQuery] string id)
{
var key = $"GetModel-{id ?? ""}";
// 获取缓存key的值,没有默认值
var value = await _defaultProvider.GetAsync<string>(key);
// 获取缓存key的值,默认值返回当前时间,过期时间一分钟
var value2 = await _defaultProvider.GetAsync(key, async () => await Task.FromResult(DateTime.Now.ToString()), TimeSpan.FromMinutes(1));
// 设置缓存Key=demo,value=123,过期时间为一分钟
await _easyProvider.SetAsync("demo", "123", TimeSpan.FromMinutes(1));
// 删除缓存key=demo
//await _easyProvider.RemoveAsync("demo");
return Content(value2.Value);
}
}
AOP操作
依赖包
https://www.nuget.org/packages/EasyCaching.Interceptor.AspectCore
dotnet add package EasyCaching.Interceptor.AspectCore
public interface IOrderService
{
//[EasyCachingAble(Expiration = 30, CacheKeyPrefix = "GetOrder-", CacheProviderName = "DefaultRedis")]
[EasyCachingAble(Expiration = 30)]
Task<OrderModel> GetOrderAsync(string id);
}
public class OrderService : IOrderService
{
public Task<OrderModel> GetOrderAsync(string id)
{
return Task.FromResult(new OrderModel { Id = id, Date = DateTime.Now });
}
}
需要在ConfigureService
增加下中间件配置
services.ConfigureAspectCoreInterceptor(options =>
{
// 可以在这里指定你要用那个provider
// 或者在Attribute上面指定
options.CacheProviderName = "DefaultRedis";
});
在Controller
这里添加调用的接口
/// <summary>
/// 获取模型
/// </summary>
/// <param name="easyCachingProvider"></param>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> GetOrderV2([FromServices]IOrderService orderService, [FromQuery] string id)
{
var order = await orderService.GetOrderAsync(id);
return Ok(order);
}
多级缓存
依赖包
dotnet add package EasyCaching.InMemory
dotnet add package EasyCaching.HybridCache
dotnet add package EasyCaching.Bus.Redis
services.AddEasyCaching(easyCachingOptions =>
{
easyCachingOptions.UseInMemory("m1");
easyCachingOptions.UseRedis(Configuration, name: "EasyCaching").WithJson("redis");
easyCachingOptions.UseHybrid(options =>
{
options.EnableLogging = true;
// 缓存总线的订阅主题
options.TopicName = "test_topic";
// 本地缓存的名字
options.LocalCacheProviderName = "m1";
// 分布式缓存的名字
options.DistributedCacheProviderName = "EasyCaching";
});
easyCachingOptions.WithRedisBus(Configuration, name: "DefaultRedis");
});
这里使用redis作为缓存总线。
接下来就是通过IHybridCachingProvider
使用它
/// <summary>
/// 获取模型
/// </summary>
/// <param name="easyCachingProvider"></param>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> GetHybird([FromServices] IHybridCachingProvider hybridCachingProvider, [FromQuery] string id)
{
var key = $"GetModel-{id ?? ""}";
var value = await hybridCachingProvider.GetAsync<string>(key, async () => await Task.FromResult(DateTime.Now.ToString()), TimeSpan.FromMinutes(1));
return Ok(value.Value);
}
专供Redis的模式
Redis支持多种数据结构,还有一些原子递增递减的操作等等。为了支持这些操作,EasyCaching提供了一个独立的接口,IRedisCachingProvider
/// <summary>
/// 获取模型
/// </summary>
/// <param name="easyCachingProvider"></param>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> GetRedis([FromServices] IEasyCachingProviderFactory factory, [FromQuery] string id)
{
var key = $"GetModel-{id ?? ""}";
var redisProvider = factory.GetRedisProvider("EasyCaching");
var value = await redisProvider.StringGetAsync(key);
if(value == null)
{
await redisProvider.StringSetAsync(key, DateTime.Now.ToString(), TimeSpan.FromMinutes(1));
value = await redisProvider.StringGetAsync(key);
}
return Ok(value);
}
使用SQL Server分布式缓存
什么是SQL Server分布式缓存
所谓的针对SQL Server的分布式缓存,实际上就是将标识缓存数据的字节数组存放在SQL Server数据库中某个具有固定结构的数据表中
使用SQL Server分布式缓存
依赖包
https://www.nuget.org/packages/Microsoft.Extensions.Caching.SqlServer
dotnet add package Microsoft.Extensions.Caching.SqlServer
使用NCahe分布式缓存
什么是NCache
NCache是在.NET和.NET Core中以原生方式开发的开放源代码内存中分布式缓存。
NCache是一个速度极快、可扩展的开源分布式缓存,适用于.NET应用程序。使用NCache进行数据库缓存、ASP.NET会话状态存储、ASP.NET视图状态缓存,以及更多。
NCache被世界各地的数百家公司用于关键任务的应用。
功能
- 发布/订阅(Pub/Sub)与主题
- 缓存CRUD操作
- 批量CRUD操作
- 锁定/解锁缓存项目
- 项目级别的事件通知
- 驱逐
- 绝对和滑动过期
- ASP.NET会话状态提供者
- ASP.NET视图状态缓存
- 镜像缓存拓扑结构
- NHibernate二级缓存提供者
- NuGet包
- 可在微软Azure、亚马逊和其他任何云平台上运行
使用Memcached分布式缓存
什么是Memcached
Memcached(发音为mem-cash-dee或mem-cashed)是一个通用的分布式内存缓存系统。它经常被用来加速动态数据库驱动的网站,通过在RAM中缓存数据和对象来减少必须读取外部数据源(如数据库或API)的次数。Memcached是免费的开源软件,在修订的BSD许可证下授权。Memcached在类Unix操作系统(Linux和macOS)和微软Windows上运行。它依赖于libevent库。
Memcached的API提供了一个分布在多台机器上的非常大的哈希表。当表满的时候,后续的插入会导致较旧的数据以最近使用(LRU)的顺序被清除。使用Memcached的应用程序通常会将请求和添加分层到RAM中,然后再回落到一个较慢的备份存储,如数据库。
Memcached没有内部机制来跟踪可能发生的失误。然而,一些第三方的工具提供了这个功能。
Memcached最初是由Brad Fitzpatrick于2003年5月22日为他的网站Live Journal开发的。它最初是用Perl编写的,后来由Anatoly Vorobey用C语言重写,然后被Live Journal聘用。Memcached现在被许多其他系统使用,包括YouTube、Reddit、Facebook、Pinterest、Twitter、Wikipedia和Method Studios。
使用Docker新建Memcached实例
docker run -d --name memcached --restart unless-stopped -p 11211:11211 memcached:1.6.17
其中Memcached默认的最大容量是64mb,也可以通过参数-m 64
来设置大小。
需要设置账号密码的话,可以使用-e MEMCACHED_USERNAME=$username -e MEMCACHED_PASSWORD=$password
启用Memcached缓存
这里我们使用一个来自博客园团队维护的组件:EnyimMemcachedCore
依赖包
dotnet add package EnyimMemcachedCore
配置文件appsettings.json
中添加配置节点Memcached
{
"Memcached": {
"Servers": [
{
"Address": "localhost",
"Port": 11211
}
]
}
}
如果Memcached带密码就写成
{
"Memcached": {
"Servers": [
{
"Address": "localhost",
"Port": 11211
}
],
"Authentication": {
"Type": "Enyim.Caching.Memcached.PlainTextAuthenticator",
"Parameters": {
"zone": "",
"userName": "$username",
"password": "$password"
}
}
}
}
接下来,在Startup.cs
中ConfigureServices
方法中添加对它的支持。
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// 添加EnyimMemcached服务
services.AddEnyimMemcached(options =>
{
Configuration.GetSection("Memcached").Bind(options);
});
}
这里我们多看下MemcachedClientOptions
的定义
public class MemcachedClientOptions : IOptions<MemcachedClientOptions>
{
public MemcachedProtocol Protocol { get; set; }
public SocketPoolOptions SocketPool { get; set; }
public List<Server> Servers { get; set; } = new List<Server>();
public Authentication Authentication { get; set; }
public string KeyTransformer { get; set; }
public string Transcoder { get; set; }
public bool UseSslStream { get; set; }
public IProviderFactory<IMemcachedNodeLocator> NodeLocatorFactory { get; set; }
public MemcachedClientOptions Value => this;
public void AddServer(string address, int port)
{
Servers.Add(new Server
{
Address = address,
Port = port
});
}
public void AddDefaultServer()
{
AddServer("memcached", 11211);
}
public void AddPlainTextAuthenticator(string zone, string userName, string password)
{
Authentication = new Authentication
{
Type = typeof(PlainTextAuthenticator).ToString(),
Parameters = new Dictionary<string, string>
{
{ "zone", zone },
{ "userName", userName },
{ "password", password }
}
};
}
}
其中Server
定义
public class Server
{
public string Address { get; set; }
public int Port { get; set; }
}
其中Authentication
定义
public class Authentication
{
public string Type { get; set; }
public Dictionary<string, string> Parameters { get; set; }
}
使用Memcached缓存
我们来看下AddEnyimMemcached
定义
public static class EnyimMemcachedServiceCollectionExtensions
{
public static IServiceCollection AddEnyimMemcached(this IServiceCollection services)
{
return AddEnyimMemcachedInternal(services, null);
}
public static IServiceCollection AddEnyimMemcached(this IServiceCollection services, Action<MemcachedClientOptions> setupAction)
{
if (services == null)
{
throw new ArgumentNullException("services");
}
if (setupAction == null)
{
throw new ArgumentNullException("setupAction");
}
return AddEnyimMemcachedInternal(services, delegate (IServiceCollection s)
{
s.Configure(setupAction);
});
}
private static IServiceCollection AddEnyimMemcachedInternal(IServiceCollection services, Action<IServiceCollection> configure)
{
services.AddOptions();
configure?.Invoke(services);
services.TryAddSingleton<ITranscoder, DefaultTranscoder>();
services.TryAddSingleton<IMemcachedKeyTransformer, DefaultKeyTransformer>();
services.TryAddSingleton<IMemcachedClientConfiguration, MemcachedClientConfiguration>();
services.AddSingleton<MemcachedClient>();
services.AddSingleton((Func<IServiceProvider, IMemcachedClient>)((IServiceProvider factory) => factory.GetService<MemcachedClient>()));
services.AddSingleton((Func<IServiceProvider, IDistributedCache>)((IServiceProvider factory) => factory.GetService<MemcachedClient>()));
return services;
}
}
这里我们看到,它通过工厂模式注册了一个MemcachedClient
为IMemcachedClient
、IDistributedCache
的实现。
首先我们来通过
IMemcachedClient
使用看看
/// <summary>
/// 获取订单
/// </summary>
/// <param name="memoryCache"></param>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> GetOrder([FromServices] IMemcachedClient memcachedClient, [FromServices] ILogger<OrderController> logger, [FromQuery] string id)
{
var key = $"GetOrder-{id ?? ""}";
var order = await memcachedClient.GetValueOrCreateAsync(key, cacheSeconds:30, async () =>
{
return await Task.FromResult(new OrderModel
{
Id = id,
Date = DateTime.Now
});
});
return await Task.FromResult(Ok(order));
}
这里使用了IMemcachedClient
的GetValueOrCreateAsync
获取或者创建一个缓存值,使用起来非常简单。
首先我们来通过
IDistributedCache
使用看看
/// <summary>
/// 获取订单
/// </summary>
/// <param name="distributedCache"></param>
/// <param name="logger"></param>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> GetOrders([FromServices] IDistributedCache distributedCache, [FromServices] ILogger<OrderController> logger, [FromQuery] string id)
{
// 生成Key
var key = $"GetOrders-{id ?? ""}";
List<OrderModel> orders = null;
// 根据Key获取缓存值
var orderJson = await distributedCache.GetStringAsync(key);
if (string.IsNullOrEmpty(orderJson))
{
orders = new List<OrderModel>()
{
new OrderModel
{
Id = id,
Date = DateTime.Now
}
};
var distributedCacheOptions = new DistributedCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromSeconds(30),
};
orderJson = JsonConvert.SerializeObject(orders);
// 根据Key写入缓存值
await distributedCache.SetStringAsync(key, orderJson, distributedCacheOptions);
}
else
{
try
{
orders = JsonConvert.DeserializeObject<List<OrderModel>>(orderJson);
}
catch (Exception ex)
{
logger.LogError(ex, "DeserializeObject OrderJson Failure");
}
}
return Ok(orders);
}
这里使用IDistributedCache
的GetStringAsync
和SetStringAsync
来读取和写入数据。
对象池重用对象
什么是对象池
Microsoft.Extensions.ObjectPool
是ASP.NET Core基础结构的一部分,它支持将一组对象保留在内存中以供重用,而不是允许对对象进行垃圾回收。
如果要管理的对象具有以下特征,你可能希望使用对象池:
- 分配/初始化成本高昂。
- 表示某些有限资源。
- 可预见地频繁使用。
例如,ASP.NET Core框架在某些地方使用对象池来重用StringBuilder
实例。StringBuilder
分配并管理自己的缓冲区来保存字符数据。ASP.NET Core经常使用StringBuilder
来实现功能,重用这些对象会带来性能优势。
对象池并不总是能提高性能:
- 除非对象的初始化成本很高,否则从池中获取对象通常较慢。
- 在池解除分配之前,池管理的对象无法解除分配。
- 仅在使用应用或库的真实场景收集性能数据后才使用对象池。
注意:ObjectPool
不限制将分配的对象数量,但限制将保留的对象数量。
使用对象池
依赖包
https://www.nuget.org/packages/Microsoft.Extensions.ObjectPool
dotnet add package Microsoft.Extensions.ObjectPool
注册对象池提供服务到容器,添加针对StringBuilder的对象池策略。
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// 注册对象池提供服务到容器
services.TryAddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
// 添加针对StringBuilder的对象池策略
services.TryAddSingleton<ObjectPool<StringBuilder>>(serviceProvider =>
{
var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
var policy = new StringBuilderPooledObjectPolicy();
return provider.Create(policy);
});
}
使用它
[Route("api/[controller]/[action]")]
[ApiController]
public class OrderController : ControllerBase
{
/// <summary>
/// 获取订单
/// </summary>
/// <param name="distributedCache"></param>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public async Task<string> GetOrder([FromServices] ObjectPool<StringBuilder> builderPool, [FromServices]ILogger<OrderController> logger, [FromQuery] string id)
{
// 从对象池中获取StringBuilder对象
var stringBuilder = builderPool.Get();
for (int i = 0; i < 10000; i++)
{
// 从对象池中获取StringBuilder对象
var stringBuilderItem = builderPool.Get();
stringBuilderItem.Append(id);
stringBuilder.Append(stringBuilderItem.ToString());
}
return await Task.FromResult(stringBuilder.ToString());
}
}
响应压缩
为什么需要响应压缩
网络带宽是一种有限资源。减小响应大小通常可显著提高应用的响应速度。减小有效负载大小的一种方式是压缩应用的响应。
通常,任何未本机压缩的响应都可以从响应压缩中获益。本机压缩的响应通常包括CSS、JavaScript、HTML、XML和JSON。不要压缩本机压缩的资产,例如PNG文件。尝试进一步压缩本机压缩的响应时,任何较小的额外减少大小和传输时间都可能会被处理压缩所需的时间所掩盖。不要压缩小于150-1000字节的文件,具体取决于文件的内容和压缩效率。压缩小文件的开销可能会产生比未压缩文件更大的压缩文件。
当客户端可以处理压缩内容时,客户端必须通过随请求发送Accept-Encoding
标头来通知服务器其功能。当服务器发送压缩的内容时,它必须在Content-Encoding
标头中包含有关如何对压缩响应进行编码的信息。
下表显示了响应压缩中间件支持的内容编码指定
Accept-Encoding标头值 | 支持的中间件 | 说明 |
---|---|---|
br | 是(默认) | Brotli压缩数据格式 |
deflate | 否 | DEFLATE压缩数据格式 |
exi | 否 | W3C高效XML交换 |
gzip | 是 | Gzip文件格式 |
identity | 是 | “无编码”标识符:不得对响应进行编码。 |
pack200-gzip | 否 | Java存档的网络传输格式 |
* |
是 | 任何未显式请求的可用内容编码 |
响应压缩中间件允许为自定义Accept-Encoding
标头值添加其他压缩提供程序。
响应压缩中间件能够响应质量值(qvalue,q)客户端发送的权重来确定压缩方案的优先级。
压缩算法需要在压缩速度和压缩效率之间进行权衡。在此上下文中,有效性是指压缩后的输出大小。最小大小是通过最佳压缩实现的。
下表介绍了请求、发送、缓存和接收压缩内容所涉及的标头。
标头 | 角色 |
---|---|
Accept-Encoding | 从客户端发送到服务器以指示客户端可接受的内容编码方案。 |
Content-Encoding | 从服务器发送到客户端以指示有效负载中内容的编码。 |
Content-Length | 发生压缩时,会删除Content-Length标头,因为压缩响应时正文内容会发生更改。 |
Content-MD5 | 发生压缩时,会删除Content-MD5标头,因为正文内容已更改,哈希不再有效。 |
Content-Type | 指定内容的MIME类型。每个响应都应指定其Content-Type。响应压缩中间件检查此值以确定是否应压缩响应。响应压缩中间件指定一组可以编码的默认MIME类型,并且可以替换或添加它们。 |
Vary | 当服务器将值为Accept-Encoding的Vary标头发送到客户端和代理时,该标头会指示客户端或代理应该根据请求的Accept-Encoding标头值缓存(改变)响应。带有Vary:Accept-Encoding标头的返回内容的结果是,压缩的响应和未压缩的响应都单独进行缓存。 |
启用响应压缩中间件
注入响应压缩中间件: AddResponseCompression
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// 启用响应压缩中间件
services.AddResponseCompression(options =>
{
// 为Https也启用
options.EnableForHttps = true;
});
}
启用它: UseResponseCompression
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseResponseCompression();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
未开启前
content-type: application/json; charset=utf-8
date: Fri, 28 Oct 2022 14:51:29 GMT
server: Kestrel
开启后
content-encoding: br
content-type: application/json; charset=utf-8
date: Fri, 28 Oct 2022 14:52:26 GMT
server: Kestrel
vary: Accept-Encoding
压力测试
Azure负载测试
负载测试和压力测试对于确保web应用的性能和可缩放性非常重要。尽管它们的某些测试是相同的,但目标不同。
- 负载测试:测试应用是否可以在特定情况下处理指定的用户负载,同时仍满足响应目标。应用在正常状态下运行。
- 压力测试:在极端条件下(通常为长时间)运行时测试应用的稳定性。测试会对应用施加高用户负载(峰值或逐渐增加的负载)或限制应用的计算资源。
压力测试可确定压力下的应用是否能够从故障中恢复,并正常返回到预期的行为。在压力下,应用不会在正常状态下运行。
Azure负载测试(预览版)是一项完全托管的负载测试服务,可用于生成大规模负载。该服务可以模拟应用的流量,且无需其托管位置。通过Azure负载测试(预览版),可以使用现有的ApacheJMeter脚本生成大规模负载。
第三方工具
列表包含具有各种功能集的第三方web性能工具:
- Apache JMeter
- ApacheBench(ab)
- Gatling
- k6
- Locust
- West Wind WebSurge
- Netling
- Vegeta
- NBomber
参考
- 数据库——Redis超详细总结
- 这次彻底读透Redis
- 谈谈ASP.NET Core中的ResponseCaching
- .NET Core ResponseCache【缓存篇(一)】
- .Net Core缓存组件(MemoryCache)【缓存篇(二)】
- 拥抱.NET Core系列:MemoryCache 初识
- ASP.NET Core 中的响应缓存中间件
- ASP.NET Core 中的响应缓存中间件
- ASP.NET Core 中的响应缓存
- ResponseCachingMiddleware
- Redis
- Docker安装Redis并使用Another Redis Desktop Manager连接
- qishibo/AnotherRedisDesktopManager
- Redis DeskTop Manager
- Redis Desktop Manager(Redis可视化工具)安装及使用教程
- 一篇短文带您了解一下EasyCaching
- 通过ASP.NET Core中的ObjectPool重用对象
- ASP.NET Core中的响应压缩
- Unable to connect to memcached by account username and password?
- cnblogs/EnyimMemcachedCore