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

使用 .NET/C# 客户端通过 Redis 服务器实现分布式缓存

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (69投票s)

2013年8月16日

CPOL

7分钟阅读

viewsIcon

367795

downloadIcon

10525

在 Windows 机器上设置 Redis 服务器,并使用 C# 客户端访问它

引言

在本文中,我想描述我以最紧凑的方式安装和配置 Redis 服务器的经验。此外,我还想简要概述 .NET/C# 客户端中使用 Redis 哈希和列表。

本文内容

背景

Redis 是最快、功能最丰富的内存键值数据存储之一。

缺点

  • 无本地数据缓冲 (例如 Azure 缓存上的同步本地数据缓冲)
  • 尚未完全支持集群 (预计今年年底)

优点

  • 易于配置
  • 使用简单
  • 高性能
  • 支持不同数据类型 (例如哈希、列表、集合、有序集合)
  • ASP.NET 会话集成
  • 查看缓存内容的 Web UI

在这个简单的演示中,我将演示如何在服务器上安装和配置 Redis,并在 C# 代码中使用它。

Redis 安装

https://github.com/dmajkic/redis/downloads 下载二进制文件 (win32 win64 直接链接),将存档解压到应用程序目录 (例如 C:\Program Files\Redis)

https://github.com/kcherenkov/redis-windows-service/downloads 下载编译好的 Redis 服务,然后复制到程序文件夹 (例如 C:\Program Files\Redis。如果缺少配置文件,请下载并将其复制到应用程序目录。有效的 Redis 配置文件示例如 https://raw.github.com/antirez/redis/2.6/redis.conf

Redis 应用程序的完整文件集也可在 zip 文件 (x64) 中找到.

当您拥有完整的应用程序文件集时 (如下图所示),

redis application folder conten

导航到应用程序目录并运行以下命令

sc create %name% binpath= "\"%binpath%\" %configpath%" start= "auto" DisplayName= "Redis" 

其中

  • %name% -- 服务实例的名称,例如: redis-instance;
  • %binpath% -- 此项目 EXE 文件的路径,例如: C:\Program Files\Redis\RedisService_1.1.exe;
  • %configpath% -- Redis 配置文件路径,例如: C:\Program Files\Redis\redis.conf;

示例

sc create Redis start= auto DisplayName= Redis binpath= "\"C:\Program Files\Redis\RedisService_1.1.exe\
" \"C:\Program Files\Redis\redis.conf\"" 

它应该看起来像这样

Installing redis as windows service

确保您有足够的权限来启动服务。安装后,请检查服务是否已成功创建并正在运行

redis running as a windows service

或者,您可以使用某人创建的安装程序 (我没有试过): https://github.com/rgl/redis/downloads

Redis 服务器保护: 密码、IP 过滤

保护 Redis 服务器的首要方法 是使用 Windows 防火墙或活动网络连接的属性设置 IP 过滤额外的保护 可以通过 Redis 密码 来设置。这需要以以下方式更新 Redis 配置文件 (redis.conf)

首先,找到这一行

# requirepass foobared   

删除开头的 # 符号并将 foobared 替换为新密码

requirepass foobared

然后重启 Redis Windows 服务!!!

实例化客户端时,使用带密码的构造函数

RedisClient client = new RedisClient(serverHost, port, redisPassword);

Redis 服务器复制 (主从配置)

这项技术允许创建服务器数据的副本到同步副本中,这意味着每次主服务器修改时,从服务器都会收到通知并自动同步。复制主要用于读取 (但不写) 可伸缩性或数据冗余以及服务器故障转移。设置两个 Redis 实例 (在同一服务器或不同服务器上的两个服务),然后将其中一个配置为主服务器的从属服务器。要使 Redis 服务器实例成为另一个服务器的从属服务器,请按以下方式更改配置文件

找到下面的行

# slaveof <masterip> <masterport>

替换为

slaveof 192.168.1.1 6379

(指定主服务器的实际 IP,以及端口,如果您自定义了的话)。如果主服务器配置为需要密码 (认证),请按如下方式修改 redis.conf,找到一行

# masterauth <master-password>

删除开头的 # 符号并将 <master-password> 替换为主密码,使其变为

masterauth mastpassword

现在此 Redis 实例可用作主服务器的只读同步副本。

从 C# 代码使用 Redis 缓存

要从 C# 使用 Redis,请运行 "管理 NuGet 程序包" 插件,找到 ServiceStack.Redis 包,然后安装它。

直接从实例化客户端使用 Set/Get 方法的示例

string host = "localhost";
string elementKey = "testKeyRedis";

using (RedisClient redisClient = new RedisClient(host))
{
      if (redisClient.Get<string>(elementKey) == null)
      {
           // adding delay to see the difference
           Thread.Sleep(5000); 
           // save value in cache
           redisClient.Set(elementKey, "some cached value");
      }
      // get value from the cache by key
      message = "Item value is: " + redisClient.Get<string>("some cached value");
 }

类型化的实体集合更有趣也更实用,因为它们操作的是确切的对象类型。在下面的代码示例中,定义了两个类 PhonePerson - 手机的所有者。每个手机实例都有一个指向所有者的引用。此代码演示了我们如何按条件添加、删除或查找缓存中的项目

public class Phone
{
   public int Id { get; set; }
   public string Model { get; set; }
   public string Manufacturer { get; set; }
   public Person Owner { get; set; }
}

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Surname { get; set; }
    public int Age { get; set; }
    public string Profession { get; set; }
}

using (RedisClient redisClient = new RedisClient(host))
{
     IRedisTypedClient<phone> phones = redisClient.As<phone>();
     Phone phoneFive = phones.GetValue("5");
     if (phoneFive == null)
     {
          // make a small delay
          Thread.Sleep(5000);
          // creating a new Phone entry
          phoneFive = new Phone
          {
               Id = 5,
               Manufacturer = "Motorolla",
               Model = "xxxxx",
               Owner = new Person
               {
                    Id = 1,
                    Age = 90,
                    Name = "OldOne",
                    Profession = "sportsmen",
                    Surname = "OldManSurname"
               }
          };
          // adding Entry to the typed entity set
          phones.SetEntry(phoneFive.Id.ToString(), phoneFive);
     }
     message = "Phone model is " + phoneFive.Manufacturer;
     message += "Phone Owner Name is: " + phoneFive.Owner.Name;
}

在上面的示例中,我们实例化了类型化客户端 IRedisTypedClient它操作特定类型的缓存对象: Phone 类型。

ASP.NET 会话状态与 Redis

要配置 ASP.NET 会话状态 与 Redis 提供程序,请向您的 Web 项目添加一个新文件,名为 RedisSessionStateProvider.cs,从 https://github.com/chadman/redis-service-provider/raw/master/RedisProvider/SessionProvider/RedisSessionProvider.cs 复制代码,然后添加或更改配置文件中的以下部分 (sessionState 标签必须在 system.web 标签内),或者您可以下载附加的源代码并复制代码。

<sessionstate timeout="1" mode="Custom" 
customprovider="RedisSessionStateProvider" cookieless="false">
      <providers>
        <add name="RedisSessionStateProvider" writeexceptionstoeventlog="false" 
        type="RedisProvider.SessionProvider.CustomServiceProvider" 
        server="localhost" port="6379" password="pasword">
      </add> </providers>
</sessionstate>

注意,密码是可选的,取决于服务器认证。必须将其替换为实际值,或者在 Redis 服务器不需要认证时将其删除。服务器属性和端口也必须根据具体值进行替换 (默认端口是 6379)。然后在项目中,您可以使用会话状态

// in the Global.asax
public class MvcApplication1 : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        //....
    }

    protected void Session_Start()
    {
        Session["testRedisSession"] = "Message from the redis ression";
    }
}

在 Home 控制器中

public class HomeController : Controller
{
    public ActionResult Index()
    {
       //...
       ViewBag.Message = Session["testRedisSession"];
       return View();
    }
//...
}

结果

redis aspnet session state

ASP.NET 输出缓存提供程序与 Redis 的配置方式类似。

Redis 集合和列表

主要注意的是,Redis 列表 实现 IList<T>,而 Redis 集合实现 ICollection<T>。让我们看看如何使用它们。

列表主要用于需要区分同一类型对象的不同类别时。例如,我们有 "最畅销手机" 和 "旧收藏" 两个手机列表

string host = "localhost";
using (var redisClient = new RedisClient(host))
{
    //Create a 'strongly-typed' API that makes all Redis Value operations to apply against Phones
    IRedisTypedClient<phone> redis = redisClient.As<phone>();

    IRedisList<phone> mostSelling = redis.Lists["urn:phones:mostselling"];
    IRedisList<phone> oldCollection = redis.Lists["urn:phones:oldcollection"];

    Person phonesOwner = new Person
        {
            Id = 7,
            Age = 90,
            Name = "OldOne",
            Profession = "sportsmen",
            Surname = "OldManSurname"
        };
                
    // adding new items to the list
    mostSelling.Add(new Phone
            {
                Id = 5,
                Manufacturer = "Sony",
                Model = "768564564566",
                Owner = phonesOwner
            });

    oldCollection.Add(new Phone
            {
                Id = 8,
                Manufacturer = "Motorolla",
                Model = "324557546754",
                Owner = phonesOwner
            });

    var upgradedPhone  = new Phone
    {
        Id = 3,
        Manufacturer = "LG",
        Model = "634563456",
        Owner = phonesOwner
    };

    mostSelling.Add(upgradedPhone);

    // remove item from the list
    oldCollection.Remove(upgradedPhone);

    // find objects in the cache
    IEnumerable<phone> LGPhones = mostSelling.Where(ph => ph.Manufacturer == "LG");

    // find specific
    Phone singleElement = mostSelling.FirstOrDefault(ph => ph.Id == 8);

    //reset sequence and delete all lists
    redis.SetSequence(0);
    redisClient.Remove("urn:phones:mostselling");
    redisClient.Remove("urn:phones:oldcollection");
}

Redis 集合在需要存储相关数据集合并收集统计信息时很有用,例如答案 -> 问题,对答案或问题的投票。假设我们有问题和答案,需要将它们存储在缓存中以提高性能。使用 Redis,我们可以这样做

/// <summary>
/// Gets or sets the Redis Manager. The built-in IoC used with ServiceStack autowires this property.
/// </summary>
IRedisClientsManager RedisManager { get; set; }
/// <summary>
/// Delete question by performing compensating actions to 
/// StoreQuestion() to keep the datastore in a consistent state
/// </summary>
/// <param name="questionId">
public void DeleteQuestion(long questionId)
{
    using (var redis = RedisManager.GetClient())
    {
        var redisQuestions = redis.As<question>();

        var question = redisQuestions.GetById(questionId);
        if (question == null) return;
                
        //decrement score in tags list
        question.Tags.ForEach(tag => redis.IncrementItemInSortedSet("urn:tags", tag, -1));

        //remove all related answers
        redisQuestions.DeleteRelatedEntities<answer>(questionId);

        //remove this question from user index
        redis.RemoveItemFromSet("urn:user>q:" + question.UserId, questionId.ToString());

        //remove tag => questions index for each tag
        question.Tags.ForEach("urn:tags>q:" + tag.ToLower(), questionId.ToString()));

        redisQuestions.DeleteById(questionId);
    }
}

public void StoreQuestion(Question question)
{
    using (var redis = RedisManager.GetClient())
    {
        var redisQuestions = redis.As<question>();

        if (question.Tags == null) question.Tags = new List<string>();
        if (question.Id == default(long))
        {
            question.Id = redisQuestions.GetNextSequence();
            question.CreatedDate = DateTime.UtcNow;

            //Increment the popularity for each new question tag
            question.Tags.ForEach(tag => redis.IncrementItemInSortedSet("urn:tags", tag, 1));
        }

        redisQuestions.Store(question);
        redisQuestions.AddToRecentsList(question);
        redis.AddItemToSet("urn:user>q:" + question.UserId, question.Id.ToString());

        //Usage of tags - Populate tag => questions index for each tag
        question.Tags.ForEach(tag => redis.AddItemToSet
        ("urn:tags>q:" + tag.ToLower(), question.Id.ToString()));
    }
}

/// <summary>
/// Delete Answer by performing compensating actions to 
/// StoreAnswer() to keep the datastore in a consistent state
/// </summary>
/// <param name="questionId">
/// <param name="answerId">
public void DeleteAnswer(long questionId, long answerId)
{
    using (var redis = RedisManager.GetClient())
    {
        var answer = redis.As<question>().GetRelatedEntities<answer>
        (questionId).FirstOrDefault(x => x.Id == answerId);
        if (answer == null) return;
                
        redis.As<question>().DeleteRelatedEntity<answer>(questionId, answerId);
                
        //remove user => answer index
        redis.RemoveItemFromSet("urn:user>a:" + answer.UserId, answerId.ToString());
    }
}

public void StoreAnswer(Answer answer)
{
    using (var redis = RedisManager.GetClient())
    {
        if (answer.Id == default(long))
        {
            answer.Id = redis.As<answer>().GetNextSequence();
            answer.CreatedDate = DateTime.UtcNow;
        }

        //Store as a 'Related Answer' to the parent Question
        redis.As<question>().StoreRelatedEntities(answer.QuestionId, answer);
        //Populate user => answer index
        redis.AddItemToSet("urn:user>a:" + answer.UserId, answer.Id.ToString());
    }
}

public List<answer> GetAnswersForQuestion(long questionId)
{
    using (var redis = RedisManager.GetClient())
    {
        return redis.As<question>().GetRelatedEntities<answer>(questionId);
    }
}

public void VoteQuestionUp(long userId, long questionId)
{
    //Populate Question => User and User => Question set indexes in a single transaction
    RedisManager.ExecTrans(trans =>
    {
        //Register upvote against question and remove any downvotes if any
        trans.QueueCommand(redis => 
        redis.AddItemToSet("urn:q>user+:" + questionId, userId.ToString()));
        trans.QueueCommand(redis => 
        redis.RemoveItemFromSet("urn:q>user-:" + questionId, userId.ToString()));

        //Register upvote against user and remove any downvotes if any
        trans.QueueCommand(redis => 
        redis.AddItemToSet("urn:user>q+:" + userId, questionId.ToString()));
        trans.QueueCommand(redis => 
        redis.RemoveItemFromSet("urn:user>q-:" + userId, questionId.ToString()));
    });
}

public void VoteQuestionDown(long userId, long questionId)
{
    //Populate Question => User and User => Question set indexes in a single transaction
    RedisManager.ExecTrans(trans =>
    {
        //Register downvote against question and remove any upvotes if any
        trans.QueueCommand(redis => 
        redis.AddItemToSet("urn:q>user-:" + questionId, userId.ToString()));
        trans.QueueCommand(redis => 
        redis.RemoveItemFromSet("urn:q>user+:" + questionId, userId.ToString()));

        //Register downvote against user and remove any upvotes if any
        trans.QueueCommand(redis => 
        redis.AddItemToSet"urn:user>q-:" + userId, questionId.ToString()));
        trans.QueueCommand(redis => 
        redis.RemoveItemFromSet("urn:user>q+:" + userId, questionId.ToString()));
    });
}

public void VoteAnswerUp(long userId, long answerId)
{
    //Populate Question => User and User => Question set indexes in a single transaction
    RedisManager.ExecTrans(trans =>
    {
        //Register upvote against answer and remove any downvotes if any
        trans.QueueCommand(redis => 
        redis.AddItemToSet("urn:a>user+:" + answerId, userId.ToString()));
        trans.QueueCommand(redis => 
        redis.RemoveItemFromSet("urn:a>user-:" + answerId, userId.ToString()));

        //Register upvote against user and remove any downvotes if any
        trans.QueueCommand(redis => 
        redis.AddItemToSet("urn:user>a+:" + userId, answerId.ToString()));
        trans.QueueCommand(redis => 
        redis.RemoveItemFromSet("urn:user>a-:" + userId, answerId.ToString()));
    });
}

public void VoteAnswerDown(long userId, long answerId)
{
    //Populate Question => User and User => Question set indexes in a single transaction
    RedisManager.ExecTrans(trans =>
    {
        //Register downvote against answer and remove any upvotes if any
        trans.QueueCommand(redis => 
        redis.AddItemToSet("urn:a>user-:" + answerId, userId.ToString()));
        trans.QueueCommand(redis => 
        redis.RemoveItemFromSet("urn:a>user+:" + answerId, userId.ToString()));

        //Register downvote against user and remove any upvotes if any
        trans.QueueCommand(redis => 
        redis.AddItemToSet("urn:user>a-:" + userId, answerId.ToString()));
        trans.QueueCommand(redis => 
        redis.RemoveItemFromSet("urn:user>a+:" + userId, answerId.ToString()));
    });
}

public QuestionResult GetQuestion(long questionId)
{
    var question = RedisManager.ExecAs<question>
    (redisQuestions => redisQuestions.GetById(questionId));
    if (question == null) return null;

    var result = ToQuestionResults(new[] { question })[0];
    var answers = GetAnswersForQuestion(questionId);
    var uniqueUserIds = answers.ConvertAll(x => x.UserId).ToHashSet();
    var usersMap = GetUsersByIds(uniqueUserIds).ToDictionary(x => x.Id);

    result.Answers = answers.ConvertAll(answer =>
        new AnswerResult { Answer = answer, User = usersMap[answer.UserId] });

    return result;
}

public List<user> GetUsersByIds(IEnumerable<long> userIds)
{
    return RedisManager.ExecAs<user>(redisUsers => redisUsers.GetByIds(userIds)).ToList();
}

public QuestionStat GetQuestionStats(long questionId)
{
    using (var redis = RedisManager.GetReadOnlyClient())
    {
        var result = new QuestionStat
        {
            VotesUpCount = redis.GetSetCount("urn:q>user+:" +questionId),
            VotesDownCount = redis.GetSetCount("urn:q>user-:" + questionId)
        };
        result.VotesTotal = result.VotesUpCount - result.VotesDownCount;
        return result;
    }
}

public List<tag> GetTagsByPopularity(int skip, int take)
{
    using (var redis = RedisManager.GetReadOnlyClient())
    {
        var tagEntries = redis.GetRangeWithScoresFromSortedSetDesc("urn:tags", skip, take);
        var tags = tagEntries.ConvertAll(kvp => new Tag { Name = kvp.Key, Score = (int)kvp.Value });
        return tags;
    }
}

public SiteStats GetSiteStats()
{
    using (var redis = RedisManager.GetClient())
    {
        return new SiteStats
        {
            QuestionsCount = redis.As<question>().TypeIdsSet.Count,
            AnswersCount = redis.As<answer>().TypeIdsSet.Count,
            TopTags = GetTagsByPopularity(0, 10)
        };
    }
}

附加源代码说明

包含的程序包列表packages.config 中,
Funq IoC 配置,注册类型和当前控制器工厂 - 在 Global.asax 中 (属性依赖注入)
简单客户端的使用 - 在 home 控制器中
IoC 基于缓存的使用Question 和 Answer 控制器,以及 Global.asax 应用程序文件中。要查看其工作原理,您可以运行项目,并在浏览器中打开以下 URL: https://:37447/Question/GetQuestions?tag=test
您可以尝试使用标签,例如 test3, test11, test2, 等等。
Redis 缓存配置 - 在 Web.config (<system.web><sessionState> 部分) 和 RedisSessionStateProvider.cs 文件中。
MVC 项目中有很多 TODO,所以如果您想改进/继续,请更新并上传。

我非常希望有人能帮助构建一个使用 Redis (带 Funq IoC) 缓存的简单 UI 的 MVC 应用程序。Funq IoC 已经配置好,Question 控制器中有一个使用示例。

注意: 示例部分内容取自 "ServiceStack.Examples-master" 解决方案

结论。使用快速本地缓存优化应用程序中的缓存

由于 Redis 不本地存储数据 (无本地复制),因此通过将某些轻量级或用户相关的对象存储在本地缓存中 (以跳过序列化为字符串和客户端-服务器数据传输) 来优化性能可能是有意义的。例如,在 Web 应用程序中,最好使用 'System.Runtime.Caching.ObjectCache' 来处理轻量级对象,这些对象与用户相关且应用程序频繁使用。否则,当对象被通用使用且体积较大时,它必须保存在分布式 Redis 缓存中。用户相关对象的示例 - 配置文件信息、个性化信息。通用对象 - 本地化数据、不同用户之间共享的信息等。

链接

© . All rights reserved.