使用缓存
缓存分类
缓存无处不在,例如:CPU内核缓存,硬盘缓存,操作系统缓存,数据库缓存,等等……
以上这些缓存不受我们控制,本文只谈我们能在开发过程中能做到的缓存设计。
它们大致可分为以下几类
- 数据库结构缓存
- 冗余表
- 冗余字段
- 进程外缓存
- 例如Redis
- 进程内缓存
- 静态变量
- 局部变量
冗余表/字段
有些时候为了优化查询性能,增加一些冗余的设计是很有必要的。
冗余表也称为“清洗表”,它需要结合后台任务来实现的,它通过定时快照方式将复杂的查询转成简单的结果表,让应用程序可以快速获取查询结果。
冗余字段就更简单了,它可以减少关联查询或者子查询的使用场景,简化查询,提升性能。
进程外缓存
将一些复杂的查询结果或者计算结果 放在独立的缓存服务中,本质上也是一种冗余的设计,通过冗余结果来简化获取过程,最终获得性能的提升。
最常见用法是缓存数据库查询结果,减少数据库的查询压力。
补充说明:
- 进程外缓存不一定性能非常好,它的价值是 减少数据库的压力
- 相比较进程内缓存,它可以实现在多个进程间共享缓存结果
- 数据库通常难以实现水平扩展,而缓存服务则比较容易
局部变量
局部变量(或者私有成员)的作用是减少一些反复的计算任务,例如:重复读取某个配置参数,重复查找集合,等等。
它的作用范围比较小,可以是一个方法内部,也可以是一个实例的生存周期内。
使用场景:
- 反复查找
- 集合/字典
- DOM
- 反复计算(封装)
- 调用方法求值
- 查询数据库求值
- 调用服务求值
- 获取配置
- 创建对象
- 反复打开句柄
- 数据库连接
- Socket连接
- 文件
静态变量
静态变量的生命周期和应用程序的生命周期一样长,可以长时间缓存结果,会有非常好的性能。
做为对比,从Redis获取结果会经过以下步骤:
- 对象序列化(Set方法)
- Redis ClientAPI 发起请求
- 网络数据传输
- Redis服务端处理
- 网络数据传输
- Redis ClientAPI 接收响应
- 反序列化
如果使用静态变量做缓存,以上这些步骤都没有了,显然性能可以得到 极大 的提升,
因此,从静态变量中获取结果相比于Redis服务会非常快,反之 Redis其实非常慢!
在程序中使用静态变量需要考虑3个问题
- 线程并发
- 缓存时间
- 内存占用
注意:如果不理解这几个问题,以及不知道解决方法,请不要使用静态变量!!
缓存工具类
为了简化使用过程,ClownFish提供了3个类型可供使用:
- CacheItem<T>
- CacheDictionary<T>
- AppCache
CacheItem用法示例
/// <summary>
/// 缓存允许登录的用户清单
/// </summary>
private static CacheItem<List<LoginUser>> s_users = new CacheItem<List<LoginUser>>(null, DateTime.MinValue, true);
private async Task<LoginUser> FindUser(string loginName)
{
List<LoginUser> list = s_users.Get();
if( list == null ) {
list = await List();
s_users.Set(list, DateTime.Now.AddSeconds(UserCache.CacheSeconds));
}
return list.FirstOrDefault(x => x.LoginName.Is(loginName));
}
CacheDictionary用法示例
/// <summary>
/// 租户相关的缓存工具类
/// </summary>
public static class TenantCache
{
private static readonly CacheDictionary<string> s_cache = new CacheDictionary<string>();
/// <summary>
/// 根据请求头中的 x-custid 获取对应的租户ID
/// </summary>
/// <param name="controller">BaseController实例</param>
/// <returns>租户ID</returns>
public static async Task<string> GetTenanatId( BaseController controller)
{
if( controller == null )
throw new ArgumentNullException(nameof(controller));
string customerId = controller.GetHeader("x-custid");
if( customerId.IsNullOrEmpty() )
throw new ValidationException("当前请求中不包含 x-custid 请求头。");
// 首先从缓存中获取
string tenantId = s_cache.Get(customerId);
if( tenantId.IsNullOrEmpty() ) {
// 缓存中没有,就查询数据库
using( DbContext dbContext = controller.OpenMasterConnection() ) {
string sql = "select tenant_id from p_tenant where code = @code";
var args = new { code = tenantId };
tenantId = await dbContext.CPQuery.Create(sql, args).ExecuteScalarAsync<string>();
if( tenantId.IsNullOrEmpty() )
return null;
}
// 将数据库中查询的结果放入缓存
s_cache.Set(tenantId, tenantId, DateTime.Now.AddYears(1));
}
return tenantId;
}
}
AppCache是一个静态类,
内部包含了一个CacheDictionary<object>成员,提供了3个公开方法供调用
/// <summary>
/// 简单的缓存工具类,供应用程序所有业务代码共用
/// </summary>
public static class AppCache
{
/// <summary>
/// 尝试从缓存中获取一个对象,如果缓存对象不存在,则调用“加载委托”进行加载并存入缓存。
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key">缓存键</param>
/// <param name="loadFunc">对象“加载委托”,用于当缓存对象不存时获取对象,调用结束后,新产生的对象将插入缓存。</param>
/// <returns></returns>
public static T GetObject<T>(string key, Func<T> loadFunc = null) where T : class
/// <summary>
/// 将一个对象添加到缓存中。
/// </summary>
/// <param name="key">缓存键</param>
/// <param name="value">需要缓存的对象</param>
/// <param name="expiration">缓存的过期时间</param>
public static void SetObject(string key, object value, DateTime expiration)
/// <summary>
/// 删除指定键对应的缓存对象
/// </summary>
/// <param name="key">缓存键</param>
public static void RemoveObject(string key)
}
缓存注意事项
对于SaaS应用程序来说,缓存一定要考虑多租户,因此缓存Key应该包含租户ID