ASP.NET Core – Distributed cache of cache
A distributed cache is a cache shared by multiple application servers and is typically maintained as an external service to the application servers that access it. Distributed caching can improve the performance and scalability of ASP.NET Core applications, especially when the application is hosted by a cloud service or server farm.
Distributed caching has several advantages over other caching solutions that store cached data on a single application server.
When distributing cached data, data:
- Maintain consistency (consistency) across requests from multiple servers.
- Still valid after server restart and application deployment.
- Does not use local memory.
1. Use of distributed cache
The use of distributed cache under the .NET Core framework is based on the IDistributedCache interface, which abstracts and unifies the use of distributed cache. Its access to cached data is based on byte[].
The IDistributedCache interface provides the following methods for handling items in distributed cache implementations:
- Get, GetAsync: Accepts a string key and retrieves the cache item as a byte[] array if found in the cache.
- Set, SetAsync: Add an item (as a byte[] array) to the cache using a string key.
- Refresh, RefreshAsync: Refresh an item in the cache based on a key, resetting its adjustable expiration timeout (if any).
- Remove, RemoveAsync: Delete cache items based on string keys.
When using it, you only need to inject it into the corresponding class through the container.
2. Distributed cache access
Distributed caching is implemented based on specific caching applications and needs to rely on specific third-party applications. When accessing specific distributed caching applications, you need to apply the corresponding Nuget package. Microsoft officially provides implementation based on SqlServer and Redis. The Nuget package for distributed caching also recommends solutions based on Ncache. In addition, there are solutions like Memcache. Although Microsoft does not provide the corresponding Nuget package, the community also has related open source projects.
Here we only talk about the access and use of two distributed caches under .NET Core, one is the distributed memory cache and the other is the widely used Redis. The other uses under the .NET Core framework are similar, but there are some differences when connecting. Of course, in addition to being used as a distributed cache, Redis also has other richer functions. We will find time to introduce them later.
2.1 Memory-based distributed cache
Distributed Memory Cache (AddDistributedMemoryCache) is the framework-provided IDistributedCache implementation for storing items in memory, and is available in the Microsoft.Extensions.Caching.Memory Nuget package. Distributed memory cache is not a true distributed cache. Cache items are stored by the application instance on the server running the application.
Distributed memory caching is a useful implementation:
-
In development and testing scenarios.
-
When using a single server in a production environment and memory consumption is not important. Implementing a distributed memory cache abstracts the cached data storage. It allows for a truly distributed caching solution in the future if multiple nodes or fault tolerance are required.
When the application is running in the development environment of Program.cs, we can use distributed cache in the following ways. The following sample code is based on the .NET console program:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddDistributedMemoryCache();
})
.Build();
host.Run();
The following is an example similar to the memory cache, demonstrating cache access, deletion, and refresh.
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) { // Distributed cache's access to cached values is based on byte[], so various objects must be serialized into strings first and then converted into byte[] arrays cacheValue = Encoding.UTF8.GetBytes(time); var distributedCacheEntryOption = new DistributedCacheEntryOptions() { //AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(20), AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20), SlidingExpiration = TimeSpan.FromSeconds(3) }; // Existence is based on �28725-20230212170840108-358888119.png" alt="image" loading="lazy">
There is another point to note here. In theory, using distributed cache can enhance the performance and experience of the application. However, distributed caches like Redis are generally deployed on different servers from the application. There will be a certain network transmission consumption for a cache acquisition. When the amount of cached data is relatively large and the cache access is frequent, there will also be a large performance consumption. I have encountered such a problem in the project before. Since a query function needs to be calculated in real time, the calculation needs to be looped, and the calculation depends on the basic data. This part of the data is cached. At first, the Redis cache performance was directly used and not ideal. Of course, it can be said that this method is problematic, but due to business needs at that time, the encapsulated calculation method required basic data to be initialized externally when the application was started, so that the basic data could be refreshed according to front-end changes, so the cache method was used .
The following is an example comparing memory cache and Redis cache:
BenchmarkDotNet is used here for performance testing. The original code needs to be modified first. Here, the constructor is adjusted and the relevant cache objects are instantiated by oneself. After that, there are three methods, using Redis cache, memory cache, and memory. The cache is combined with the Redis cache. Each method simulates 1,000 cycles in the business, and caches data for access during the cycles.
Click to view the performance test code
[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) { // Distributed cache's access to cached values is based on byte[], so various objects must be serialized into strings first and then converted into byte[] arrays cacheValue = Encoding.UTF8.GetBytes(time); var distributedCacheEntryOption = new DistributedCacheEntryOptions() { //AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(10), AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20), SlidingExpiration = TimeSpan.FromSeconds(3) }; // There is a string-based access extension method, which is actually encoded internally through Encoding.UTF8 // await _distributedCache.SetStringAsync(CacheKey, time, distributedCacheEntryOption); await _distributedCache.SetAsync(CacheKey, cacheValue, distributedCacheEntryOption); } time = Encoding.UTF8.GetString(cacheValue); Console.WriteLine("Cache time: " + time); Console.WriteLine("Current time:" + 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) { // Distributed cache's access to cached values is based on byte[], so various objects must be serialized into strings first and then converted into byte[] arrays cacheValue = Encoding.UTF8.GetBytes(time); var distributedCacheEntryOption = new DistributedCacheEntryOptions() { //AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(10), AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10), SlidingExpiration = TimeSpan.FromMinutes(5) }; // There is a string-based access extension method, which is actually encoded internally through 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) { // Distributed cache accesses cached values based on byte[], so various objects must be serialized into strings first and then converted into byte[] arrays cacheValue = Encoding.UTF8.GetBytes(time); var distributedCacheEntryOption = new DistributedCacheEntryOptions() { //AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(10), AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10), SlidingExpiration = TimeSpan.FromMinutes(5) }; // There is a string-based access extension method, which is actually encoded internally through 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 { var time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); var redisCacheValue = await _distributedCache.GetAsync(CacheKey); if (redisCacheValue == null) { // Distributed cache's access to cached values is based on byte[], so various objects must be serialized into strings first and then converted into byte[] arrays redisCacheValue = Encoding.UTF8.GetBytes(time); var distributedCacheEntryOption = new DistributedCacheEntryOptions() { //AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(10), AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10), SlidingExpiration = TimeSpan.FromMinutes(5) }; // There is a string-based access extension method, which is actually encoded internally through 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); } }
Only the following code remains in the Program.cs file:
Summary summary = BenchmarkRunner.Run(); Console.ReadLine();
The test results are as follows:
You can see that the performance of using Redis cache in this case is terrible, but the other two methods are different.
Our caching in business is ultimately the third method, combining memory caching and Redis caching. The basic idea is to temporarily save data locally during use, reducing network transmission consumption, and based on actual business conditions Control the timeout of the memory cache to maintain data consistency.
Reference article:
Distributed caching in ASP.NET CoreASP.NET Core Series:
Table of Contents: ASP.NET Core Series Summary
Previous article: ASP.NET Core - Memory cache of cache (Part 2)
Next article: ASP.NET Core - Logging System (1)butedCacheEntryOptions()
{
//AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(10),
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),
SlidingExpiration = TimeSpan.FromMinutes(5)
};
// There is a string-based access extension method, which is actually encoded internally through 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
{
var time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
var redisCacheValue = await _distributedCache.GetAsync(CacheKey);
if (redisCacheValue == null)
{
// Distributed cache's access to cached values is based on byte[], so various objects must be serialized into strings first and then converted into byte[] arrays
redisCacheValue = Encoding.UTF8.GetBytes(time);
var distributedCacheEntryOption = new DistributedCacheEntryOptions()
{
//AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(10),
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),
SlidingExpiration = TimeSpan.FromMinutes(5)
};
// There is a string-based access extension method, which is actually encoded internally through 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);
}
}
Only the following code remains in the Program.cs file:
Summary summary = BenchmarkRunner.Run(); Console.ReadLine();
The test results are as follows:
You can see that the performance of using Redis cache in this case is terrible, but the other two methods are different.
Our caching in business is ultimately the third method, combining memory caching and Redis caching. The basic idea is to temporarily save data locally during use, reducing network transmission consumption, and based on actual business conditions Control the timeout of the memory cache to maintain data consistency.
Reference article:
Distributed caching in ASP.NET CoreASP.NET Core Series:
Table of Contents: ASP.NET Core Series Summary
Previous article: ASP.NET Core – Memory cache of cache (Part 2)
Next article: ASP.NET Core – Logging System (1)