Redis服务器的分布式缓存
发布日期:2016-4-24 11:4:1
Redis服务器的分布式缓存 一、介绍 在这篇文章中,我想介绍一种最紧凑的安装与配置Redis服务器的方式。此外,我想简短地概述一下在.NET / C#客户端下Redis hash(哈希类型)与list(链表)的使用。 在这篇文章主要讲以下几个内容:
二、背景 Redis是最快也是功能最丰富的内存Key-Value数据存储系统之一。 1.缺点 redis有以下缺点:
2.优点 redis有以下优点:
下面我将简单说明如何在服务器上安装与配置Redis,并用C#使用它。 三、Redis的安装 1.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应用程序的完整文件也可以从压缩文件(x64)得到。 当你拥有了全套的应用程序文件,如图1所示: 图1 导航到应用程序目录,然后运行以下所示的命令: sc create %name% binpath= "\"%binpath%\" %configpath%" start= "auto" DisplayName= "Redis" 注释:
例: sc create Redis start= auto DisplayName= Redis binpath= "\"C:\Program Files\Redis\RedisService_1.1.exe\ " \"C:\Program Files\Redis\redis.conf\"" 即应该是这样的,如图2所示: 图2 请确保有足够的权限启动该服务。安装完毕后,请检查该服务是否创建成功,检查当前是否正在运行,如图3所示: 图3 或者,你也可以使用安装程序(我没试过):https://github.com/rgl/redis/downloads。 2.Redis服务器保护:密码,IP过滤 使用Windows防火墙或者活跃的网络连接属性设置IP过滤是保护Redis服务器的主要方式(类似mssql)。此外,还可以使用Redis密码设置额外保护。这需要用下面所示的方式更新Redis配置文件(redis.conf): 首先,找到这行,如下所示: # requirepass foobared 删除开头的#符号,用新密码替换foobared,如下所示: requirepass foobared 然后,重新启动Redis Windows服务! 当具体使用客户端的时候,使用带密码的构造函数,如下所示: RedisClient client = new RedisClient(serverHost, port, redisPassword); 3.Redis服务器复制(主—从配置) Redis支持主从同步,即,每次主服务器修改,从服务器得到通知,并自动同步。大多复制用于读取(但不能写)扩展和数据冗余和服务器故障转移。设置两个Redis实例(在相同或不同服务器上的两个服务),然后配置其中之一作为从站。为了让Redis服务器实例是另一台服务器的从属,可以这样更改配置文件: 找到以下代码,如下所示: # slaveof 替换为下面所示的代码: slaveof 192.168.1.1 6379 (可以自定义指定主服务器的真实IP和端口)。如果主服务器配置为需要密码(验证),可以如下所示改变redis.conf,找到这一行代码: # masterauth 删除开头的#符号,用主服务器的密码替换,即下面所示的代码: masterauth mastpassword 现在这个Redis实例可以被用来作为主服务器的只读同步副本。 4.用C#代码使用Redis缓存 用C#代码使用Redis运行Manage NuGet包插件,找到ServiceStack.Redis包,并进行安装。如图4所示: 图4 直接从实例化客户端使用Set/Get方法示例,如下所示: string host = "localhost"; string elementKey = "testKeyRedis"; using (RedisClient redisClient = new RedisClient(host)) { if (redisClient.Get(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("some cached value"); } 类型化实体集更有意思、更实用,这是因为它们操作的是确切类型的对象,也在很多数据库也有用,如mssql。在下面的代码示例中,有两个类分别定义为Phone与Person——phone的主人。每个phone实例引用它的主人。下面的代码演示我们如何通过标准添加、删除与发现缓存项,如下所示 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 phones = redisClient.As(); 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类型一起工作。 5.Redis ASP.NET会话状态 要用Redis提供商配置ASP.NET会话状态,添加新文件到你的Web项目,命名为RedisSessionStateProvider.cs,可以从https://github.com/chadman/redis-service-provider/raw/master/RedisProvider/SessionProvider/RedisSessionProvider.cs复制代码,然后添加或更改配置文件中的以下部分(sessionState标签已经内置于system.web标签),或者你也可以下载附加来源与复制代码。 customprovider="RedisSessionStateProvider" cookieless="false"> type="RedisProvider.SessionProvider.CustomServiceProvider" server="localhost" port="6379" password="pasword"> 请注意,这个密码是可以选择的,看服务器是否需要认证。它必须被真实的值替换或者删除,如果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 controller(主控制器),如下所示: public class HomeController : Controller { public ActionResult Index() { //... ViewBag.Message = Session["testRedisSession"]; return View(); } //... } 结果如图5所示: 图5 ASP.NET输出缓存提供者,并且Redis可以用类似的方式进行配置。 6.Redis Set(集合)与List(列表) 在这里主要要注意的是,Redis列表实现IList,而Redis集合实现ICollection。下面我们来说说如何使用它们。 当需要区分相同类型的不同分类对象时,使用列表。例如,我们有“mostSelling(热销手机)”与“oldCollection(回收手机)”两个列表,如下所示: 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 redis = redisClient.As(); IRedisList mostSelling = redis.Lists["urn:phones:mostselling"]; IRedisList 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 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"); } 当需要存储相关的数据集和收集统计信息,例如answer -> queustion给答案或问题投票时,Redis集合就非常好使。假设我们有很多的问题(queustion)和答案(answer ),需要将它们存储在缓存中。使用Redis,我们可以这么做。如下所示: /// /// Gets or sets the Redis Manager. The built-in IoC used with ServiceStack autowires this property. /// IRedisClientsManager RedisManager { get; set; } /// /// Delete question by performing compensating actions to /// StoreQuestion() to keep the datastore in a consistent state /// /// public void DeleteQuestion(long questionId) { using (var redis = RedisManager.GetClient()) { var redisQuestions = redis.As(); 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(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(); if (question.Tags == null) question.Tags = new List(); 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())); } } /// /// Delete Answer by performing compensating actions to /// StoreAnswer() to keep the datastore in a consistent state /// /// /// public void DeleteAnswer(long questionId, long answerId) { using (var redis = RedisManager.GetClient()) { var answer = redis.As().GetRelatedEntities (questionId).FirstOrDefault(x => x.Id == answerId); if (answer == null) return; redis.As().DeleteRelatedEntity(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().GetNextSequence(); answer.CreatedDate = DateTime.UtcNow; } //Store as a 'Related Answer' to the parent Question redis.As().StoreRelatedEntities(answer.QuestionId, answer); //Populate user => answer index redis.AddItemToSet("urn:user>a:" + answer.UserId, answer.Id.ToString()); } } public List GetAnswersForQuestion(long questionId) { using (var redis = RedisManager.GetClient()) { return redis.As().GetRelatedEntities(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 (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 GetUsersByIds(IEnumerable userIds) { return RedisManager.ExecAs(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 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().TypeIdsSet.Count, AnswersCount = redis.As().TypeIdsSet.Count, TopTags = GetTagsByPopularity(0, 10) }; } } 7.附加资源说明 项目中引用的一些包在packages.config文件中配置。 Funq IoC的相关配置,以及注册类型与当前控制器目录,在Global.asax文件中配置。 基于IoC的缓存使用以及Global.asax可以打开以下URL:http://localhost:37447/Question/GetQuestions?tag=test 查看。 你可以将tag字段设置成test3,test1,test2等。 Redis缓存配置——在web config文件(节点)以及RedisSessionStateProvider.cs文件中。 在MVC项目中有很多待办事项,因此,如果你想改进/继续,请更新,并上传。 如果有人能提供使用Redis(以及Funq IOC)缓存的MVC应用程序示例,本人将不胜感激。Funq IOC已经配置,使用示例已经在Question controller中。 说明:部分取样于“ServiceStack.Examples-master”解决方案。 四、结论。优化应用程序缓存以及快速本地缓存 因为Redis并不在本地存储(也不在本地复制)数据,那么通过在本地缓存区存储一些轻量级或用户依赖的对象(跳过序列化字符串和客户端—服务端数据转换)来优化性能是有意义的。比如,在Web应用中,对于轻量级的对象使用’System.Runtime.Caching.ObjectCache‘ 会更好——用户依赖,并且应用程序时常要用。否则,当经常性地需要使用该对象时,就必须在分布式Redis缓存中存储大量容积的内容。用户依赖的对象举例——个人资料信息,个性化信息 。常用对象——本地化数据,不同用户之间的共享信息,等等。 下载源代码(Redis Funq LoC MVC 4版本) 五、链接 以下为资源的网址:
六、许可证 这篇文章,以及任何相关的源代码与文件,依据The Code Project Open License (CPOL)。
|