[Abp 源码分析]八、缓存管理


0.简介

缓存在一个业务系统中十分重要,常用的场景就是用来储存调用频率较高的数据。Abp 也提供了一套缓存机制供用户使用,在使用 Abp 框架的时候可以通过注入 ICacheManager 来新建/设置缓存。

同时 Abp 框架也提供了 Redis 版本的 ICacheManager 实现,你也可以很方便的将现有的内存缓存替换为 Redis 缓存。

0.1 典型使用方法

public class TestAppService : ApplicationService
{
    private readonly ICacheManager _cacheMgr;
    private readonly IRepository _rep;

	// 注入缓存管理器与测试实体的仓储
    public TestAppService(ICacheManager cacheMgr, IRepository rep)
    {
        _cacheMgr = cacheMgr;
        _rep = rep;
    }

    public void TestMethod()
    {
        // 获取/创建一个新的缓存
        var cache = _cacheMgr.GetCache("缓存1");
        // 转换为强类型的缓存
        var typedCache = cache.AsTyped();

        // 获取缓存的数据,如果存在则直接返回。
        // 如果不存在则执行工厂方法,将其值存放到
        // 缓存项当中,最后返回缓存项数据。
        var cacheValue = typedCache.Get(10, id => _rep.Get(id).Name);

        Console.WriteLine(cacheValue);
    }
}

1.启动流程

同其他的基础设施一样,缓存管理器 ICacheManager 在 Abp 框架启动的时候就自动被注入到了 Ioc 容器当中,因为他的基类 CacheManagerBase 继承了 ISingletonDependency 接口。

public abstract class CacheManagerBase : ICacheManager, ISingletonDependency
{
	// ... 其他代码
}

其次就是他的 ICachingConfiguration 缓存配置是在 AbpCoreInstaller 注入到 Ioc 容器,并且同其他基础设施的配置一起被集成到了 IAbpStartupConfiguration

    internal class AbpCoreInstaller : IWindsorInstaller
    {
        public void Install(IWindsorContainer container, IConfigurationStore store)
        {
            container.Register(
            	// 其他被注入的基础设施配置
            	
                Component.For().ImplementedBy().LifestyleSingleton()
                
                // 其他被注入的基础设施配置
                );
        }
    }

你可以在其他模块的 PreInitialize() 方法里面可以直接通过 Configuration.Caching 来配置缓存过期时间等功能。

public override void PreInitialize()
{
    Configuration.Caching.ConfigureAll(z=>z.DefaultSlidingExpireTime = TimeSpan.FromHours(1));
}

2. 代码分析

缓存这块可能是 Abp 框架实现当中最简单的一部分了,代码量不多,但是设计思路还是值得借鉴的。

2.1 缓存管理器

2.1.1 基本定义

缓存管理器即 ICacheManager ,通常它用于管理所有缓存,他的接口定义十分简单,就两个方法:

public interface ICacheManager : IDisposable
{
    // 获得所有缓存
    IReadOnlyList GetAllCaches();
    
    // 根据缓存名称获取缓存
    [NotNull] ICache GetCache([NotNull] string name);
}

2.1.2 获取/创建缓存

Abp 实现了一个抽象基类 CacheBase 实现了本接口,在 CacheBase 内部维护了一个 ConcurrentDictionary 字典,这个字典里面就是存放的所有缓存。

同时在他的 GetCache(string name) 内部呢,通过传入的缓存名字来从字典获取已经存在的缓存,如果不存在呢,执行其工厂方法来创建一个新的缓存。

public virtual ICache GetCache(string name)
{
    Check.NotNull(name, nameof(name));

    // 从字典根据名称取得缓存,不存在则使用工厂方法
    return Caches.GetOrAdd(name, (cacheName) =>
    {
        // 得到创建成功的缓存
        var cache = CreateCacheImplementation(cacheName);

        // 遍历缓存配置集合,查看当前名字的缓存是否存在配置项
        var configurators = Configuration.Configurators.Where(c => c.CacheName == null || c.CacheName == cacheName);

        // 遍历这些配置项执行配置操作,更改缓存的过期时间等参数
        foreach (var configurator in configurators)
        {
            configurator.InitAction?.Invoke(cache);
        }

        // 返回配置完成的缓存
        return cache;
    });
}

// 真正创建缓存的方法
protected abstract ICache CreateCacheImplementation(string name);

这里的 CreateCacheImplementation()由具体的缓存管理器实现的缓存创建方法,因为 Redis 与 MemoryCache 的实现各不一样,所以这里定义了一个抽象方法。

2.1.3 缓存管理器销毁

当缓存管理器被销毁的时候,首先是遍历字典内存储的所有缓存,并通过 IIocManager.Release() 方法来释放这些缓存,之后则是调用字典的 Clear() 方法清空字典。

public virtual void Dispose()
{
    DisposeCaches();
    // 清空字典
    Caches.Clear();
}

// 遍历字典,释放对象
protected virtual void DisposeCaches()
{
    foreach (var cache in Caches)
    {
        IocManager.Release(cache.Value);
    }
}

2.1.4 内存缓存管理器

Abp 对于缓存管理器的默认实现是 AbpMemoryCacheManager ,其实没多复杂,就是实现了基类的 CreateCacheImplementation() 返回特定的 ICache

public class AbpMemoryCacheManager : CacheManagerBase
{
	// ... 忽略了的代码

    protected override ICache CreateCacheImplementation(string name)
    {
    	// 就 new 一个新的内存缓存而已,内存缓存的实现请看后面的
    	// 这里是因为 AbpMemory 没有注入到 IOC 容器,所以需要手动 new
        return new AbpMemoryCache(name)
        {
            Logger = Logger
        };
    }

	// 重写了基类的缓存释放方法
    protected override void DisposeCaches()
    {
        foreach (var cache in Caches.Values)
        {
            cache.Dispose();
        }
    }
}

2.1.5 Redis 缓存管理器

如果要使用 Redis 缓存管理器,根据模块的加载顺序,你需要在启动模块的 PreInitialize() 调用 Abp.Redis 库提供的集成方法即可。

这里先来看看他的实现:

public class AbpRedisCacheManager : CacheManagerBase
{
    public AbpRedisCacheManager(IIocManager iocManager, ICachingConfiguration configuration)
        : base(iocManager, configuration)
    {
    	// 注册 Redis 缓存
        IocManager.RegisterIfNot(DependencyLifeStyle.Transient);
    }

    protected override ICache CreateCacheImplementation(string name)
    {
    	// 解析已经注入的 Redis 缓存
    	// 这里可以看到解析的时候如何传入构造参数
        return IocManager.Resolve(new { name });
    }
}

一样的,非常简单,没什么可以说的。

2.2 缓存

我们从缓存管理器当中拿到具体的缓存之后才能够进行真正的缓存操作,这里需要明确的一个概念是缓存是一个缓存项的集合,缓存项里面的值才是我们真正缓存的结果。

就如同一个用户表,他拥有多条用户数据,那么我们要针对这个用户表做缓存,就会创建一个缓存名称叫做 "用户表" 的缓存,在需要获得用户数据的时候,我们拿去数据就直接从这个 "用户表" 缓存当中取得具体的缓存项,也就是具体的用户数据。

其实每个缓存项也是几个 键值对 ,键就是缓存的键,以上面的 "用户表缓存" 为例子,那么他缓存项的键就是 int 型的 Id ,他的值呢就是一个用户实体。

2.2.1 基本定义

所有缓存的定义都在 ICache 当中,每个缓存都拥有增删查改这些基本操作,并且还拥有过期时间与名称等属性。

同样,缓存也有一个抽象基类的实现,名字叫做 CacheBase 。与缓存管理器的抽象基类一样,CacheBase 内部仅实现了 Get 方法的基本逻辑,其他的都是抽象方法,需要由具体的类型进行实现。

public interface ICache : IDisposable
{
	// 缓存名称
    string Name { get; }
    
    // 相对过期时间
    TimeSpan DefaultSlidingExpireTime { get; set; }

    // 绝对过期时间
    TimeSpan? DefaultAbsoluteExpireTime { get; set; }

    // 根据缓存项 Key 获取到缓存的数据,不存在则执行工厂方法
    object Get(string key, Func factory);

    // Get 的异步实现
    Task GetAsync(string key, Func> factory);

    // 根据缓存项 Key 获取到缓存的数据,没有则返回默认值,一般为 null
    object GetOrDefault(string key);

    // GetOrDefault 的异步实现
    Task GetOrDefaultAsync(string key);

    // 设置缓存项值和过期时间等参数
    void Set(string key, object value, TimeSpan? slidingExpireTime = null, TimeSpan? absoluteExpireTime = null);

    // Set 的异步实现
    Task SetAsync(string key, object value, TimeSpan? slidingExpireTime = null, TimeSpan? absoluteExpireTime = null);

	// 移除指定缓存名称的缓存项
    void Remove(string key);

    // Remove 的异步实现
    Task RemoveAsync(string key);

    // 清空缓存内所有缓存项
    void Clear();

	// Clear 的异步实现
    Task ClearAsync();
}

2.2.2 内存缓存的实现

这里我们以 Abp 的默认 MemoryCache 实现为例子来看看里面是什么构造:

public class AbpMemoryCache : CacheBase
{
    private MemoryCache _memoryCache;
	
	// 初始化 MemoryCahce
    public AbpMemoryCache(string name)
        : base(name)
    {
        _memoryCache = new MemoryCache(new OptionsWrapper(new MemoryCacheOptions()));
    }

	// 从 MemoryCahce 取得缓存
    public override object GetOrDefault(string key)
    {
        return _memoryCache.Get(key);
    }

	// 设置缓存
    public override void Set(string key, object value, TimeSpan? slidingExpireTime = null, TimeSpan? absoluteExpireTime = null)
    {
    	// 值为空的时候抛出异常
        if (value == null)
        {
            throw new AbpException("Can not insert null values to the cache!");
        }

        if (absoluteExpireTime != null)
        {
            _memoryCache.Set(key, value, DateTimeOffset.Now.Add(absoluteExpireTime.Value));
        }
        else if (slidingExpireTime != null)
        {
            _memoryCache.Set(key, value, slidingExpireTime.Value);
        }
        else if (DefaultAbsoluteExpireTime != null)
        {
            _memoryCache.Set(key, value, DateTimeOffset.Now.Add(DefaultAbsoluteExpireTime.Value));
        }
        else
        {
            _memoryCache.Set(key, value, DefaultSlidingExpireTime);
        }
    }

	// 删除缓存
    public override void Remove(string key)
    {
        _memoryCache.Remove(key);
    }

	// 清空缓存
    public override void Clear()
    {
        _memoryCache.Dispose();
        _memoryCache = new MemoryCache(new OptionsWrapper(new MemoryCacheOptions()));
    }

    public override void Dispose()
    {
        _memoryCache.Dispose();
        base.Dispose();
    }
}

可以看到在 AbpMemoryCache 内部就是将 MemoryCahce 进行了一个二次包装而已。

其实可以看到这些缓存超期时间之类的参数 Abp 自己并没有用到,而是将其传递给具体的缓存实现来进行管理。

2.2.3 Redis 缓存的实现

Abp.Redis 库使用的是 StackExchange.Redis 库来实现对 Redis 的通讯的,其实现为 AbpRedisCache ,里面也没什么好说的,如同内存缓存一样,实现那些抽象方法就可以了。

public class AbpRedisCache : CacheBase
{
    private readonly IDatabase _database;
    private readonly IRedisCacheSerializer _serializer;

    public AbpRedisCache(
        string name, 
        IAbpRedisCacheDatabaseProvider redisCacheDatabaseProvider, 
        IRedisCacheSerializer redisCacheSerializer)
        : base(name)
    {
        _database = redisCacheDatabaseProvider.GetDatabase();
        _serializer = redisCacheSerializer;
    }

    // 获取缓存
    public override object GetOrDefault(string key)
    {
        var objbyte = _database.StringGet(GetLocalizedKey(key));
        return objbyte.HasValue ? Deserialize(objbyte) : null;
    }

    public override void Set(string key, object value, TimeSpan? slidingExpireTime = null, TimeSpan? absoluteExpireTime = null)
    {
        if (value == null)
        {
            throw new AbpException("Can not insert null values to the cache!");
        }

        //TODO: 这里是一个解决实体序列化的方法.
        //TODO: 通常实体不应该存储在缓存当中,目前 Abp.Zero 包是这样来进行处理的,这个问题将会在未来被修正.
        var type = value.GetType();
        if (EntityHelper.IsEntity(type) && type.GetAssembly().FullName.Contains("EntityFrameworkDynamicProxies"))
        {
            type = type.GetTypeInfo().BaseType;
        }

        _database.StringSet(
            GetLocalizedKey(key),
            Serialize(value, type),
            absoluteExpireTime ?? slidingExpireTime ?? DefaultAbsoluteExpireTime ?? DefaultSlidingExpireTime
            );
    }

    // 移除缓存
    public override void Remove(string key)
    {
        _database.KeyDelete(GetLocalizedKey(key));
    }

    // 清空缓存
    public override void Clear()
    {
        _database.KeyDeleteWithPrefix(GetLocalizedKey("*"));
    }

    // 序列化对象
    protected virtual string Serialize(object value, Type type)
    {
        return _serializer.Serialize(value, type);
    }

    // 反序列化对象
    protected virtual object Deserialize(RedisValue objbyte)
    {
        return _serializer.Deserialize(objbyte);
    }

    // 获得缓存的 Key
    protected virtual string GetLocalizedKey(string key)
    {
        return "n:" + Name + ",c:" + key;
    }
}

2.3 缓存配置

缓存配置的作用就是可以为每个缓存配置不同的过期时间,我们最开始说过 Abp 是通过 ICachingConfiguration 来配置缓存的,在这个接口里面呢定义了这样几个东西。

public interface ICachingConfiguration
{
    // 配置项集合
    IReadOnlyList Configurators { get; }

    // 配置所有缓存
    void ConfigureAll(Action initAction);

    // 配置指定名称的缓存
    void Configure(string cacheName, Action initAction);
}

Emmmm,可以看到他有个 Configurators 属性存了一大堆 ICacheConfigurator ,这个玩意儿呢就是对应到具体缓存的配置项了。

public interface ICacheConfigurator
{
    // 关联的缓存名称
    string CacheName { get; }

    // 缓存初始化的时候执行的配置操作
    Action InitAction { get; }
}

这玩意儿的实现也没什么好看的,跟接口差不多,这下我们知道了缓存的配置呢就是存放在 Configurators 里面的。

然后呢,就在我们最开始的地方,缓存管理器创建缓存的时候不是根据名字去遍历这个 Configurators 集合么,在那里面就直接通过这个 ICacheConfiguratorAction 来配置缓存的超期时间。

至于 Configure()ConfigureAll() 方法嘛,前者就是根据你传入的缓存名称初始化一个 CacheConfigurator ,然后扔到那个列表里面去。

private readonly List _configurators;

public void Configure(string cacheName, Action initAction)
{
    _configurators.Add(new CacheConfigurator(cacheName, initAction));
}

后者的话则是添加了一个没有名字的 CacheConfigurator ,正因为没有名字,所以他的 cacheName 肯定 null,也就是在缓存管理器创建缓存的时候如果该缓存没有对应的配置,那么就会使用这个名字为空的 CacheConfigurator 了。

2.4 强类型缓存

在最开始的使用方法里面可以看到我们通过 AsType() 方法将 ICache 对象转换为 ITypedCache ,这样我们就无需再将缓存项手动进行强制类型转换。

注:虽然这里是指定了泛型操作,但是呢,在其内部实现还是进行的强制类型转换,也是会发生装/拆箱操作的。

Abp 自己则通过 TypedCacheWrapper 来将原有的 ICache 缓存包装为 ITypedCache

看看这个扩展方法的定义,他是放在 CacheExtensions 里面的:

public static ITypedCache AsTyped(this ICache cache)
{
    return new TypedCacheWrapper(cache);
}

Emmm,这里是 new 了一个 TypedCacheWrapper 来处理的,从方法定义可以看出来 TypedCacheWrapper 是 ITypedCache 的一个默认实现。

ITypedCache 拥有 ICache 的所有方法签名,所以使用 ITypedCache 与使用 ICache 的方式是一样的。

TypedCacheWrapper 的各种方法其实就是调用的传入的 ICache 对象的方法,只不过在返回值得时候他自己进行了强制类型转换而已,比如说,看看他的 Get 方法。

public class TypedCacheWrapper : ITypedCache
{
    // 返回的是内部 ICache 的名称
    public string Name
    {
        get { return InternalCache.Name; }
    }

    public TimeSpan DefaultSlidingExpireTime
    {
        get { return InternalCache.DefaultSlidingExpireTime; }
        set { InternalCache.DefaultSlidingExpireTime = value; }
    }
    public TimeSpan? DefaultAbsoluteExpireTime
    {
        get { return InternalCache.DefaultAbsoluteExpireTime; }
        set { InternalCache.DefaultAbsoluteExpireTime = value; }
    }

	// 调用 AsTyped() 方法时候传入的 ICache 对象
    public ICache InternalCache { get; private set; }

    public TypedCacheWrapper(ICache internalCache)
    {
        InternalCache = internalCache;
    }

	// 调用的是一个 ICache 的扩展方法
    public TValue Get(TKey key, Func factory)
    {
        return InternalCache.Get(key, factory);
    }
    
    // ..... 忽略了其他方法
}

看看 InternalCache.Get(key, factory); 这个扩展方法的定义吧:

public static TValue Get(this ICache cache, TKey key, Func factory)
{
    // 本质上就是调用的 ICache 的 Get 方法,返回的时候进行了强制类型转换而已
    return (TValue)cache.Get(key.ToString(), (k) => (object)factory(key));
}

3.