使用缓存

缓存分类

缓存无处不在,例如: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