C#客戶端Redis伺服器的分散式快取
轉自:http://www.codeceo.com/article/distributed-caching-redis-server.html
介紹
在這篇文章中,我想介紹我知道的一種最緊湊的安裝和配置Redis伺服器的方式。另外,我想簡短地概述一下在.NET / C#客戶端下Redis hash(雜湊型別)和list(連結串列)的使用。
在這篇文章主要講到:
- Redis伺服器保護(配置身份驗證)
- 配置伺服器複製
- 從C#應用程式訪問快取
- 使用Redis ASP.NET會話狀態
- Redis 集合(Set)、列表(List)和事務處理用法示例
- 說明附加的源(Redis Funq LoC MVC專案:舉例)
- 快取的優化思路
背景
Redis是最快也是功能最豐富的記憶體Key-Value資料儲存系統之一。
缺點
- 沒有本地資料快取(如在Azure快取同步本地資料快取)
- 沒有完全叢集化的支援(不過,可能今年年底會實現)
優點
- 易於配置
- 使用簡單
- 高效能
- 支援不同的資料型別(如hash(雜湊型別)、list(連結串列)、set(集合)、sorted set(有序集))
- ASP.NET會話整合
- Web UI用於瀏覽快取內容
下面我將簡單說明如何在伺服器上安裝和配置Redis,並用C#使用它。
Redis的安裝
Redis應用程式的完整檔案也可以從壓縮檔案(x64)得到。
當你擁有了全套的應用程式檔案(如下圖所示),
導航到應用程式目錄,然後執行以下命令:
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\""
即應該是這樣的:
請確保有足夠的許可權啟動該服務。安裝完畢後,請檢查該服務是否建立成功,當前是否正在執行:
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例項(在相同或不同伺服器上的兩個服務),然後配置其中之一作為從站。為了讓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執行Manage 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");
}
型別化實體集更有意思和更實用,這是因為它們操作的是確切型別的物件。在下面的程式碼示例中,有兩個類分別定義為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<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型別一起工作。
Redis ASP.NET會話狀態
<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 controller(主控制器):
public class HomeController : Controller
{
public ActionResult Index()
{
//...
ViewBag.Message = Session["testRedisSession"];
return View();
}
//...
}
結果:
ASP.NET輸出快取提供者,並且Redis可以用類似的方式進行配置。
Redis Set(集合)和List(列表)
主要要注意的是,Redis列表實現IList<T>,而Redis集合實現ICollection<T>
。下面來說說如何使用它們。
當需要區分相同型別的不同分類物件時,使用列表。例如,我們有“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<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");
}
當需要儲存相關的資料集和收集統計資訊,例如answer -> queustion給答案或問題投票時,Redis集合就非常好使。假設我們有很多的問題(queustion)和答案(answer ),需要將它們儲存在快取中。使用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檔案中配置。
基於IoC的快取使用以及Global.asax可以開啟以下URL:http://localhost:37447/Question/GetQuestions?tag=test 檢視。
你可以將tag欄位設定成test3,test1,test2等。
Redis快取配置——在web config檔案(<system.web><sessionState>節點)以及RedisSessionStateProvider.cs檔案中。
在MVC專案中有很多待辦事項,因此,如果你想改進/繼續,請更新,並上傳。如果有人能提供使用Redis(以及Funq IOC)快取的MVC應用程式示例,本人將不勝感激。Funq IOC已經配置,使用示例已經在Question controller中。
注:部分取樣於“ServiceStack.Examples-master”解決方案。
結論。優化應用程式快取以及快速本地快取
由於Redis並不在本地儲存(也不在本地複製)資料,那麼通過在本地快取區儲存一些輕量級或使用者依賴的物件(跳過序列化字串和客戶端—服務端資料轉換)來優化效能是有意義的。例如,在Web應用中,對於輕量級的物件使用’System.Runtime.Caching.ObjectCache
‘
會更好——使用者依賴,並且應用程式時常要用。否則,當經常性地需要使用該物件時,就必須在分散式Redis快取中儲存大量容積的內容。使用者依賴的物件舉例——個人資料資訊,個性化資訊 。常用物件——本地化資料,不同使用者之間的共享資訊,等等。
連結
如何執行Redis服務:
文件:
.NET / C#示例:
關於如何用C#在Windows上使用Redis的好建議:
關於Redis:
Azure快取
許可證
這篇文章,以及任何相關的原始碼和檔案,依據The Code Project Open License (CPOL)。