ABP Framework-Cache源码解析

目录

https://abp.io/docs/6.0/Caching

Version

6.0.3

Package

Volo.Abp.Caching

基于Redis的缓存包

Volo.Abp.Caching.StackExchangeRedis

Abp的缓存包作为基础模块,在Abp模板中被很多模块所依赖,因此开发中,一般不用手动加入AbpCachingModule,但对于Redis的module,需要手动加入模块依赖中。

缓存实现

ABP的缓存其本质上还是借助于Asp.Net Core的缓存接口与实现,包括IMemoryCache和IDistributedCache,从Abp.Caching的模块中可见

[DependsOn(
    typeof(AbpThreadingModule),
    typeof(AbpSerializationModule),
    typeof(AbpUnitOfWorkModule),
    typeof(AbpMultiTenancyModule),
    typeof(AbpJsonModule))]
public class AbpCachingModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        context.Services.AddMemoryCache();
        context.Services.AddDistributedMemoryCache();


        //...
    }
}

了解Asp.Net Core中的缓存

在该缓存上,ABP设计了IDistributedCache<,>, 提供了一些额外的方法扩展使用场景,如下简要列出几个方法。

public interface IDistributedCache<TCacheItem, TCacheKey>
    where TCacheItem : class
{
    Task<TCacheItem?> GetAsync();
    Task<KeyValuePair<TCacheKey, TCacheItem?>[]> GetManyAsync();
    Task<TCacheItem?> GetOrAddAsync();
    Task<KeyValuePair<TCacheKey, TCacheItem?>[]> GetOrAddManyAsync();


    Task SetAsync();
    Task SetManyAsync();
    Task RefreshAsync();
    Task RefreshManyAsync();
    Task RemoveAsync();
    Task RemoveManyAsync();
}

该部分类图如下 204829956_834edc61-f7e2-45de-9958-dd0c8a801e05 IDistributedCache实则是指定了Key类型为String的IDistributedCache<TCacheItem,TCacheKey>。

public interface IDistributedCache<TCacheItem> : IDistributedCache<TCacheItem, string>
    where TCacheItem : class
{
    IDistributedCache<TCacheItem, string> InternalCache { get; }
}

DistributedCache内部注入DistributedCache<TCacheItem,TCacheKey>

public class DistributedCache<TCacheItem> : 
    IDistributedCache<TCacheItem>
    where TCacheItem : class
{
    public IDistributedCache<TCacheItem, string> InternalCache { get; }


    public DistributedCache(IDistributedCache<TCacheItem, string> internalCache)
    {
        InternalCache = internalCache;
    }
}

如此依赖,DistributedCache内部的所有接口最终都是通过InternalCache进入到DistributedCache<,>的方法中。如下所示

public class DistributedCache<TCacheItem> : 
    IDistributedCache<TCacheItem>
    where TCacheItem : class
{
    public IDistributedCache<TCacheItem, string> InternalCache { get; }
    public TCacheItem? Get(string key, bool? hideErrors = null, bool considerUow = false)
    {
        return InternalCache.Get(key, hideErrors, considerUow);
    } 
}

所以只用关注DistributedCache<TCacheItem,TCacheKey>了 204831229_1454fe1f-53aa-479b-82dc-c219479cb802

  • IDistributedCache

关键部分是Microsoft.IDistributedCache,在模块服务注册中,默认加上了分布式缓存,对应实现默认为内存分布式缓存(Redis缓存一节中提及Redis分布式缓存)。以如下方法为例,执行一些处理,最终是调用Cache.Get或者Set方法;

public virtual async Task<TCacheItem?> GetOrAddAsync(
    TCacheKey key,
    Func<Task<TCacheItem>> factory,
    Func<DistributedCacheEntryOptions>? optionsFactory = null,
    bool? hideErrors = null,
    bool considerUow = false,
    CancellationToken token = default)
{
    token = CancellationTokenProvider.FallbackToProvider(token);
    var value = await GetAsync(key, hideErrors, considerUow, token);
    if (value != null)
    {
        return value;
    }


    using (await SyncSemaphore.LockAsync(token))
    {
        value = await GetAsync(key, hideErrors, considerUow, token);
        if (value != null)
        {
            return value;
        }


        value = await factory();


        if (ShouldConsiderUow(considerUow))
        {
            var uowCache = GetUnitOfWorkCache();
            if (uowCache.TryGetValue(key, out var item))
            {
                item.SetValue(value);
            }
            else
            {
                uowCache.Add(key, new UnitOfWorkCacheItem<TCacheItem>(value));
            }
        }


        await SetAsync(key, value, optionsFactory?.Invoke(), hideErrors, considerUow, token);
    }


    return value;
}
  • IDistributedCacheKeyNormalizer

DistributedCache<TCacheItem,TCacheKey>,对于TCacheKey中的名字,默认格式如下代码中,如想要自己指定格式,可以override该方法。

public virtual string NormalizeKey(DistributedCacheKeyNormalizeArgs args)
{
    var normalizedKey = $"c:{args.CacheName},k:{DistributedCacheOptions.KeyPrefix}{args.Key}";


    if (!args.IgnoreMultiTenancy && CurrentTenant.Id.HasValue)
    {
        normalizedKey = $"t:{CurrentTenant.Id.Value},{normalizedKey}";
    }


    return normalizedKey;
}

在DistributedCache<TCacheItem,TCacheKey>本身的方法中,也可以重写该方法,因此想要按照TCacheKey转换期望的名字,可以有多种方式。

protected virtual string NormalizeKey(TCacheKey key)
{
    return KeyNormalizer.NormalizeKey(
        new DistributedCacheKeyNormalizeArgs(
            key.ToString()!,
            CacheName,
            IgnoreMultiTenancy
        )
    );
}
  • IDistributedCacheSerializer

提供对TCacheItem的序列化,其实现中调用IJsonSerializer,最终调用Json序列化包。如果没有特别需求,也无需扩展了,当然此处可以参照Utf8JsonDistributedCacheSerializer扩展自己的缓存序列化服务。

ICacheSupportsMultipleItems

该接口中扩展了一些批量操作的方法,简要查看如下,提供了批量查询,写入,刷新和删除等。

public interface ICacheSupportsMultipleItems
{
    Task<byte[]?[]> GetManyAsync(IEnumerable<string> keys,CancellationToken token = default);
    Task SetManyAsync(IEnumerable<KeyValuePair<string, byte[]>> items,DistributedCacheEntryOptions options,CancellationToken token = default);
    Task RefreshManyAsync(IEnumerable<string> keys,CancellationToken token = default);
    Task RemoveManyAsync(IEnumerable<string> keys,CancellationToken token = default);
}

该接口在DistributedCache<TCacheItem,TCacheKey>中,先进行判断是否有继承该类型,没有的情况下,走SetManyFallbackAsync方法其内部循环调用Cache.Set方法。有则调用cacheSupportsMultipleItems中的方法。该接口在AbpRedisCache中有使用到。

public virtual async Task SetManyAsync(
    IEnumerable<KeyValuePair<TCacheKey, TCacheItem>> items,
    DistributedCacheEntryOptions? options = null,
    bool? hideErrors = null,
    bool considerUow = false,
    CancellationToken token = default)
{
    var itemsArray = items.ToArray();


    var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
    if (cacheSupportsMultipleItems == null)
    {
        await SetManyFallbackAsync(
            itemsArray,
            options,
            hideErrors,
            considerUow,
            token
        );


        return;
    }


    async Task SetRealCache()
    {
        hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;


        try
        {
            await cacheSupportsMultipleItems.SetManyAsync(
                ToRawCacheItems(itemsArray),
                options ?? DefaultCacheOptions,
                CancellationTokenProvider.FallbackToProvider(token)
            );
        }
        catch (Exception ex)
        {
            if (hideErrors == true)
            {
                await HandleExceptionAsync(ex);
                return;
            }


            throw;
        }
    }


    if (ShouldConsiderUow(considerUow))
    {
        var uowCache = GetUnitOfWorkCache();


        foreach (var pair in itemsArray)
        {
            if (uowCache.TryGetValue(pair.Key, out _))
            {
                uowCache[pair.Key].SetValue(pair.Value);
            }
            else
            {
                uowCache.Add(pair.Key, new UnitOfWorkCacheItem<TCacheItem>(pair.Value));
            }
        }


        UnitOfWorkManager.Current?.OnCompleted(SetRealCache);
    }
    else
    {
        await SetRealCache();
    }
}

Redis缓存

在Abp.Redis包中,并没有对IDistributedCache<TCacheItem,TCacheKey>做扩展,实则是在Microsoft.IDistributedCache对应的RedisCache下做的实现。

public class AbpRedisCache : RedisCache, ICacheSupportsMultipleItems
{
  
}

204832287_9bc1b94b-65cf-4ff4-ad1e-98702afef05b 该模块中服务注册时,会调用AddStackExchangRedisCache配置Redis参数,默认取参数格式为,取值格式固定,没有提供自定义扩展。

{
  "Redis":{
    "IsEnabled":false,
    "Configuration":""
  }
}

在AbpRedisCache中,实现了ICacheSupportsMultipleItems中的批量方法,而对于基础的单个方法如GetAsync,SetAsync,则由StackExchangeRedis.RedisCache中提供。

因此,这个模块存在的意义并不是很大,如果想要使用Redis缓存,可以直接调用AddStackExchangRedisCache配置即可,因为从IDistributedCache<TCacheItem,TCacheKey>调用Microsoft.IDistributedCache对应的实现则是RedisCache,再使用其中的GetAsync,SetAsync即可。该模块更多的是提供了批量操作。

再次说明Volo.Abp.Caching.StackExchangeRedis包并不是直接扩展IDistributedCache<TCacheItem,TCacheKey>,而是对其内部依赖的IDistributedCache的实现RedisCache进行扩展从而由AbpRedisCache,这个扩展方式不同于其他扩展组件。

扩展

  • 为了指定key名或是替换默认key名实现,可有多种方式,

    • 一种是override在IDistributedCacheKeyNormalizer中的方法,或者写一个同等服务替换默认实现。

    • 还可以override在DistributedCache<TCacheItem,TCacheKey>中的NormalizeKey,直接写期望的格式。

  • 如果想要按照项目中Redis参数配置的格式配置StackExchangeRedis,那么可以直接服务注册中加入AddStackExchangeRedisCache,不需要使用到Abp.Redis包。

2024-09-16,望技术有成后能回来看见自己的脚步。