C#客戶(hù)端Redis服務(wù)器的分布式緩存
介紹
在這篇文章中,我想介紹我知道的一種最緊湊的安裝和配置Redis服務(wù)器的方式。另外,我想簡(jiǎn)短地概述一下在.NET / C#客戶(hù)端下Redis hash(哈希類(lèi)型)和list(鏈表)的使用。
在這篇文章主要講到:
-
安裝Redis服務(wù)器(附完整的應(yīng)用程序文件設(shè)置)
-
Redis服務(wù)器保護(hù)(配置身份驗(yàn)證)
-
配置服務(wù)器復(fù)制
-
從C#應(yīng)用程序訪問(wèn)緩存
-
使用Redis ASP.NET會(huì)話狀態(tài)
-
Redis 集合(Set)、列表(List)和事務(wù)處理用法示例
-
說(shuō)明附加的源(Redis Funq LoC MVC項(xiàng)目:舉例)
-
緩存的優(yōu)化思路
背景
Redis是最快也是功能最豐富的內(nèi)存Key-Value數(shù)據(jù)存儲(chǔ)系統(tǒng)之一。
缺點(diǎn)
-
沒(méi)有本地?cái)?shù)據(jù)緩存(如在Azure緩存同步本地?cái)?shù)據(jù)緩存)
-
沒(méi)有完全集群化的支持(不過(guò),可能今年年底會(huì)實(shí)現(xiàn))
優(yōu)點(diǎn)
-
易于配置
-
使用簡(jiǎn)單
-
高性能
-
支持不同的數(shù)據(jù)類(lèi)型(如hash(哈希類(lèi)型)、list(鏈表)、set(集合)、sorted set(有序集))
-
ASP.NET會(huì)話集成
-
Web UI用于瀏覽緩存內(nèi)容
下面我將簡(jiǎn)單說(shuō)明如何在服務(wù)器上安裝和配置Redis,并用C#使用它。
Redis的安裝
從https://gith
下載從https://gith
Redis應(yīng)用程序的完整文件也可以從壓縮文件(x64)得到。
當(dāng)你擁有了全套的應(yīng)用程序文件(如下圖所示),
導(dǎo)航到應(yīng)用程序目錄,然后運(yùn)行以下命令:
sc create %name% binpath= "\"%binpath%\" %configpath%" start= "auto" DisplayName= "Redis"
其中:
-
%name%——服務(wù)實(shí)例的名稱(chēng),例如:redis-instance;
-
%binpath%——到項(xiàng)目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\""
即應(yīng)該是這樣的:
請(qǐng)確保有足夠的權(quán)限啟動(dòng)該服務(wù)。安裝完畢后,請(qǐng)檢查該服務(wù)是否創(chuàng)建成功,當(dāng)前是否正在運(yùn)行:
或者,你可以使用安裝程序(我沒(méi)試過(guò)):https://gith
Redis服務(wù)器保護(hù):密碼,IP過(guò)濾
保護(hù)Redis服務(wù)器的主要方式是使用Windows防火墻或活躍的網(wǎng)絡(luò)連接屬性設(shè)置IP過(guò)濾。此外,還可以使用Redis密碼設(shè)置額外保護(hù)。這需要用下面的方式更新Redis配置文件(redis.conf):
首先,找到這行:
# requirepass foobared
刪除開(kāi)頭的#符號(hào),用新密碼替換foobared:
requirepass foobared
然后,重新啟動(dòng)Redis Windows服務(wù)!
當(dāng)具體使用客戶(hù)端的時(shí)候,使用帶密碼的構(gòu)造函數(shù):
RedisClient client = new RedisClient(serverHost, port, redisPassword);
Redis服務(wù)器復(fù)制(主—從配置)
Redis支持主從同步,即,每次主服務(wù)器修改,從服務(wù)器得到通知,并自動(dòng)同步。大多復(fù)制用于讀取(但不能寫(xiě))擴(kuò)展和數(shù)據(jù)冗余和服務(wù)器故障轉(zhuǎn)移。設(shè) 置兩個(gè)Redis實(shí)例(在相同或不同服務(wù)器上的兩個(gè)服務(wù)),然后配置其中之一作為從站。為了讓Redis服務(wù)器實(shí)例是另一臺(tái)服務(wù)器的從屬,可以這樣更改配 置文件:
找到以下代碼:
# slaveof <masterip> <masterport>
替換為:
slaveof 192.168.1.1 6379
(可以自定義指定主服務(wù)器的真實(shí)IP和端口)。如果主服務(wù)器配置為需要密碼(驗(yàn)證),可以如下所示改變r(jià)edis.conf,找到這一行代碼:
# masterauth <master-password>
刪除開(kāi)頭的#符號(hào),用主服務(wù)器的密碼替換<master-password>,即:
masterauth mastpassword
現(xiàn)在這個(gè)Redis實(shí)例可以被用來(lái)作為主服務(wù)器的只讀同步副本。
用C#代碼使用Redis緩存
用C#代碼使用Redis運(yùn)行Manage NuGet包插件,找到ServiceStack.Redis包,并進(jìn)行安裝。
直接從實(shí)例化客戶(hù)端使用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");
- }
類(lèi)型化實(shí)體集更有意思和更實(shí)用,這是因?yàn)樗鼈儾僮鞯氖谴_切類(lèi)型的對(duì)象。在下面的代碼示例中,有兩個(gè)類(lèi)分別定義為Phone和Person——phone的主人。每個(gè)phone實(shí)例引用它的主人。下面的代碼演示我們?nèi)绾瓮ㄟ^(guò)標(biāo)準(zhǔn)添加、刪除和發(fā)現(xiàn)緩存項(xiàng):
- 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;
- }
在上面的例子中,我們實(shí)例化了輸入端IRedisTypedClient,它與緩存對(duì)象的特定類(lèi)型——Phone類(lèi)型一起工作。
Redis ASP.NET會(huì)話狀態(tài)
要用Redis提供商配置ASP.NET會(huì)話狀態(tài),添加新文件到你的Web項(xiàng)目,命名為RedisSessionStateProvider.cs,可以從https://gith
- <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>
注意,此密碼是可以選擇的,看服務(wù)器是否需要認(rèn)證。它必須被真實(shí)的值替換或刪除,如果Redis服務(wù)器不需要身份驗(yàn)證,那么服務(wù)器屬性和端口得由具體的數(shù)值代替(默認(rèn)端口為6379)。然后在項(xiàng)目中,你才可以使用會(huì)話狀態(tài):
- // 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();
- }
- //...
- }
結(jié)果:
ASP.NET輸出緩存提供者,并且Redis可以用類(lèi)似的方式進(jìn)行配置。
Redis Set(集合)和List(列表)
主要要注意的是,Redis列表實(shí)現(xiàn)IList<T>,而Redis集合實(shí)現(xiàn)ICollection<T>
。下面來(lái)說(shuō)說(shuō)如何使用它們。
當(dāng)需要區(qū)分相同類(lèi)型的不同分類(lèi)對(duì)象時(shí),使用列表。例如,我們有“mostSelling(熱銷(xiāo)手機(jī))”和“oldCollection(回收手機(jī))”兩個(gè)列表:
- 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");
- }
當(dāng)需要存儲(chǔ)相關(guān)的數(shù)據(jù)集和收集統(tǒng)計(jì)信息,例如answer -> queustion給答案或問(wèn)題投票時(shí),Redis集合就非常好使。假設(shè)我們有很多的問(wèn)題(queustion)和答案(answer ),需要將它們存儲(chǔ)在緩存中。使用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)
- };
- }
- }
附加資源說(shuō)明
項(xiàng)目中引用的一些包在packages.config文件中配置。
Funq IoC的相關(guān)配置,以及注冊(cè)類(lèi)型和當(dāng)前控制器目錄,在Global.asax文件中配置。
基于IoC的緩存使用以及Global.asax可以打開(kāi)以下URL:http://local
你可以將tag字段設(shè)置成test3,test1,test2等。
Redis緩存配置——在web config文件(<system.web><sessionState>節(jié)點(diǎn))以及RedisSessionStateProvider.cs文件中。
在MVC項(xiàng)目中有很多待辦事項(xiàng),因此,如果你想改進(jìn)/繼續(xù),請(qǐng)更新,并上傳。
如果有人能提供使用Redis(以及Funq IOC)緩存的MVC應(yīng)用程序示例,本人將不勝感激。Funq IOC已經(jīng)配置,使用示例已經(jīng)在Question controller中。
注:部分取樣于“ServiceStack.Examples-master”解決方案。
結(jié)論。優(yōu)化應(yīng)用程序緩存以及快速本地緩存
由于Redis并不在本地存儲(chǔ)(也不在本地復(fù)制)數(shù)據(jù),那么通過(guò)在本地緩存區(qū)存儲(chǔ)一些輕量級(jí)或用戶(hù)依賴(lài)的對(duì)象(跳過(guò)序列化字符串和客戶(hù)端—服務(wù)端數(shù)據(jù)轉(zhuǎn)換)來(lái)優(yōu)化性能是有意義的。例如,在Web應(yīng)用中,對(duì)于輕量級(jí)的對(duì)象使用’System.Runtime.Caching.ObjectCache
‘ 會(huì)更好——用戶(hù)依賴(lài),并且應(yīng)用程序時(shí)常要用。否則,當(dāng)經(jīng)常性地需要使用該對(duì)象時(shí),就必須在分布式Redis緩存中存儲(chǔ)大量容積的內(nèi)容。用戶(hù)依賴(lài)的對(duì)象舉例——個(gè)人資料信息,個(gè)性化信息 。常用對(duì)象——本地化數(shù)據(jù),不同用戶(hù)之間的共享信息,等等。
鏈接
如何運(yùn)行Redis服務(wù):
https://gith
文檔:
.NET / C#示例:
https://gith
關(guān)于如何用C#在Windows上使用Redis的好建議:
http://maxiv
http://www.p
關(guān)于Redis:
https://gith
Azure緩存
http://kotug
許可證
這篇文章,以及任何相關(guān)的源代碼和文件,依據(jù)The Code Project Open License (CPOL)。
譯文鏈接:http://www.codeceo.com/article/distributed-caching-redis-server.html
英文原文:Distributed Caching using Redis Server with .NET/C# Client