C#实现自由组合本地缓存、分布式缓存和数据查询

一、背景介绍:

我们在进行数据存储的时候,有时候会加入本地缓存、分布式缓存以及数据库存储三级的结构,当我们取值的时候经常是像下面这样的流程:

1.先取本地缓存,如果值存在直接返回

2.本地缓存不存在,获取分布式缓存,存在直接返回,并更新本地缓存

3.分布式缓存不存在,查询数据库,更新分布式缓存、更新本地缓存,最后返回

但如果对于一些场景,可能只有本地缓存、只有分布式缓存或者说上面三种的几种组合,我们怎么要应对这样的变化,怎么能抽象出一套方式,能够应对各种不同数据存储方式造成的变化。

二、设计思路:

首先我们分析一下上面这个过程的模型,可以抽象出5个方法:

  • 1.GetDataFromLocalCache
  • 2.GetDataFromDistributeCache
  • 3.GetDataFromDB
  • 4.SetDataToLocalCache
  • 5.SetDataToDistributeCache

其实,不同的场景无非就是这几个方法的组合,只不过里面的内容不同罢了,说到这里我们应该已经有思路了,可以利用委托来实现。

三、详细设计:

①定义一个类,包含上面五个方法的委托;

public class DataOperateInput<T>
{
    public Func<T> GetDataFromLocalCache { get; set; } = null;        //获取本地缓存数据
    public Func<T> GetDataFromDistributeCache { get; set; } = null;   //获取分布式缓存数据
    public Func<T> GetDataFromDb { get; set; } = null;                //获取DB数据
    public Action<T> SetDataTolocalCache { get; set; } = null;        //设置本地缓存数据
    public Action<T> SetDataToDistributeCache { get; set; } = null;   //设置分布式缓存数据
}

②实现一个方法,组合这五个方法。

    public class DataOperate
    {
        /// <summary>
        /// 获取数据入口
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="input"></param>
        /// <returns></returns>
        public T GetData<T>(DataOperateInput<T> input) where T : class, new()
        {
            T result = null;

            //需要从本地缓存取
            if (input.GetDataFromLocalCache != null)
            {
                //调用本地缓存委托方法获取值
                result = input.GetDataFromLocalCache();

                if (result != null)
                {
                    return result;
                }
            }
            //获取值为空或者不从本地缓存获取,调用下面的方法,从分布式缓存和Db中获取数据
            return GetDataFromDistributeAndDB(input);
        }

        /// <summary>
        /// 从分布式缓存和Db中获取数据
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="input"></param>
        /// <returns></returns>
        private T GetDataFromDistributeAndDB<T>(DataOperateInput<T> input) where T : class, new()
        {
            T result = null;

            if (input.GetDataFromDistributeCache != null)
            {
                //从缓存中取值
                result = input.GetDataFromDistributeCache();

                //如果需要设置会本地缓存,那么设置
                if (result != null)
                {
                    //如果设置本地缓存的委托存在,调用它设置本地缓存
                    input.SetDataTolocalCache?.Invoke(result);
                }
            }

            //获取值为空或者不从分布式缓存获取,调用下面的方法,从Db中获取数据
            return GetDataFromDB(input);
        }

        /// <summary>
        /// 从数据库中获取数据
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="input"></param>
        /// <returns></returns>
        private T GetDataFromDB<T>(DataOperateInput<T> input) where T : class, new()
        {
            T result = null;
            if (input.GetDataFromDb != null)
            {
                //从DB中取值
                result = input.GetDataFromDb();

                //如果需要设置会分布式缓存和本地缓存,那么设置
                if (result != null)
                {
                    input.SetDataToDistributeCache?.Invoke(result);
                    input.SetDataTolocalCache?.Invoke(result);
                }
            }
            return result;
        }

    }

③ 具体实现一个服务类,和各种GetData、SetData方法;

A.定义一个枚举类,通过这个枚举可以自由组合数据源

/// <summary>
/// 数据源类别
/// </summary>
[Flags]
public enum DataSourceKind
{
    /// <summary>
    /// 本地缓存
    /// </summary>
    LocalCache = 1,
    /// <summary>
    /// 分布式缓存
    /// </summary>
    DistributeCache = 2,
    /// <summary>
    /// 数据库
    /// </summary>
    DataBase = 4
}

B.定义一个具体的实体类,举例我这里定义了一个User类

public class User : IUser
{
    public long UserId { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public int Sex { get; set; }
}

C.实现一个获取用户信息的方法

    /// <summary>
    /// 获取用户数据
    /// </summary>
    /// <param name="userId">用户Id(可以是自己相关的业务代码)</param>
    /// <param name="dataSources">数据源类型(调用方可以自己组合)</param>
    /// <param name="needUpdatelocal">是否需要更新本地缓存</param>
    /// <param name="needUpdateDistribute">是否需要更新分布式缓存</param>
    /// <returns></returns>
    public User GetUserInfo(long userId,
        DataSourceKind dataSources = DataSourceKind.LocalCache ,
        bool needUpdatelocal = false,
        bool needUpdateDistribute = false)
    {
        Console.WriteLine($"======数据源:{dataSources.ToString()} 是否更新本地:{needUpdatelocal}  是否更新Redis:{needUpdateDistribute}======");

        //初始化一个输入参数类
        var input = new DataOperateInput<User>();

        //如果包含从本地缓存取值
        if (dataSources.HasFlag(DataSourceKind.LocalCache))
        {
            input.GetDataFromLocalCache = () =>
            {
                //!!这里可以写具体的 获取本地缓存的处理逻辑
                return GetUserFromLocalCache(userId);
            };
        }

        //如果包含从分布式缓存取值
        if (dataSources.HasFlag(DataSourceKind.DistributeCache))
        {
            input.GetDataFromDistributeCache = () =>
            {
                //!!这里可以写具体的获取分布式缓存的处理逻辑
                return GetUserFromRedisCache(userId);
            };

            if (needUpdatelocal)
            {
                input.SetDataTolocalCache = (value) =>
                {
                    //!!这里可以写具体的设定本地缓存的处理逻辑
                    SetUserToLocalCache(value);
                };
            }
        }

        //如果包含从数据库缓存取值
        if (dataSources.HasFlag(DataSourceKind.DataBase))
        {
            input.GetDataFromDb = () =>
            {
                //!!这里可以写具体的获取数据库数据的处理逻辑
                return GetUserFromDB(userId);
            };

            if (needUpdateDistribute)
            {
                //!!这里可以写具体的设定分布式缓存的处理逻辑
                input.SetDataToDistributeCache = (value) =>
                {
                    SetUserToRedisCache(value);
                };
            }

            if (needUpdatelocal)
            {
                //!!这里可以写具体的设定本地缓存的处理逻辑
                input.SetDataTolocalCache = (value) =>
                {
                    SetUserToLocalCache(value);
                };
            }
        }

        //执行我们组合好的input
        var result =  new DataOperate().GetData(input);

        Console.WriteLine("=============================================\n");

        return result;
    }

上面的代码描述了使用封装好的GetData的方法的使用,其中有些委托的方法是需要具体实现的,这里我没有详细写。下面列出用于测试的GetUserFromLocalCache、GetUserFromRedisCache、GetUserFromDB、SetUserToLocalCache以及SetUserToRedisCache的代码。

/// <summary>
/// 从本地缓存获取用户信息
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
private User GetUserFromLocalCache(long userId)
{
    User user = null;

    if (userId == 1 )
    {
        user = new User
        {
            UserId = userId,
            Age = 10,
            Name = $"BigOrange_{userId}",
            Sex = 1
        };
    }

    if (user == null)
    {
        Console.WriteLine($"从本地缓存取值 未查询到  UserId={userId}");
    }
    else
    {
        Console.WriteLine($"从本地缓存取值  UserId={user.UserId} Name={user.Name} ");
    }

    return user;
}

/// <summary>
/// 从Redis缓存获取用户信息
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
private User GetUserFromRedisCache(long userId )
{
    User user = null;

    if (userId == 1 || userId == 2 )
    {
        user =  new User
        {
            UserId = userId,
            Age = 10,
            Name = $"BigOrange_{userId}",
            Sex = 1
        };
    }

    if (user == null)
    {
        Console.WriteLine($"从Redis缓存取值 未查询到  UserId={userId}");
    }
    else
    {
        Console.WriteLine($"从Redis缓存取值 UserId={user.UserId} Name={user.Name}");
    }

    return user;
}

/// <summary>
/// 从DB获取用户信息
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
private User GetUserFromDB(long userId)
{
    Console.WriteLine("从数据库中取值");

    User user = null;

    if (userId == 1 || userId == 2 || userId == 3)
    {
        user = new User
        {
            UserId = userId,
            Age = 10,
            Name = $"BigOrange_{userId}",
            Sex = 1
        };
    }

    if (user == null)
    {
        Console.WriteLine($"从DB取值 未查询到  UserId={userId}");
    }
    else
    {
        Console.WriteLine($"从DB取值 UserId={user.UserId} Name={user.Name}");
    }

    return user;
}

/// <summary>
/// 设置用户信息到本地缓存
/// </summary>
/// <param name="userInfo"></param>
/// <returns></returns>
private bool SetUserToLocalCache(User userInfo)
{
    Console.WriteLine($"设置值到本地缓存:useId = {userInfo.UserId}");
    return true;
}

/// <summary>
/// 设置用户信息到Redis缓存
/// </summary>
/// <param name="userInfo"></param>
/// <returns></returns>
private bool SetUserToRedisCache(User userInfo)
{
    Console.WriteLine($"设置值到Redis缓存:useId = {userInfo.UserId}");
    return true;
}

④测试一下

根据上面的代码,写了一些测试用的条目:

static void Main(string[] args)
{
    var userInfoService = new UserInfoService();

    /*
     * 测试用例
       数据库中存在 User1、User2、User3
       分布式缓存 User1、User2
       本地缓存 User1
     */

    //1.只从本地缓存取值
    userInfoService.GetUserInfo(1, DataSourceKind.LocalCache);
    userInfoService.GetUserInfo(2, DataSourceKind.LocalCache); 

    //2.只从Redis缓存取值
    userInfoService.GetUserInfo(2, DataSourceKind.DistributeCache);
    userInfoService.GetUserInfo(3, DataSourceKind.DistributeCache);  

    //3.只从DB取值
    userInfoService.GetUserInfo(3, DataSourceKind.DataBase);
    userInfoService.GetUserInfo(4, DataSourceKind.DataBase); 

    //4.从本地缓存和Redis取值
    userInfoService.GetUserInfo(1, DataSourceKind.LocalCache | DataSourceKind.DistributeCache);
    //不更新到本地
    userInfoService.GetUserInfo(2, DataSourceKind.LocalCache | DataSourceKind.DistributeCache, false);
    //更新到本地
    userInfoService.GetUserInfo(2, DataSourceKind.LocalCache | DataSourceKind.DistributeCache, true);

    //5.从Redis和DB取值
    userInfoService.GetUserInfo(2, DataSourceKind.DistributeCache | DataSourceKind.DataBase);
    userInfoService.GetUserInfo(3, DataSourceKind.DistributeCache | DataSourceKind.DataBase, false, false);
    userInfoService.GetUserInfo(3, DataSourceKind.DistributeCache | DataSourceKind.DataBase, false, true);

    //6.从本地和DB取值
    userInfoService.GetUserInfo(1, DataSourceKind.LocalCache | DataSourceKind.DataBase);
    userInfoService.GetUserInfo(3, DataSourceKind.LocalCache | DataSourceKind.DataBase, false,false);
    userInfoService.GetUserInfo(3, DataSourceKind.LocalCache | DataSourceKind.DataBase, true, false);

    //7.三者都使用
    userInfoService.GetUserInfo(1, DataSourceKind.LocalCache | DataSourceKind.DistributeCache | DataSourceKind.DataBase,false,false);
    userInfoService.GetUserInfo(2, DataSourceKind.LocalCache | DataSourceKind.DistributeCache | DataSourceKind.DataBase,false,false);
    userInfoService.GetUserInfo(2, DataSourceKind.LocalCache | DataSourceKind.DistributeCache | DataSourceKind.DataBase, true,false);
    userInfoService.GetUserInfo(3, DataSourceKind.LocalCache | DataSourceKind.DistributeCache | DataSourceKind.DataBase,false,false);
    userInfoService.GetUserInfo(3, DataSourceKind.LocalCache | DataSourceKind.DistributeCache | DataSourceKind.DataBase, true, false);
    userInfoService.GetUserInfo(3, DataSourceKind.LocalCache | DataSourceKind.DistributeCache | DataSourceKind.DataBase, false, true);
    userInfoService.GetUserInfo(3, DataSourceKind.LocalCache | DataSourceKind.DistributeCache | DataSourceKind.DataBase, true,true);

    Console.ReadKey();
}

执行结果:

======数据源:LocalCache 是否更新本地:False 是否更新Redis:False======
从本地缓存取值 UserId=1 Name=BigOrange_1
=============================================

======数据源:LocalCache 是否更新本地:False 是否更新Redis:False======
从本地缓存取值 未查询到 UserId=2
=============================================

======数据源:DistributeCache 是否更新本地:False 是否更新Redis:False======
从Redis缓存取值 UserId=2 Name=BigOrange_2
=============================================

======数据源:DistributeCache 是否更新本地:False 是否更新Redis:False======
从Redis缓存取值 未查询到 UserId=3
=============================================

======数据源:DataBase 是否更新本地:False 是否更新Redis:False======
从DB取值 UserId=3 Name=BigOrange_3
=============================================

======数据源:DataBase 是否更新本地:False 是否更新Redis:False======
从DB取值 未查询到 UserId=4
=============================================

======数据源:LocalCache, DistributeCache 是否更新本地:False 是否更新Redis:False======
从本地缓存取值 UserId=1 Name=BigOrange_1
=============================================

======数据源:LocalCache, DistributeCache 是否更新本地:False 是否更新Redis:False======
从本地缓存取值 未查询到 UserId=2
从Redis缓存取值 UserId=2 Name=BigOrange_2
=============================================

======数据源:LocalCache, DistributeCache 是否更新本地:True 是否更新Redis:False======
从本地缓存取值 未查询到 UserId=2
从Redis缓存取值 UserId=2 Name=BigOrange_2
设置值到本地缓存:useId = 2
=============================================

======数据源:DistributeCache, DataBase 是否更新本地:False 是否更新Redis:False======
从Redis缓存取值 UserId=2 Name=BigOrange_2
从DB取值 UserId=2 Name=BigOrange_2
=============================================

======数据源:DistributeCache, DataBase 是否更新本地:False 是否更新Redis:False======
从Redis缓存取值 未查询到 UserId=3
从DB取值 UserId=3 Name=BigOrange_3
=============================================

======数据源:DistributeCache, DataBase 是否更新本地:False 是否更新Redis:True======
从Redis缓存取值 未查询到 UserId=3
从DB取值 UserId=3 Name=BigOrange_3
设置值到Redis缓存:useId = 3
=============================================

======数据源:LocalCache, DataBase 是否更新本地:False 是否更新Redis:False======
从本地缓存取值 UserId=1 Name=BigOrange_1
=============================================

======数据源:LocalCache, DataBase 是否更新本地:False 是否更新Redis:False======
从本地缓存取值 未查询到 UserId=3
从DB取值 UserId=3 Name=BigOrange_3
=============================================

======数据源:LocalCache, DataBase 是否更新本地:True 是否更新Redis:False======
从本地缓存取值 未查询到 UserId=3
从DB取值 UserId=3 Name=BigOrange_3
设置值到本地缓存:useId = 3
=============================================

======数据源:LocalCache, DistributeCache, DataBase 是否更新本地:False 是否更新Redis:False======
从本地缓存取值 UserId=1 Name=BigOrange_1
=============================================

======数据源:LocalCache, DistributeCache, DataBase 是否更新本地:False 是否更新Redis:False======
从本地缓存取值 未查询到 UserId=2
从Redis缓存取值 UserId=2 Name=BigOrange_2
从DB取值 UserId=2 Name=BigOrange_2
=============================================

======数据源:LocalCache, DistributeCache, DataBase 是否更新本地:True 是否更新Redis:False======
从本地缓存取值 未查询到 UserId=2
从Redis缓存取值 UserId=2 Name=BigOrange_2
设置值到本地缓存:useId = 2
从DB取值 UserId=2 Name=BigOrange_2
设置值到本地缓存:useId = 2
=============================================

======数据源:LocalCache, DistributeCache, DataBase 是否更新本地:False 是否更新Redis:False======
从本地缓存取值 未查询到 UserId=3
从Redis缓存取值 未查询到 UserId=3
从DB取值 UserId=3 Name=BigOrange_3
=============================================

======数据源:LocalCache, DistributeCache, DataBase 是否更新本地:True 是否更新Redis:False======
从本地缓存取值 未查询到 UserId=3
从Redis缓存取值 未查询到 UserId=3
从DB取值 UserId=3 Name=BigOrange_3
设置值到本地缓存:useId = 3
=============================================

======数据源:LocalCache, DistributeCache, DataBase 是否更新本地:False 是否更新Redis:True======
从本地缓存取值 未查询到 UserId=3
从Redis缓存取值 未查询到 UserId=3
从DB取值 UserId=3 Name=BigOrange_3
设置值到Redis缓存:useId = 3
=============================================

======数据源:LocalCache, DistributeCache, DataBase 是否更新本地:True 是否更新Redis:True======
从本地缓存取值 未查询到 UserId=3
从Redis缓存取值 未查询到 UserId=3
从DB取值 UserId=3 Name=BigOrange_3
设置值到Redis缓存:useId = 3
设置值到本地缓存:useId = 3
=============================================

四、总结一下

类似上面的用户信息,可能对于不同系统、不同性能要求,获取方式会有所不同。

打个比方:对于一个后台管理系统,用户信息获取是一个低频操作,可能只需要从数据库中获取,此时一般后台系统不会设置本地缓存和分布式缓存,而对于一个接口系统,可能每天有几百万的访问量,此时如果只从数据库获取,很难承受,所以要利用到分布式缓存和本地缓存。层次越多那么变化和组合也就越多,但是每个实体的存取如果都各自实现自己的方式,又比较浪费,所以如果能抽象出一套方法,只需要告诉方法存取的方式,然后得到自己想要的数据,或许这样是比较好的方式,而具体怎么拿、怎么存,还是由调用的人去给出,这样可以应对复杂的规则。这也是为什么要使用这么多委托的原因,由于像上面获取和设定User缓存的方式多种多样,这么做可以把具体的获取和设置缓存的操作开放给使用者。在系统重构方面上,可以将一些通用的方法抽象出来,相对成本较低,扩展性好一些。

五、题外话

上面的代码中对于更新数据,没有做线程安全处理,多个进程去更新分布式缓存、同一进程的多个线程去更新本地缓存,可能都需要进行锁操作。

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。如果你想了解更多相关内容请查看下面相关链接

(0)

相关推荐

  • asp.net(C#)禁止缓存文件不让文件缓存到客户端

    IIS会按文件地址及参数将文件缓存到客户端,以便再次访问该内容时速度更快.如果要取消这种机制则需要禁止缓存文件. 一.编程方式 Response.Buffer = true; Response.ExpiresAbsolute = DateTime.Now.AddDays(-1); Response.Cache.SetExpires(DateTime.Now.AddDays(-1)); Response.Expires = 0; Response.CacheControl = "no-cache&

  • C#利用缓存分块读写大文件

    C#利用缓存分块读写大文件,供大家参考,具体内容如下 在日常生活中,可能会遇到大文件的读取,不论是什么格式,按照储存文件的格式读取大文件,就会在Buffer中看到相关的文件头合内容, 以一次.txt文件存取为例. using System.IO; 首先创建demo文件,此处文件大小没关系,只是演示 private void button2_Click(object sender, EventArgs e) { using (FileStream fsWrite = new FileStream(

  • C#函数式编程中的缓存技术详解

    缓存技术 该节我们将分成两部分来讲解,第一部分为预计算,第二部分则为缓存.缓存这个技术对应从事开发的人员来说是非常熟悉的,从页面缓存到数据库缓存无处不在,而其最重要的特点就是在第一次查询后将数据缓存,在以后的查询过程中就无需重新计算而直接从内存中将结果返回,大大提高了性能,而我们这里的缓存则集中运用在函数上.  预计算 可能一些人并不能立马理解这个词的含义,所以我们就简单的从生活例子出发介绍一下.很多人在工作中一定会这样做事,比如上级吩咐了你一件事,但是这件事的后半部分要等另一个同事做好之后把对

  • C# .NET 中的缓存实现详情

    目录 一.缓存的基本概念 二.缓存 三.进程内缓存早期做法 四.更好的解决方案 1. Microsoft.Extensions.Caching.Memory 2.具有驱逐策略的 IMemoryCache 3.问题和缺失的功能 4.代码说明 五.何时使用 WaitToFinishMemoryCache 一.缓存的基本概念 缓存 .这是一个简单但非常有效的概念,这个想法的核心是记录过程数据,重用操作结果.当执行繁重的操作时,我们会将结果保存在我们的 缓存容器中 .下次我们需要该结果时,我们将从缓存容

  • C#使用CallContext缓存线程数据

    一.CallContext 概述 命名空间:System.Runtime.Remoting.Messaging CallContext 用于提供与执行代码路径一起传送的属性集,直白讲就是:提供线程(多线程/单线程)代码执行路径中数据传递的能力. 当对另一个 AppDomain 中的对象进行远程方法调用时,CallContext 类将生成一个与该远程调用一起传播的 LogicalCallContext 实例.只有公开 ILogicalThreadAffinative 接口并存储在 CallCont

  • 详解C#中普通缓存的使用

    一.首先,新建控制台程序(.NET Core).以下为项目结构 CacheHelper缓存帮助类 DemoTest 为测试有无缓存的Demo代码 Program 你们懂得 就不多说了 二.编写缓存类 public class CacheHelper { //缓存容器 private static Dictionary<string, object> CacheDictionary = new Dictionary<string, object>(); /// <summary

  • C#自定义缓存封装类实例

    本文实例讲述了C#自定义缓存封装类.分享给大家供大家参考.具体如下: 这个自定义的C#类封装了部分常用的缓存操作,包括写入缓存,读取缓存,设置缓存过期时间等等,简化了C#的缓存操作,代码非常简单,易于阅读. using System; using System.Web; namespace DotNet.Utilities { /// <summary> /// 缓存相关的操作类 /// </summary> public class DataCache { /// <sum

  • C#缓存之SqlCacheDependency用法实例总结

    本文整理汇总了C#缓存的数据库依赖类SqlCacheDependency的使用方法,具体内容如下: 1.数据库依赖类SqlCacheDependency 数据库缓存依赖主要解决的是当数据库的内容发生改变时,如何及时通知缓存,并更新缓存中的数据的问题. 语法定义: SqlCacheDependency类主要的构造函数如下: public SqlCacheDependency(string database,string table) 其中参数一代表要启用缓存的数据库,参数二表示缓存的表.在实际使用

  • C# Cache缓存读取的设置方法

    先创建一个CacheHelper.cs类,代码如下: using System; using System.Web; using System.Collections; using System.Web.Caching;    public class CacheHelper {     /// <summary>     /// 获取数据缓存     /// </summary>     /// <param name="cacheKey">键&l

  • C#中缓存System.Web.Caching用法总结

    System.Web.Caching.Cache Insert和Add方法的区别 Add() object Add(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback); Insert(

  • C#中缓存的基本使用方法

    前言 缓存主要是为了提高数据的读取速度.因为服务器和应用客户端之间存在着流量的瓶颈,所以读取大容量数据时,使用缓存来直接为客户端服务,可以减少客户端与服务器端的数据交互,从而大大提高程序的性能. 缓存这个东西可大可小,小到一个静态的字段,大到将整个数据库Cache起来.项目开发过程中缓存的应用到处可见,本文主要介绍一下使用的方法,下面话不多说了,来一起看看详细的介绍吧 1.在ASP.NET中页面缓存的使用方法简单,只需要在aspx页的顶部加上一句声明即可: <%@ OutputCache Dur

随机推荐