ASP.NET Core – 缓存之分布式缓存

分布式缓存是由多个应用服务器共享的缓存,通常作为访问它的应用服务器的外部服务进行维护。 分布式缓存可以提高 ASP.NET Core 应用的性能和可伸缩性,尤其是当应用由云服务或服务器场托管时。

与其他将缓存数据存储在单个应用服务器上的缓存方案相比,分布式缓存具有多个优势。

当分发缓存数据时,数据:

  • 在多个服务器的请求之间保持一致(一致性)。
  • 在进行服务器重启和应用部署后仍然有效。
  • 不使用本地内存。

1. 分布式缓存的使用

.NET Core 框架下对于分布式缓存的使用是基于 IDistributedCache 接口的,通过它进行抽象,统一了分布式缓存的使用方式,它对缓存数据的存取都是基于 byte[] 的。

IDistributedCache 接口提供以下方法来处理分布式缓存实现中的项:

  • Get、GetAsync:如果在缓存中找到,则接受字符串键并以 byte[] 数组的形式检索缓存项。
  • Set、SetAsync:使用字符串键将项(作为 byte[] 数组)添加到缓存。
  • Refresh、RefreshAsync:根据键刷新缓存中的项,重置其可调到期超时(如果有)。
  • Remove、RemoveAsync:根据字符串键删除缓存项。

使用的时候只需要将其通过容器注入到相应的类中即可。

2. 分布式缓存的接入

分布式缓存是基于特定的缓存应用实现的,需要依赖特定的第三方应用,在接入特定的分布式缓存应用时,需要应用对于的 Nuget 包,微软官方提供了基于 SqlServer 、Redis 实现分布式缓存的 Nuget 包,还推荐了基于 Ncache 的方案,除此之外还有像 Memcache 之类的方案,微软虽然没有提供相应的 Nuget 包,但是社区也有相关开源的项目。

这里只讲 .NET Core 下两种分布式缓存的接入和使用,一种是分布式内存缓存,一种是使用得比较广泛的 Redis。其他的在 .NET Core 框架下的使用是差不多的,仅仅只是接入的时候有点区别。当然,Redis 除了作为分布式缓存来使用,还有其他更加丰富的一些功能,后续也会找时间进行一些介绍。

2.1 基于内存的分布式缓存

分布式内存缓存 (AddDistributedMemoryCache) 是框架提供的 IDistributedCache 实现,用于将项存储在内存中,它就在 Microsoft.Extensions.Caching.Memory Nuget 包中。 分布式内存缓存不是真正的分布式缓存。 缓存项由应用实例存储在运行该应用的服务器上。

分布式内存缓存是一个有用的实现:

  • 在开发和测试场景中。

  • 当在生产环境中使用单个服务器并且内存消耗不重要时。 实现分布式内存缓存会抽象缓存的数据存储。 如果需要多个节点或容错,它允许在未来实现真正的分布式缓存解决方案。

当应用在 Program.cs 的开发环境中运行时,我们可以通过以下方式使用分布式缓存,以下示例代码基于 .NET 控制台程序:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var host = Host.CreateDefaultBuilder(args)
	.ConfigureServices(services =>
	{
		services.AddDistributedMemoryCache();
	})
	.Build();

host.Run();

之后还是和内存缓存差不多的例子,演示一下缓存的存取、删除、刷新。

public interface IDistributedCacheService
{
	Task PrintDateTimeNow();
}
public class DistributedCacheService : IDistributedCacheService
{
	public const string CacheKey = nameof(DistributedCacheService);
	private readonly IDistributedCache _distributedCache;
	public DistributedCacheService(IDistributedCache distributedCache)
	{
		_distributedCache = distributedCache;
	}

	public async Task FreshAsync()
	{
		await _distributedCache.RefreshAsync(CacheKey);
	}

	public async Task PrintDateTimeNowAsync()
	{
		var time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
		var cacheValue = await _distributedCache.GetAsync(CacheKey);
		if(cacheValue == null)
		{
			// 分布式缓存对于缓存值的存取都是基于 byte[],所以各种对象必须先序列化为字符串,之后转换为 byte[] 数组
			cacheValue = Encoding.UTF8.GetBytes(time);
			var distributedCacheEntryOption = new DistributedCacheEntryOptions()
			{
				//AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(20),
				AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20),
				SlidingExpiration = TimeSpan.FromSeconds(3)
			};
			// 存在基于字符串的存取扩展方法,内部其实也是通过 Encoding.UTF8 进行了编码
			// await _distributedCache.SetStringAsync(CacheKey, time, distributedCacheEntryOption);
			await _distributedCache.SetAsync(CacheKey, cacheValue, distributedCacheEntryOption);
		}
		time = Encoding.UTF8.GetString(cacheValue);
		Console.WriteLine("缓存时间:" + time);
		Console.WriteLine("当前时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
	}

	public async Task RemoveAsync()
	{
		await _distributedCache.RemoveAsync(CacheKey);
	}
}

之后,在入口文件添加以下代码,查看控制台结果是否与预想的一致:

using DistributedCacheSample;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var host = Host.CreateDefaultBuilder(args)
	.ConfigureServices(services =>
	{
		services.AddDistributedMemoryCache();
		services.AddTransient<IDistributedCacheService, DistributedCacheService>();
	})
	.Build();

var distributedCache = host.Services.GetRequiredService<IDistributedCacheService>();
// 第一次调用,设置缓存
Console.WriteLine("第一次调用,设置缓存");
await distributedCache.PrintDateTimeNowAsync();
await Task.Delay(TimeSpan.FromSeconds(1));
// 未过滑动时间,数据不变
Console.WriteLine("未过滑动时间,数据不变");
await distributedCache.PrintDateTimeNowAsync();
await Task.Delay(TimeSpan.FromSeconds(3));
// 已过滑动时间,数据改变
Console.WriteLine("已过滑动时间,数据改变");
await distributedCache.PrintDateTimeNowAsync();
await Task.Delay(TimeSpan.FromSeconds(1));
// 未过滑动时间,手动刷新过期时间
Console.WriteLine("未过滑动时间,手动刷新过期时间");
await distributedCache.FreshAsync();
await Task.Delay(TimeSpan.FromSeconds(2));
// 距离上一次调用此方法,已过滑动时间,但由于手动刷新过过期时间,过期时间重新计算,数据不变
Console.WriteLine("距离上一次调用此方法,已过滑动时间,但由于手动刷新过过期时间,过期时间重新计算,数据不变");
await distributedCache.PrintDateTimeNowAsync();
await Task.Delay(TimeSpan.FromSeconds(2));
// 移除缓存
Console.WriteLine("移除缓存");
await distributedCache.RemoveAsync();
// 原有的缓存已移除,调用此方法是重新设置缓存,数据改变
Console.WriteLine("原有的缓存已移除,调用此方法是重新设置缓存,数据改变");
await distributedCache.PrintDateTimeNowAsync();

host.Run();

image

结果和预想的是一致的。

2.2 基于 Redis 的分布式缓存

Redis 是一种开源的基于内存的非关系型数据存储,通常用作分布式缓存。在 .NET Core 框架中使用 Redis 实现分布式缓存,需要引用 Microsoft.Extensions.Caching.StackExchangeRedis Nuget 包,包中通过 AddStackExchangeRedisCache 添加 RedisCache 实例来配置缓存实现,该类基于 Redis 实现了 IDistributedCache 接口。

(1) 安装 Redis

这里我在云服务器上通过 Docker 快速安装了 Redis ,映射容器内 Redis 默认端口 6379 到主机端口 6379,并且设置了访问密码为 123456 。

docker run -d --name redis -p 6379:6379 redis --requirepass "123456"

(2) 应用添加依赖包,并且通过配置服务依赖关系

Install-Package Microsoft.Extensions.Caching.StackExchangeRedis

或者通过 VS 的 Nuget 包管理工具进行安装

依赖关系配置如下:

var host = Host.CreateDefaultBuilder(args)
	.ConfigureServices(services =>
	{
		// services.AddDistributedMemoryCache();
		services.AddStackExchangeRedisCache(opyions =>
		{
			opyions.Configuration = "xxx.xxx.xxx.xxx:6379,password=123456";
		});
	})
	.Build();

这里只需要将原来的分布式内存缓存服务的配置切换为分布式 Redis 缓存的配置即可,其他的什么都不用改,就可以从内存缓存切换到 Redis 分布式缓存了。所以我们在日常工作的应用搭建中推荐使用基于分布式缓存方案,前期或者开发环境中可以使用基于内存的分布式缓存,后面项目的性能要求高了,可以很方便地切换到真正的分布式缓存,只需改动一行代码。

之后基于前面的例子运行应用,可以看到输出的结果是一样的。

image

而在 Redis 上也可以看得到我们缓存上去的数据。

image

这里还有一个要注意的点,理论上使用分布式缓存是能够增强应用的性能和体验性的,但是像 Redis 这样的分布式缓存一般情况下是和应用部署在不同的服务器,每一次缓存的获取会存在一定的网络传输消耗,当缓存的数据量比较大,而且缓存存取频繁的时候,也会有很大的性能消耗。之前在项目中就遇到过这样的问题,由于一个查询功能需要实时进行计算,计算中需要进行循环,而计算依赖于基础数据,这部分的数据是使用缓存的,当初直接使用 Redis 缓存性能并不理想。当然可以说这种方式是有问题的,但是当时由于业务需要,封装的计算方法中需要在应用启动的时候由外部初始化基础数据,为基础数据能够根据前端改动而刷新,所以用了缓存的方式。

下面是一个示例进行内存缓存和 Redis 缓存的对比:

这里利用 BenchmarkDotNet 进行性能测试,需要先对原有的代码进行一下改造,这里调整了一下构造函数,自行实例化相关缓存的对象,之后有三个方法,分别使用 Redis 缓存、内存缓存、内存缓存结合 Redis 缓存,每个方法中模拟业务中的1000次循环,循环中缓存数据进行存取。

点击查看性能测试代码
[SimpleJob(RuntimeMoniker.Net60)]
public class DistributedCacheService : IDistributedCacheService
{
	public const string CacheKey = nameof(DistributedCacheService);
	private readonly IDistributedCache _distributedCache;
	private readonly IDistributedCache _distributedMemoryCache;
	private readonly IMemoryCache _memoryCache;

	[Params(1000)]
	public int N;

	public DistributedCacheService()
	{
		_distributedCache = new RedisCache(Options.Create(new RedisCacheOptions()
		{
			Configuration = "1.12.64.68:6379,password=123456"
		}));
		_distributedMemoryCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions()));
		_memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
	}

	public async Task FreshAsync()
	{
		await _distributedCache.RefreshAsync(CacheKey);
	}

	public async Task PrintDateTimeNowAsync()
	{
		var time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
		var cacheValue = await _distributedCache.GetAsync(CacheKey);
		if (cacheValue == null)
		{
			// 分布式缓存对于缓存值的存取都是基于 byte[],所以各种对象必须先序列化为字符串,之后转换为 byte[] 数组
			cacheValue = Encoding.UTF8.GetBytes(time);
			var distributedCacheEntryOption = new DistributedCacheEntryOptions()
			{
				//AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(10),
				AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20),
				SlidingExpiration = TimeSpan.FromSeconds(3)
			};
			// 存在基于字符串的存取扩展方法,内部其实也是通过 Encoding.UTF8 进行了编码
			// await _distributedCache.SetStringAsync(CacheKey, time, distributedCacheEntryOption);
			await _distributedCache.SetAsync(CacheKey, cacheValue, distributedCacheEntryOption);
		}
		time = Encoding.UTF8.GetString(cacheValue);
		Console.WriteLine("缓存时间:" + time);
		Console.WriteLine("当前时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
	}

	[Benchmark]
	public async Task PrintDateTimeNowWithRedisAsync()
	{
		for(var i =0; i< N; i++)
		{
			var time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
			var cacheValue = await _distributedCache.GetAsync(CacheKey);
			if (cacheValue == null)
			{
				// 分布式缓存对于缓存值的存取都是基于 byte[],所以各种对象必须先序列化为字符串,之后转换为 byte[] 数组
				cacheValue = Encoding.UTF8.GetBytes(time);
				var distributedCacheEntryOption = new DistributedCacheEntryOptions()
				{
					//AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(10),
					AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),
					SlidingExpiration = TimeSpan.FromMinutes(5)
				};
				// 存在基于字符串的存取扩展方法,内部其实也是通过 Encoding.UTF8 进行了编码
				// await _distributedCache.SetStringAsync(CacheKey, time, distributedCacheEntryOption);
				await _distributedCache.SetAsync(CacheKey, cacheValue, distributedCacheEntryOption);
			}
			time = Encoding.UTF8.GetString(cacheValue);
		}
	}
	[Benchmark]
	public async Task PrintDateTimeWithMemoryAsync()
	{
		for (var i = 0; i < N; i++)
		{
			var time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
			var cacheValue = await _distributedMemoryCache.GetAsync(CacheKey);
			if (cacheValue == null)
			{
				// 分布式缓存对于缓存值的存取都是基于 byte[],所以各种对象必须先序列化为字符串,之后转换为 byte[] 数组
				cacheValue = Encoding.UTF8.GetBytes(time);
				var distributedCacheEntryOption = new DistributedCacheEntryOptions()
				{
					//AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(10),
					AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),
					SlidingExpiration = TimeSpan.FromMinutes(5)
				};
				// 存在基于字符串的存取扩展方法,内部其实也是通过 Encoding.UTF8 进行了编码
				// await _distributedCache.SetStringAsync(CacheKey, time, distributedCacheEntryOption);
				await _distributedMemoryCache.SetAsync(CacheKey, cacheValue, distributedCacheEntryOption);
			}
			time = Encoding.UTF8.GetString(cacheValue);
		}
	}

	[Benchmark]
	public async Task PrintDateTimeWithMemoryAndRedisAsync()
	{
		for (var i = 0; i < N; i++)
		{
			var cacheValue = await _memoryCache.GetOrCreateAsync(CacheKey, async cacheEntry =>
			{
				var time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
				var redisCacheValue = await _distributedCache.GetAsync(CacheKey);
				if (redisCacheValue == null)
				{
					// 分布式缓存对于缓存值的存取都是基于 byte[],所以各种对象必须先序列化为字符串,之后转换为 byte[] 数组
					redisCacheValue = Encoding.UTF8.GetBytes(time);
					var distributedCacheEntryOption = new DistributedCacheEntryOptions()
					{
						//AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(10),
						AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),
						SlidingExpiration = TimeSpan.FromMinutes(5)
					};
					// 存在基于字符串的存取扩展方法,内部其实也是通过 Encoding.UTF8 进行了编码
					// await _distributedCache.SetStringAsync(CacheKey, time, distributedCacheEntryOption);
					await _distributedCache.SetAsync(CacheKey, redisCacheValue, distributedCacheEntryOption);
				}
				time = Encoding.UTF8.GetString(redisCacheValue);
				cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(20);
				return time;
			});
		}
	}

	public async Task RemoveAsync()
	{
		await _distributedCache.RemoveAsync(CacheKey);
	}
}

Program.cs 文件中只保留以下代码:

Summary summary = BenchmarkRunner.Run<DistributedCacheService>();
Console.ReadLine();

测试结果如下:

image

可以看到这种情况下使用 Redis 缓存性能是惨不忍睹的,但是另外两种方式就不一样了。

我们在业务中的缓存最终就是第三种方法的方式,结合内存缓存和 Redis 缓存,根本的思路就是在使用时将数据临时保存在本地,减少网络传输的消耗,并且根据实际业务情况控制内存缓存的超时时间以保持数据的一致性。

参考文章:
ASP.NET Core 中的分布式缓存

ASP.NET Core 系列:

目录:ASP.NET Core 系列总结
上一篇:ASP.NET Core - 缓存之内存缓存(下)

原文链接:https://www.cnblogs.com/wewant/p/17114119.html

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:ASP.NET Core – 缓存之分布式缓存 - Python技术站

(0)
上一篇 2023年4月24日
下一篇 2023年4月24日

相关文章

  • WPF实现文本描边+外发光效果的示例代码

    WPF(Windows Presentation Foundation)是一种用于创建Windows桌面应用程序的技术。WPF提供了丰富的UI元素和效果,可以轻松实现文本描边和外发光效果。本文将提供WPF实现文本描边和外发光效果的完整攻略,包括创建WPF应用程序、实现文本描边、实现外发光效果、示例等。 创建WPF应用程序 要创建WPF应用程序,可以使用Vis…

    C# 2023年5月15日
    00
  • C# 9 新特性——record的相关总结

    C# 9 新特性——record的相关总结 引言 C# 9.0 新特性 —— record 类 vs record record 类型语法介绍 with表达式 record struct 总结 引言 C# 9.0 版本带来了一些非常实用的新特性,其中最引人注目的莫过于record。这是一种新型的数据类型,专门用于表示“记录”数据,以及可以设置许多与属性相关的…

    C# 2023年6月7日
    00
  • 代码实现打印功能(asp.net+javascript)

    下面是代码实现打印功能的完整攻略。 1. 前言 在 ASP.NET 中实现打印功能,主要需要使用 JavaScript 的 window.print() 方法,将当前页面的内容打印出来。在打印前可能还需要进行一些页面内容的样式调整、打印内容的选择等操作。下面我们将详细介绍如何使用 ASP.NET 和 JavaScript 实现打印功能。 2. 准备工作 在 …

    C# 2023年5月31日
    00
  • C# 动态输出Dos命令执行结果的实例(附源码)

    下面是对主题“C# 动态输出Dos命令执行结果的实例(附源码)”的详细讲解攻略。 1. 背景介绍 在C#中执行DOS命令并输出执行结果,通常采用Process类来实现。但如果要及时输出命令执行结果,我们需要借助于StandardOutput和BeginOutputReadLine两个属性和方法。 2. 具体步骤 2.1 创建Process实例 首先,我们需要…

    C# 2023年6月7日
    00
  • c#创建vc可调用的com组件方法分享

    下面我就来详细讲解一下“C#创建VC可调用的COM组件方法分享”的完整攻略。 1. 确定组件需求 首先,我们需要确定下自己需要开发什么样的组件,这是COM组件开发的第一步。根据需求确定组件接口和类,建议先画一张组件结构图,方便我们更好地理解组件整体架构。 2. 创建COM组件项目 打开Visual Studio创建新的C# Class Library项目,选…

    C# 2023年6月7日
    00
  • Asp.Net Core配置多环境log4net配置文件的全过程

    下面是详细讲解”Asp.Net Core配置多环境log4net配置文件的全过程”的完整攻略: 1. 引入log4net依赖 可以通过Nuget包管理器或手动安装的方式引入log4net,以Nuget包管理器为例,可以在项目中右键点击“管理NuGet程序包”,然后搜索log4net安装即可。 2. 配置log4net log4net的配置文件可以放置在多个环…

    C# 2023年6月3日
    00
  • c#中实现图片灰度化技术详解

    c#中实现图片灰度化技术详解 什么是图片灰度化? 在计算机图形学中,灰度化是一种将图片从彩色图转化为灰度图的处理过程。灰度图不同于彩色图,它只有黑白两色,其颜色是通过将红色、绿色和蓝色通道加权平均得到的。 实现灰度化的方法 像素级操作法 像素级操作法是一种对图片进行遍历,针对每个像素点单独处理的方法。具体实现是将每个像素点的 R、G、B 通道值按一定权重进行…

    C# 2023年6月7日
    00
  • asp.net(c#)有关 Session 操作的几个误区

    下面是关于 ASP.NET(C#) 中 Session 操作的几个误区的详细讲解: 误区一:Session数据的不安全性 很多人认为 Session 的数据是存在 Web 服务器的内存中,并且只有用户可以访问它们,因此 Session 数据非常安全。然而,这是不准确的。攻击者可以通过多种方式访问这些 Session 数据,从而影响应用程序的可靠性和安全性。 …

    C# 2023年5月31日
    00
合作推广
合作推广
分享本页
返回顶部