65.9K
CodeProject 正在变化。 阅读更多。
Home

使用Redis作为提供程序的简单C#缓存组件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.70/5 (9投票s)

2016年8月24日

CPOL

8分钟阅读

viewsIcon

66551

一组简单的 C# 类/项目,创建了一个与 Redis 交互的简单缓存组件。

引言

缓存并非新鲜事物。您可以轻松地将数据放入常用的缓存提供程序中,但如果您能够拥有一种策略和一个简单的组件,让您可以隐藏与缓存提供程序通信的细节呢?

此外,大多数公司或团队需要一个简单的解决方案,能够为理解内部工作原理的人和不理解的人提供服务,类似于即插即用,然后就完成了。

考虑到这一点,本文的目的是提供一个简单的客户端组件来执行基本的缓存操作。我选择 Redis 作为我的缓存提供程序,以演示该组件与外部系统交互的能力(我也可以使用其他任何东西)。目标是将缓存提供程序的详细信息存储在自定义配置节中,并提供一个简单的入口点,使开发人员无需了解我们如何与 Redis 交互的细节。

请注意,此组件本身不是 Redis 客户端,它是一个使用 ServiceStack 包与 Redis 服务器通信的 fachada。优点是开发人员每次调用 Redis 时都不需要了解客户端程序集的详细信息。

本文不包含安装和配置 Redis 服务器的介绍,目前假设服务器已安装并正在运行。本文不包含与其他解决方案的比较或性能测试。

背景

迟早,人们会为性能提升而苦苦挣扎,尤其(但不限于)当涉及到用户界面交互速度时。

每个加载的页面或窗体通常会加载大量理论上总是相同的信息(但它会不时变化,或者在某些情况下……永远不变)。一些项目从一开始就考虑到了这一点,另一些则随着发展而适应。

缓存是解决此问题的一种技术。虽然这不是什么新鲜事,但始终重要的是要记住,信息缓存的方式有多种,信息缓存的方式也可能不同,尤其是在 NoSQL 解决方案中。

我可以写关于几种缓存策略或解决方案的文章,但我在这里的目的是与社区分享一种将任何 .Net 应用程序与 Redis 服务器集成,并考虑到缓存的解决方案(我也可以完全探索 Redis,但这超出了范围)。

使用代码

让我们确定我们需要什么。

目标是拥有一个可以在每个 .Net 项目中使用的小型组件。考虑到易用性、可伸缩性和配置灵活性,我建议

  • 创建一个类库(Configuration),其中包含一个用于自定义配置节的类,可以在任何 app/web.config 中声明,并包含构建 RedisEndpoint 所需的 Redis 基本信息(配置灵活性);
  • 创建另一个类库(Common),并在其中创建一个充当 Redis 缓存提供程序的类。为了解决方案未来的灵活性,让我们继承一个具有以下操作的接口
    • Set(将给定对象设置为缓存);
    • Get(从缓存中获取给定对象);
    • Remove(从缓存中删除对象);
    • IsInCache(指示给定对象是否在缓存中)。
  • 创建一个单元测试项目,引用 Configuration 和 Common 项目,以测试我们的提供程序。

请注意,这只是实现我们所需功能的一种方式。也可以通过其他方式与缓存提供程序交互,我选择此选项只是为了分离所有关注点——配置、基本横切功能(Common/Caching)和测试作为顶层,就像演示一样。

因此,为了开始编码,让我们从先决条件开始。

  • Visual Studio:我使用了 2015 + .Net Framework 4.5.2,但我相信您可以使用早期版本;
  • 已安装并运行 Redis 服务器(下载和安装有多种方法,我发现最简单的方法是通过 MS Guys。您可以在此处找到一键安装 - https://github.com/MSOpenTech/redis/releases - 它将 Redis 安装为 Windows 服务,然后您就可以开始使用了!);
  • 我还安装了 Redis Desktop Manager,以便查看 Redis 中的内容(https://redisdesktop.com/);
  • ServiceStack 的 Nuget 包(稍后将介绍)。

所以,少说废话,多写代码。

我建议您开始创建一个空白解决方案。将其命名为“CacheComponent”。

创建一个新的类库项目,命名为“Configuration”。我们的目标是为 Redis 创建自定义配置节。

添加对“System.Configuration”的引用。

添加一个新类,命名为“RedisConfigurationSection”。我们的自定义配置节将有四个属性。

  • host(必需)
  • port(必需)
  • password(可选)
  • databaseID(可选)

将以下代码复制并粘贴到您的类中。

using System.Configuration;

namespace Configuration
{
    public class RedisConfigurationSection : ConfigurationSection
    {
        #region Constants

        private const string HostAttributeName = "host";
        private const string PortAttributeName = "port";
        private const string PasswordAttributeName = "password";
        private const string DatabaseIDAttributeName = "databaseID";

        #endregion

        #region Properties

        [ConfigurationProperty(HostAttributeName, IsRequired = true)]
        public string Host
        {
            get { return this[HostAttributeName].ToString(); }
        }

        [ConfigurationProperty(PortAttributeName, IsRequired = true)]
        public int Port
        {
            get { return (int)this[PortAttributeName]; }
        }

        [ConfigurationProperty(PasswordAttributeName, IsRequired = false)]
        public string Password
        {
            get { return this[PasswordAttributeName].ToString(); }
        }

        [ConfigurationProperty(DatabaseIDAttributeName, IsRequired = false)]
        public long DatabaseID
        {
            get { return (long)this[DatabaseIDAttributeName]; }
        }

        #endregion
    }
}

添加一个新类,命名为“RedisConfigurationManager”。它将负责与配置节交互。

using System.Configuration;

namespace Configuration
{
    public class RedisConfigurationManager
    {
        #region Constants

        private const string SectionName = "RedisConfiguration";

        public static RedisConfigurationSection Config
        {
            get
            {
                return (RedisConfigurationSection)ConfigurationManager.GetSection(SectionName);
            }
        }

        #endregion
    }
}

构建项目,应该会成功。稍后我们将回到这里,在单元测试项目的 app.config 文件中声明配置。

您的解决方案应该看起来像这样。

现在是时候创建另一个类库来创建与 Redis 交互的逻辑了,命名为“Common”。

为了继续,我们需要一些东西来集成 Redis。我使用了 ServiceStack 包来完成此操作。右键单击“Common”项目,选择“Manage Nuget packages...”,然后转到“Browse”选项卡。搜索“ServiceStack”并安装 ServiceStack.Redis。

您会注意到它还会安装 Common、Interfaces 和 Text 程序包。

添加对“Configuration”类库的引用。

添加一个新接口,命名为“ICacheProvider”。

using System;

namespace Common
{
    public interface ICacheProvider
    {
        void Set<T>(string key, T value);
        
        void Set<T>(string key, T value, TimeSpan timeout);

        T Get<T>(string key);

        bool Remove(string key);

        bool IsInCache(string key);
    }
}

添加一个新类,命名为“RedisCacheProvider”。现在开始了。

using Configuration;
using ServiceStack.Redis;
using System;

namespace Common
{
    public class RedisCacheProvider : ICacheProvider
    {
        RedisEndpoint _endPoint;

        public RedisCacheProvider()
        {
            _endPoint = new RedisEndpoint(RedisConfigurationManager.Config.Host, RedisConfigurationManager.Config.Port, RedisConfigurationManager.Config.Password, RedisConfigurationManager.Config.DatabaseID);
        }

        public void Set<T>(string key, T value)
        {
            this.Set(key, value, TimeSpan.Zero);
        }

        public void Set<T>(string key, T value, TimeSpan timeout)
        {
            using (RedisClient client = new RedisClient(_endPoint))
            {
                client.As<T>().SetValue(key, value, timeout);
            }
        }

        public T Get<T>(string key)
        {
            T result = default(T);

            using (RedisClient client = new RedisClient(_endPoint))
            {
                var wrapper = client.As<T>();

                result = wrapper.GetValue(key);
            }

            return result;
        }
        
        public bool Remove(string key)
        {
            bool removed = false;

            using (RedisClient client = new RedisClient(_endPoint))
            {
                removed = client.Remove(key);
            }

            return removed;
        }

        public bool IsInCache(string key)
        {
            bool isInCache = false;

            using (RedisClient client = new RedisClient(_endPoint))
            {
                isInCache = client.ContainsKey(key);
            }

            return isInCache;
        }
    }
}

请注意,在构造函数中,我们通过 RedisConfigurationManager 访问自定义配置节,以获取构建 RedisEndpoint 所需的信息。

构建项目,应该会成功。此时,我们已经拥有了在任何 .NET 应用程序中使用我们组件所需的一切。

您的解决方案应该看起来像这样。

让我们创建一个单元测试项目来检查一切是否正常。将其命名为“UnitTests”。它将创建一个单元测试文件,将其重命名为“CacheTests”。

添加对“Configuration”和“Common”这两个类库项目的引用。

右键单击“UnitTests”项目,添加新项,选择“Application Configuration File”。这将为您的项目添加一个 app.config 文件。

打开 app.config 文件,并按如下方式放置。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="RedisConfiguration" type="Configuration.RedisConfigurationSection, Configuration, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
  </configSections>
  <RedisConfiguration host="localhost" port="6379"/>
</configuration>

这将设置我们的自定义配置节,使其可供使用。我本可以简单地将此信息添加到 AppSettings 中,但这样我认为更清晰,更容易理解。当然,这只是一种看法。

请注意,ServiceStack 程序集未签名。如果您的程序集已签名,自定义配置节可能会引发一些错误,并需要对第三方程序集进行签名(请记住这一点)。

在将数据设置到 Redis/从 Redis 获取数据之前,让我们创建几个类来存储对象。

假设我们的 DataModel 有一个 Person 类和一个 Contact 类。一个人可能有 0 个或多个联系人。为了简单起见和演示,这些类都很小。

using System.Collections.Generic;

namespace UnitTests
{
    public class Contact
    {
        public string Type { get; set; }

        public string Value { get; set; }

        public Contact(string type, string value)
        {
            this.Type = type;
            this.Value = value;
        }
    }

    public class Person
    {
        public long Id { get; set; }

        public string Name { get; set; }

        public List<Contact> Contacts { get; set; }

        public Person(long id, string name, List<Contact> contacts)
        {
            this.Id = id;
            this.Name = name;
            this.Contacts = contacts;
        }
    }
}

好了,现在将“CacheTests”类更改为如下所示。

using Common;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;

namespace UnitTests
{
    [TestClass]
    public class CacheTests
    {
        ICacheProvider _cacheProvider;

        [TestInitialize]
        public void Initialize()
        {
            _cacheProvider = new RedisCacheProvider();
        }

        [TestMethod]
        public void Test_SetValue()
        {
            List<Person> people = new List<Person>()
            {
                new Person(1, "Joe", new List<Contact>()
                {
                    new Contact("1", "123456789"),
                    new Contact("2", "234567890")
                })
            };

            _cacheProvider.Set("People", people);
        }

        [TestMethod]
        public void Test_GetValue()
        {
            var contacts = _cacheProvider.Get<List<Contact>>("People");

            Assert.IsNotNull(contacts);
            Assert.AreEqual(2, contacts.Count);
        }
    }
}

现在您有两个单元测试。

  • 一个用于将一组人设置到 Redis 中;
  • 另一个用于从 Redis 中获取该集合。

您的解决方案应该看起来像这样。

如果运行第一个“Test_SetValue”,它应该会成功。检查 Redis Desktop Manager,应该会像这样。

如果运行“Test_GetValue”,它也应该会成功。

关注点

此时,我们有了一个小型缓存组件,它可以隐藏与 Redis 通信的细节。我们可以提出以下问题,它们都将是有意义的。

  • Redis 不仅仅是这些吗?毋庸置疑,我保持简单是为了演示如何使用外部系统来缓存我们的数据。我可以不使用 Redis 或其他解决方案(例如 AppFabric 或 MongoDb)来实现这一点。无论如何,如果您需要缓存提供程序 + 更丰富的 Redis 集成组件,您可以创建自己的组件,然后缓存提供程序而不是与 ServiceStack 交互,而是与您的组件交互;
  • 如果我不想使用 Redis 怎么办?只需构建您想要的提供程序,继承自 ICacheProvider 即可!;
  • 如果我的缓存提供程序需要比 ICacheProvider 中的更多方法怎么办?您可以向 ICacheProvider 添加逻辑,或者将其扩展到另一个接口,并在您的自定义提供程序中添加所需的逻辑;
  • 如果我想要一个处理缓存的组件,但我的缓存提供程序并不总是相同的?例如,我想使用 Redis 来缓存用于网站的数据,但我想为我的 Web 或 Windows 服务使用内存缓存?  下面有解答。

这很糟糕,因为与 Redis 交互有其自身的方式,但如果我想使用 MongoDb、AppFabric 或内存缓存,我们的交互方式不同。并非所有配置都适用,类也不同等等。

我可以为我的每个提供程序创建一个组件,但如果将来我决定我过去存储内存缓存的服务也使用 Redis 怎么办?我应该重写所有代码引用以使用 Redis 组件而不是内存组件吗?

一定有更好的办法。我认为这篇文章值得进一步投入来解决这些问题,但它的目标是提供一个简单的组件来与外部存储进行缓存目的的交互……

未来工作

  • 基于这些项目和类添加其他缓存提供程序;
  • 添加逻辑以在运行时决定使用哪个提供程序;
  • 使用 AOP 自动将数据设置/获取到/从缓存中。

未完待续!

附注:请注意,并非所有项目或项目集都需要 Redis 这样的外部缓存系统。某些技术(如 ASP.NET)提供了开箱即用的缓存功能,这可能足够(或不足,您应该尝试并比较)。但是,无论您使用什么提供程序,遵循上面带有 ICacheProvider 接口的模式,应该会为您提供最低级别的灵活性,以应对未来的变化。

© . All rights reserved.