分散式緩存是由多個應用伺服器共用的緩存,通常作為訪問它的應用伺服器的外部服務進行維護。 分散式緩存可以提高 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();
結果和預想的是一致的。
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 分散式緩存了。所以我們在日常工作的應用搭建中推薦使用基於分散式緩存方案,前期或者開發環境中可以使用基於記憶體的分散式緩存,後面項目的性能要求高了,可以很方便地切換到真正的分散式緩存,只需改動一行代碼。
之後基於前面的例子運行應用,可以看到輸出的結果是一樣的。
而在 Redis 上也可以看得到我們緩存上去的數據。
這裡還有一個要註意的點,理論上使用分散式緩存是能夠增強應用的性能和體驗性的,但是像 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();
測試結果如下:
可以看到這種情況下使用 Redis 緩存性能是慘不忍睹的,但是另外兩種方式就不一樣了。
我們在業務中的緩存最終就是第三種方法的方式,結合記憶體緩存和 Redis 緩存,根本的思路就是在使用時將數據臨時保存在本地,減少網路傳輸的消耗,並且根據實際業務情況控制記憶體緩存的超時時間以保持數據的一致性。
參考文章:
ASP.NET Core 中的分散式緩存
ASP.NET Core 系列:
目錄:ASP.NET Core 系列總結
上一篇:ASP.NET Core - 緩存之記憶體緩存(下)