0.简介

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

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

0.1 典型使用方法

public class TestAppService : ApplicationService
{
private readonly ICacheManager _cacheMgr;
private readonly IRepository<TestEntity> _rep; // 注入缓存管理器与测试实体的仓储
public TestAppService(ICacheManager cacheMgr, IRepository<TestEntity> rep)
{
_cacheMgr = cacheMgr;
_rep = rep;
} public void TestMethod()
{
// 获取/创建一个新的缓存
var cache = _cacheMgr.GetCache("缓存1");
// 转换为强类型的缓存
var typedCache = cache.AsTyped<int, string>(); // 获取缓存的数据,如果存在则直接返回。
// 如果不存在则执行工厂方法,将其值存放到
// 缓存项当中,最后返回缓存项数据。
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<ICachingConfiguration, CachingConfiguration>().ImplementedBy<CachingConfiguration>().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<ICache> GetAllCaches(); // 根据缓存名称获取缓存
[NotNull] ICache GetCache([NotNull] string name);
}

2.1.2 获取/创建缓存

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

同时在他的 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<AbpRedisCache>(DependencyLifeStyle.Transient);
} protected override ICache CreateCacheImplementation(string name)
{
// 解析已经注入的 Redis 缓存
// 这里可以看到解析的时候如何传入构造参数
return IocManager.Resolve<AbpRedisCache>(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<string, object> factory); // Get 的异步实现
Task<object> GetAsync(string key, Func<string, Task<object>> factory); // 根据缓存项 Key 获取到缓存的数据,没有则返回默认值,一般为 null
object GetOrDefault(string key); // GetOrDefault 的异步实现
Task<object> 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<MemoryCacheOptions>(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<MemoryCacheOptions>(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<ICacheConfigurator> Configurators { get; } // 配置所有缓存
void ConfigureAll(Action<ICache> initAction); // 配置指定名称的缓存
void Configure(string cacheName, Action<ICache> initAction);
}

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

public interface ICacheConfigurator
{
// 关联的缓存名称
string CacheName { get; } // 缓存初始化的时候执行的配置操作
Action<ICache> InitAction { get; }
}

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

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

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

private readonly List<ICacheConfigurator> _configurators;

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

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

2.4 强类型缓存

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

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

Abp 自己则通过 TypedCacheWrapper<TKey, TValue> 来将原有的 ICache 缓存包装为 ITypedCache<TKey, TValue>

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

public static ITypedCache<TKey, TValue> AsTyped<TKey, TValue>(this ICache cache)
{
return new TypedCacheWrapper<TKey, TValue>(cache);
}

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

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

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

public class TypedCacheWrapper<TKey, TValue> : ITypedCache<TKey, TValue>
{
// 返回的是内部 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<TKey, TValue> factory)
{
return InternalCache.Get(key, factory);
} // ..... 忽略了其他方法
}

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

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

3.点此跳转到总目录

[Abp 源码分析]八、缓存管理的更多相关文章

  1. ABP源码分析八:Logger集成

    ABP使用Castle日志记录工具,并且可以使用不同的日志类库,比如:Log4Net, NLog, Serilog... 等等.对于所有的日志类库,Castle提供了一个通用的接口来实现,我们可以很方 ...

  2. ABP源码分析一:整体项目结构及目录

    ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...

  3. ABP源码分析十三:缓存Cache实现

    ABP中有两种cache的实现方式:MemroyCache 和 RedisCache. 如下图,两者都继承至ICache接口(准确说是CacheBase抽象类).ABP核心模块封装了MemroyCac ...

  4. ABP源码分析十八:UI Inputs

    以下图中描述的接口和类都在Abp项目的Runtime/Validation, UI/Inputs目录下的.在当前版本的ABP(0.83)中这些接口和类并没有实际使用到.阅读代码时可以忽略,无需浪费时间 ...

  5. ABP源码分析二十八:ABP.MemoryDB

    这个模块简单,且无实际作用.一般实际项目中都有用数据库做持久化,用了数据库就无法用这个MemoryDB 模块了.原因在于ABP限制了UnitOfWork的类型只能有一个(前文以作介绍),一般用了数据库 ...

  6. [Abp 源码分析]零、文章目录

    0.系列文章目录 一.Abp 框架启动流程分析 二.模块系统 三.依赖注入 四.模块配置 五.系统设置 六.工作单元的实现 七.仓储与 Entity Framework Core 八.缓存管理 九.事 ...

  7. ABP源码分析七:Setting 以及 Mail

    本文主要说明Setting的实现以及Mail这个功能模块如何使用Setting. 首先区分一下ABP中的Setting和Configuration. Setting一般用于需要通过外部配置文件(或数据 ...

  8. ABP源码分析三十四:ABP.Web.Mvc

    ABP.Web.Mvc模块主要完成两个任务: 第一,通过自定义的AbpController抽象基类封装ABP核心模块中的功能,以便利的方式提供给我们创建controller使用. 第二,一些常见的基础 ...

  9. ABP源码分析四十二:ZERO的身份认证

    ABP Zero模块通过自定义实现Asp.Net Identity完成身份认证功能, 对Asp.Net Identity做了较大幅度的扩展.同时重写了ABP核心模块中的permission功能,以实现 ...

随机推荐

  1. Python OpenCV 图像相识度对比

    强大的openCV能做什么我就不啰嗦,你能想到的一切图像+视频处理. 这里,我们说说openCV的图像相似度对比, 嗯,说好听一点那叫图像识别,但严格讲, 图像识别是在一个图片中进行类聚处理,比如图片 ...

  2. 使用Nexus搭建私有Nuget仓库

    前言 Nuget是ASP .NET Gallery的一员,是免费.开源的包管理工具,专注于在.Net / .Net Core应用开发过程中第三方组件库的管理,相对于传统单纯的dll引用要方便.科学得多 ...

  3. apache atlas源码编译打包 centos

    参考:https://atlas.apache.org/InstallationSteps.html https://blog.csdn.net/lingbo229/article/details/8 ...

  4. python连接mysql数据库读取数据

    #-*- coding:utf-8 -*- #Author:'Lmc' #DATE: 2019/4/28/0028 上午 11:22:47 #FileName:test.PY import pymys ...

  5. caffe编译错误记录

    1. caffe.pb.h丢失问题 错误代码如图: zhuoshi@zhuoshi-SYS-7048GR-TR:~/ZSZT/Geoffrey/caffe/caffe-master$ make all ...

  6. I - Infinite Improbability Drive

    I - Infinite Improbability Drivehttp://codeforces.com/gym/241750/problem/I不断构造,先填n-1个0,然后能放1就放1,最后这个 ...

  7. C++的编译预处理

    C++中,在编译器对源程序进行编译之前,首先要由预处理对程序文本进行预处理.预处理器提供了一组预编译处理指令和预处理操作符.预处理指令实际上不是C++语言的一部分,它只是用来扩充C++程序设计的环境. ...

  8. Linux_常用命令简单介绍(netstat,awk,top,tail,head,less,more,cat,nl)

    1.netstat netstat -tnl | grep 443 (查看443端口是否被占用) root用户,用netstat -pnl | grep 443 (还可显示出占用本机443端口的进程P ...

  9. h5唤起APP并检查是否成功

    // 检查app是否打开 function checkOpen(cb) { const clickTime = +(new Date()); function check(elsTime) { if ...

  10. 【原创】C# API 未能创建 SSL/TLS 安全通道 问题解决

    在调用执行API之前添加以下代码就行了 System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;