.NET9中的HybridCache

官方文档介绍:https://learn.microsoft.com/zh-cn/aspnet/core/performance/caching/overview?view=aspnetcore-9.0#hybridcache

github存储库:https://github.com/ssccinng/VideoCode/tree/master/HybridCacheTest

注意:HybirdCache在此刻(2024年11月20日)还处在预览版状态,正式版后续会随着.NET9的次要版本推出一起推出,本文也会在正式发布后再次更新一版

在介绍HybirdCache之前呢,先简单介绍一下 IDistributedCache 和 IMemoryCache。这两个是了解HybirdCache之前最好需要了解内容

图片

当然最最最之前我们简单介绍一下缓存。

缓存有什么用呢?例如HTTP,对于一些请求操作很昂贵(耗时,耗资源)的行为,我们可以在完成一次操作后存下我们的结果,再一次访问的时候,只要拿着相同的数据得出的key,我们就能快速拿到之前获取的内容,而不需要再次进行昂贵的操作。

图片

那我后台数据内容更新了怎么办?

当然,我们考虑到了这一点。我们可以选择当数据更新的时候手动刷新缓存中的数据,或者我们可以给我们的数据设置一个期限。当数据已经过期了的时候,如果有请求再次需要这个资源。我们就可以再去通过昂贵的操作获取一次最新的内容重新缓存起来,这样我们就实现了数据的更新。没错,这个就像是算法中所说的记忆化的概念,是类似的工作。

 

图片

但如果后面一直没有请求去访问这个数据了呢?不如说这样缓存一定会越来越多吧

这么考虑是对的,所以通常缓存会有一些机制在尽量不影响性能的前提下,后台定时去清理这些已经过期的数据,这样我们的缓存就完成了。

IMemoryCache(内存中缓存)

解决了什么是缓存的问题,接下来就是存储在哪里的问题了。最简单,效率最高的做法当然是存在内存里了,存的快读的快,何乐而不为?

注入IMemoryCache之后,我们就可以在代码中这么使用缓存


if (!(memoryCache.TryGetValue(key, out object? untyped)
            && untyped is SomeData value))
{
    value = GetSomeData();
    memoryCache.Set(key, value, Expiration);
}

这和我们刚刚的定义基本没有偏差。那我们还需要其他类型的缓存吗?

当然需要

思考几个问题

缓存在内存,若是我们的服务器重启了怎么办?

如果程序所在的服务器内存本来就已经不够用了怎么办?

如果我们有多个服务器想要共享缓存怎么办?// 缓存内容会被影响的可能性存在吗

思考了以上几个问题后,答案呼之欲出。我们需要一种能“持久化”存储缓存的手段,或是说需要有办法脱离本机的束缚的缓存

IDistributedCache (分布式缓存)

一般来说,这类缓存用于多个服务器共享缓存(redis等等)。不过,如果你只是希望缓存在本地服务器,或者甚至是以文件的形式缓存也是可以的。(甚至可以模拟IMemoryCache,(比如AddDistributedMemoryCache))。

通常来说,在使用IDistributedCache的时候,需要自己定义序列化和反序列化器,相对纯内存的缓存会稍有局限,同时耗时也会随着不同的实现而增加。获得的好处就是我们问题的答案,重启后依然有效,无需部署在本机,多个服务器可以公用缓存。

使用方式如下

var bytes = await distributedCache.GetAsync(key, cancellationToken);

SomeData value;
if (bytes is null)
{
    value = GetSomeData();
    bytes = Serialize(value); // 序列化
    await distributedCache.SetAsync(key, bytes,
        new() { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(5)}
        , cancellationToken); // 缓存五秒
}
else
{
    value = Deserialize<SomeData>(arr); // 反序列化
}
return value;

虽然我们获得了一定的好处,但使用方式相比IMemoryCache要麻烦了不少,并且也有一定缺陷。例如如果获取还没完成就再次出现了请求。也会再次重复的去调用昂贵的操作(踩踏)。而且我们也不是所有时候都会想去使用分布式存储,内存缓存当然有他优越的地方(方便,快),看起来我们似乎没有办法很好的使用两种缓存

于是

HybirdCache

在.NET9中推出了HybirdCache这一类型,这其实也不是特别新潮的概念,不过官方支持了当然是极好的。

值得注意的是由于目前还是预览版 需要一些声明才可以使用

#pragma warning disable EXTEXP0018 // experimental (pre-release)
builder.Services.AddHybridCache();
#pragma warning restore EXTEXP0018 // experimental (pre-release)

HybirdCache结合了IMemoryCache与IDistributedCache,并提供了一些,这两者兼不具备的好处。

HybirdCache会检查程序有没有IDistributedCache的实现,如果有,他便会使用现有的缓存

统一的API

HybirdCache使用同一套API管理了两种内存,通过配置可以打成自动的切换,不需要手动管理,大部分时候仅仅需要使用一个函数就足以满足需求

 var data = await cache.GetOrCreateAsync(
    $"SomeData/{id}",
    async cancellation =>
    {
        // valuetask注意
        await Task.Delay(2500);
        return new SomeData[]
        {
            new(1, "One"),
            new(2, "Two"),
            new(3, "Three")
        };
    },
    cancellationToken: cancellationToken);

return data;

踩踏防护

如果有多个请求同时在没有缓存的时候请求了资源,HybirdCache会自动的管理请求的行为,当最早的获取到了数据后会全部一起返回,不会有重复的昂贵请求的行为。

序列化

HybirdCache的序列化有默认实现并且是可配置的,默认对string会采用byte[],其余则使用System.Text.Json序列化。如果要配置,则可以使用下面的代码完成这件事

builder.Services.AddHybridCache(options =>
    {
        options.DefaultEntryOptions = new HybridCacheEntryOptions
        {
            Expiration = TimeSpan.FromSeconds(10),
            LocalCacheExpiration = TimeSpan.FromSeconds(5)
        };
    }).AddSerializer<SomeProtobufMessage, 
        GoogleProtobufSerializer<SomeProtobufMessage>>();
        
builder.Services.AddHybridCache(options =>
{
    options.DefaultEntryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromSeconds(10),
        LocalCacheExpiration = TimeSpan.FromSeconds(5)
    };
}).AddSerializerFactory<GoogleProtobufSerializerFactory>();

代码中设置的Expiration与LocalCacheExpiration 分别代表着总过期时长和内存中缓存过期时长(即内存过期则会使用分布式缓存)

如此,我们便可以愉快的使用HybridCache了
学会了吗?学会了

简单试图(?

public class FileCache : IDistributedCache
{
    private readonly string _cacheFilePath;
    private readonly TimeSpan _defaultExpiration;

    public FileCache(string cacheFilePath = "localcache.json", TimeSpan? defaultExpiration = null)
    {
        _cacheFilePath = cacheFilePath;
        _defaultExpiration = defaultExpiration ?? TimeSpan.FromMinutes(30);
    }

    private Dictionary<string, CacheItem> LoadCache()
    {
        if (File.Exists(_cacheFilePath))
        {
            var json = File.ReadAllText(_cacheFilePath);
            return JsonSerializer.Deserialize<Dictionary<string, CacheItem>>(json) ?? new Dictionary<string, CacheItem>();
        }
        return new Dictionary<string, CacheItem>();
    }

    private void SaveCache(Dictionary<string, CacheItem> cache)
    {
        var json = JsonSerializer.Serialize(cache, new JsonSerializerOptions { WriteIndented = true });
        File.WriteAllText(_cacheFilePath, json);
    }

    public byte[] Get(string key)
    {
        var cache = LoadCache();
        if (cache.ContainsKey(key))
        {
            var cacheItem = cache[key];
            if (cacheItem.Expiration > DateTime.UtcNow)
            {
                return Convert.FromBase64String(cacheItem.Value);
            }
            else
            {
                cache.Remove(key); // Remove expired item
                SaveCache(cache);
            }
        }
        return null;
    }

    public Task<byte[]?> GetAsync(string key, CancellationToken token = new CancellationToken())
    {
        return Task.FromResult<byte[]?>(Get(key));  
    }

    public async Task<byte[]> GetAsync(string key)
    {
        return await Task.FromResult(Get(key));
    }

    public Task RemoveAsync(string key, CancellationToken token = new CancellationToken())
    {
        Remove(key);
        return Task.CompletedTask;
    }

    public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
    {
        var cache = LoadCache();
        var cacheItem = new CacheItem
        {
            Value = Convert.ToBase64String(value),
            Expiration =   DateTime.UtcNow.Add(options.AbsoluteExpirationRelativeToNow??_defaultExpiration)
        };
        cache[key] = cacheItem;
        SaveCache(cache);
    }

    public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options,
        CancellationToken token = new CancellationToken())
    {
        Set(key, value, options);
        return Task.CompletedTask;
    }

    public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options)
    {
        Set(key, value, options);
        await Task.CompletedTask;
    }

    public void Refresh(string key)
    {
        var cache = LoadCache();
        if (cache.ContainsKey(key))
        {
            var cacheItem = cache[key];
            cacheItem.Expiration = DateTime.UtcNow.Add(_defaultExpiration);
            cache[key] = cacheItem;
            SaveCache(cache);
        }
    }

    public Task RefreshAsync(string key,
        CancellationToken token = new CancellationToken())
    {
        Refresh(key);
        return Task.CompletedTask;
    }

    public async Task RefreshAsync(string key)
    {
        Refresh(key);
        await Task.CompletedTask;
    }

    public void Remove(string key)
    {
        var cache = LoadCache();
        if (cache.ContainsKey(key))
        {
            cache.Remove(key);
            SaveCache(cache);
        }
    }

    public async Task RemoveAsync(string key)
    {
        Remove(key);
        await Task.CompletedTask;
    }

    // Cache item helper class
    private class CacheItem
    {
        public string Value { get; set; }
        public DateTime Expiration { get; set; }
    }
}

视频链接【C#探索】.NET9中的HybridCache

微信公众号搜索:scixing的炼丹炉

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值