ASP.NET Core - 緩存之分散式緩存

来源:https://www.cnblogs.com/wewant/archive/2023/04/19/17114119.html
-Advertisement-
Play Games

分散式緩存是由多個應用伺服器共用的緩存,通常作為訪問它的應用伺服器的外部服務進行維護。 分散式緩存可以提高 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 - 緩存之記憶體緩存(下)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ###1.首先查詢出組織機構 就是一個簡單的查詢 List<Dept> deptList = mapper.getDeptList(); Map<Long, OrgNode> nodeMap = new HashMap<>(); List<Long> rootIds = new ArrayList< ...
  • 無畏併發 併發 Concurrent:程式的不同部分之間獨立的執行(併發) Parallel:程式的不同部分同時運行(並行) Rust無畏併發:允許你編寫沒有細微Bug的代碼,併在不引入新Bug的情況下易於重構 註意:本文中的”併發“泛指 concurrent 和 parallel 一、使用線程同時 ...
  • Object 類提供的 clone() 方法是 “淺拷貝”,並沒有克隆對象中引用的其他對象,原對象和克隆的對象仍然會共用一些信息。深拷貝指的是:在對象中存在其他對象的引用的情況下,會同時克隆對象中引用的其他對象,原對象和克隆的對象互不影響。 ...
  • 對象文件存儲服務(OSS)主要用於存儲零散的文件,和直接存儲到本地文件系統中相比,有以下的幾個優勢: 跨伺服器可用 相容Amazon S3 API 橫向擴容 高可用 支持加密 MinIO就是一個高性能的文件服務,我們使用.NET來操作一下。 部署MinIO 最簡單的辦法,就是在Docker上運行Mi ...
  • 最近在對某個後端服務做 .NET Core 升級時,裡面使用了多處處理 MultipartFormDataContent 相關內容的代碼。這些地方從 .NET Framework 遷移到 .NET Core 之後的代碼改動較大,由於本身沒有測試覆蓋,導致在部署 QA 環境後引發了一些問題。這裡做一個 ...
  • 引子 最近不知怎麼的,自從學了WebAPI(為什麼是這個,而不是MVC,還不是因為MVC的Razor語法比較難學,生態不如現有的Vue等框架,webapi很好的結合了前端生態)以後,使用別人的組件一帆風順,但是不知其意,突然很想自己實現一個基於的JWT認證服務,來好好瞭解一下這個內容。 起步 自從S ...
  • 由於當前社會人力成本越來越昂貴,機器取代人力是大勢所趨,自動化的發展也隨之越來越快 。當製造公司需 要一雙手和一對 眼睛的時候卻不得不雇佣一個人的苦惱日益加重,而傳統的機器設計和電氣自動化的發展,解決一雙手的問題已經漸漸得到了緩解,現在就到了需要解決一雙眼睛的時候,機器視覺的出現和廣泛應用也隨著到來 ...
  • 概述 C#是微軟開發的一種流行的編程語言,廣泛用於開發桌面,Web和移動應用程式。在每個新版本中,C# 都會帶來令人興奮的功能和改進,使其更強大、更具表現力和更高效。C# 的最新版本是2022年發佈的 C#11,它引入了一系列新功能,例如abstract 和 virtual 引入到靜態方法中、泛型 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...