缓存在一个大型一点的系统里面是必然会涉及到的,合理的使用缓存能够给我们的系统带来更高的响应速度。由于数据提供服务涉及到数据库的相关操作,如果客户端的并发数量超过一定的数量,那么数据库的请求处理则以爆发式增长,如果数据库服务器无法快速处理这些并发请求,那么将会增加客户端的请求时间,严重者可能导致数据库服务或者应用服务直接瘫痪。缓存方案就是为这个而诞生,随着缓存的引入,可以把数据库的IO耗时操作,转换为内存数据的快速响应操作,或者把整个页面缓存到缓存系统里面。本篇随笔主要介绍利用ABP框架的支持实现的服务端缓存处理和Winform客户端缓存的处理。
1、缓存文章回顾
缓存的重要性不言而喻,我在博客园里面也写了很多缓存相关的文章,都是基于实际系统的总结处理。
《Winform里面的缓存使用》
《使用ConcurrentDictionary替代Hashtable对多线程的对象缓存处理》
《在.NET项目中使用PostSharp,使用MemoryCache实现缓存的处理》
《.NET缓存框架CacheManager在混合式开发框架中的应用(1)-CacheManager的介绍和使用》
《在.NET项目中使用PostSharp,使用CacheManager实现多种缓存框架的处理》
《在Winform开发框架中下拉列表绑定字典以及使用缓存提高界面显示速度》
《C#开发微信门户及应用(48) - 在微信框架中整合CacheManager 缓存框架》
上面这些都是和缓存相关的内容,一般来说,缓存有很多方式的实现,如MemoryCache、Redis、Memcached、Couchbase、System.Web.Caching等,为了方便我们一般使用.net的内存缓存处理,如果我们需要序列化缓存内容,那么可以采用MemoryCache或者Redis缓存等。后来我们通过综合考虑,基于配置方式选择不同缓存方式,在后端一般可以使用CacheManager 的缓存处理。
如下面是基于常规架构的缓存处理分层,如果是基于Web API的服务端,那么缓存一般可以在Web API层或者它的下面一层。
如果是基于可序列化的缓存处理,它在IIS或者其他Web 容器重新启动后,缓存不会丢失,如在Redis里面,有相关的缓存记录如下所示。
2、ABP服务端缓存处理
ABP提供了缓存的抽象,它内部使用了这个缓存抽象。虽然默认的实现使用了MemoryCache,通过配置也可以使用Redis等缓存,缓存的主要接口ICacheManager。
我们可以在应用服务层的构造函数里面,注入该接口,然后使用该接口获得一个缓存对象。
官方简单的应用服务层代码如下所示。
public class TestAppService : ApplicationService
{
private readonly ICacheManager _cacheManager;
public TestAppService(ICacheManager cacheManager)
{
_cacheManager = cacheManager;
}
实际上,我们应用服务层应该会更加复杂一些,如下是我们ABP快速开发框架的应用服务层的代码
[AbpAuthorize]
public class DictDataAppService : MyAsyncServiceBase, IDictDataAppService
{
///
/// 缓存管理接口
///
private readonly ICacheManager _cacheManager;
private readonly IRepository _repository;
public DictDataAppService(IRepository repository, ICacheManager cacheManager) : base(repository)
{
_repository = repository;
_cacheManager = cacheManager;//依赖注入缓存
}
对于字典模块,我们一般获取接口如下所示。
///
/// 根据字典类型ID获取所有该类型的字典列表集合(Key为名称,Value为值)
///
/// 字典类型ID
///
public async Task> GetDictByTypeID(string dictTypeId)
{
IList list = await Repository.GetAllListAsync(s => s.DictType_ID == dictTypeId);
Dictionary dict = new Dictionary();
foreach (DictData info in list)
{
if (!dict.ContainsKey(info.Name))
{
dict.Add(info.Name, info.Value);
}
}
return dict;
}
如果我们需要把它构建一个缓存接口,那么处理方式就是对它进行一个简单包装即可,如下代码所示。
///
/// 根据字典类型ID获取所有该类型的字典列表集合(使用缓存)
///
/// 字典类型ID
///
public async Task> GetDictByTypeIDCached(string dictTypeId)
{
//系统缓存默认为60分钟,可以在模块中配置具体的时间,配置后则是具体配置时间
return await _cacheManager.GetCache("DictDataAppService")
.GetAsync(dictTypeId, () => GetDictByTypeID(dictTypeId));
}
默认缓存超时是60分钟,它可以改。如果你超过60分钟没有使用缓存中的项,会从缓存中自动移除。你可以配置指定的缓存或是全部的缓存。
我们可以在应用服务层模块类ApplicationModule类里面进行修改,实现对缓存的过期设置。
//系统缓存默认为60分钟,可以在模块中配置具体的时间,配置后则是具体配置时间
//所有缓存设置为2小时
Configuration.Caching.ConfigureAll(cache =>
{
cache.DefaultSlidingExpireTime = TimeSpan.FromHours(2);
});
//特殊指定为5分钟
Configuration.Caching.Configure("DictDataAppService", cache =>
{
cache.DefaultSlidingExpireTime = TimeSpan.FromMinutes(5);
});
Redis 缓存集成
默认缓存管理使用的是内存缓存。所以,如果你有多个并发的Web服务器使用同个应用,可能会成为一个问题,在这种情况下,你需要一个分布/集中缓存服务,你就可以简单的使用Redis做为你的缓存服务器。
Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
Redis的代码遵循ANSI-C编写,可以在所有POSIX系统(如Linux, *BSD, Mac OS X, Solaris等)上安装运行。而且Redis并不依赖任何非标准库,也没有编译参数必需添加。
下载地址:https://github.com/MSOpenTech/redis/releases下载,安装为Windows服务即可。
安装后作为Windows服务运行,安装后可以在系统的服务里面看到Redis的服务在运行了,如下图所示。
安装好Redis后,还有一个Redis伴侣Redis Desktop Manager需要安装,这样可以实时查看Redis缓存里面有哪些数据,具体地址如下:http://redisdesktop.com/download
下载属于自己平台的版本即可
下载安装后,打开运行界面,如果我们往里面添加键值的数据,那么可以看到里面的数据了。
我们来看看如何在ABP框架中使用Redis缓存
我们现在应用服务层模块里面配置好使用Redis,如下代码所示
[DependsOn(
................
typeof(AbpRedisCacheModule) //Redis缓存加入
)]
public class ApplicationModule : AbpModule
{
public override void PreInitialize()
{
............
//使用Redis缓存
int DatabaseId = -1;
int.TryParse(AppSettingConfig.GetAppSetting("RedisCache", "DatabaseId"), out DatabaseId);
string connectionString = AppSettingConfig.GetAppSetting("RedisCache", "ConnectionString");
Configuration.Caching.UseRedis(options =>
{
options.ConnectionString = connectionString;
options.DatabaseId = DatabaseId;
});
//系统缓存默认为60分钟,可以在模块中配置具体的时间,配置后则是具体配置时间
//所有缓存设置为2小时
//Configuration.Caching.ConfigureAll(cache =>
//{
// cache.DefaultSlidingExpireTime = TimeSpan.FromHours(2);
//});
//特殊指定为5分钟
Configuration.Caching.Configure("DictDataAppService", cache =>
{
cache.DefaultSlidingExpireTime = TimeSpan.FromMinutes(5);
});
}
Host项目配置文件,Appsetting.json配置文件如下所示,增加RedisCache的配置节点。
使用缓存处理的应用服务层接口实现如下所示
///
/// 根据字典类型ID获取所有该类型的字典列表集合(使用缓存)
///
/// 字典类型ID
///
public async Task> GetDictByTypeIDCached(string dictTypeId)
{
//系统缓存默认为60分钟,可以在模块中配置具体的时间,配置后则是具体配置时间
return await _cacheManager.GetCache("DictDataAppService").GetAsync(dictTypeId, () => GetDictByTypeID(dictTypeId));
}
在测试接口页面中进行测试
查看缓存管理里面的内容,可以发现已经具有值了,如下所示。
这样我们就可以很容易的从内存缓存切换到Redis的缓存了。
实体缓存
虽然ABP缓存系统出于普通的目的,但有一个EntityCache基类,可帮你缓存实体。如果我们通过它们的Id获取的实体,我们可以用这个基类缓存它们,就不用再频繁地从数据库查询。
不过这里不对这个进行细讲了。
3、Winform客户端的缓存处理
除了在服务端进行缓存测试外,为了提高客户端的响应速度,我们还可以在Winform客户端中使用内存缓存进行缓存一些不常变化的内容的,这样可以避免频繁的请求网络接口,获取接口数据。
ABP基础模块里面也提供了一个简单的缓存类,我们可以使用它进行缓存处理。
我曾经在之前一篇随笔《在Winform开发框架中下拉列表绑定字典以及使用缓存提高界面显示速度》对字典模块中使用缓存进行了说明,这个我们也可以调整为ABP快速开发框架中Winform客户端的字典处理方式。
ABP中有两种cache的实现方式:MemroyCache 和 RedisCache. 如下图,两者都继承至ICache接口。ABP核心模块封装了MemroyCache 来实现ABP中的默认缓存功能。 Abp.RedisCache这个模块封装RedisCache来实现缓存。
我们可以在Winform客户端中使用AbpMemoryCache是实现内存缓存的处理。
例如我们在界面模块中使用一个字典辅助类来封装对字典模块的调用,同时可以使用缓存方式进行获取。
使用缓存处理的逻辑,如下所示
主要就是判断键值是否存在,否则就设置内存缓存即可。
然后在编写一个字典控件的扩展函数,如下所示。
///
/// 绑定下拉列表控件为指定的数据字典列表
///
/// 下拉列表控件
/// 数据字典类型名称
/// 控件默认值
/// 是否添加空行
public static void BindDictItems(this ComboBoxEdit control, string dictTypeName, string defaultValue, bool isCache = true, bool emptyFlag = true)
{
var dict = GetDictByDictType(dictTypeName, isCache);
List itemList = new List();
foreach (string key in dict.Keys)
{
itemList.Add(new CListItem(key, dict[key]));
}
control.BindDictItems(itemList, defaultValue, emptyFlag);
}
绑定字典控件使用的时候,就非常简单了,如下代码是实际项目中对字典列表绑定的操作,字典数据在字典模块里面统一定义的。
///
/// 初始化数据字典
///
private void InitDictItem()
{
txtInDiagnosis.BindDictItems("入院诊断");
txtLeaveDiagnosis.BindDictItems("最后诊断");
//初始化代码
this.txtFollowType.BindDictItems("随访方式");
this.txtFollowStatus.BindDictItems("随访状态");
}
这样就非常简化了我们对字典数据源的绑定操作了,非常方便易读,下面是其中一个功能界面的下拉列表展示。
使用缓存接口,对于大量字典数据显示的界面,界面显示速度有了不错的提升。