很久以前寫了一篇文章 .NET中使用Redis 介紹瞭如何安裝Redis服務端,以及如何在.NET中調用Redis讀取數據。本文簡單介紹如何設計NoSQL數據庫,以及如何使用Redis來存儲對象。
和傳統的關係型數據庫不同,NoSQL大部分都是以鍵值對存儲在內存中的,我們不能直接把RDBMS裏面的一些做法直接移植到NoSQL中來,一個最主要的原因是,在NoSQL中缺少RDBMS中的一些諸如join ,union以及一些在關係型數據庫中效率很高的執行語句,這些在NoSQL不能很好的支持,或者說效率低。
下文首先通過例子介紹在SQLServer中設計一個DB系統以及與NoSQL環境中設計一個DB的區別,最後演示如何在Redis中對數據進行讀寫操作。
一個簡單的博客系統
假設我們要設計一個簡單的博客系統,用戶可以註冊一個博客(Blog),然後可以在上面寫文章(Post),文章可以分類(Category)以及添加標籤(Tag),用戶可以對文章進行評論(Comment)。
在該系統中,我們需要實現,如下基本功能:
- 首頁:顯示所有人的博客
- 首頁:顯示最近的所有發表的文章
- 首頁:顯示所有的最近的評論
- 首頁:顯示博客的標籤雲
- 首頁:顯示所有的分類
- 文章頁面:顯示文章以及所有的評論
- 文章頁面:添加評論
- 標籤頁面:顯示所有標籤以及標籤對應的文章
- 分類頁面:顯示所有分類以及分類對應的文章
如果在SQLServer中,相信很簡單就可以設計出這樣一個DB了。
在NoSQL環境中,我們不能直接將上面的結構搬進來,所以需要根據需求重新設計我們的模型。
定義實體
在NoSQL環境下,所有的數據其實都是以key和value的形式存儲在內存中的,value通常是序列化爲字符串保存的。我們使用redis客戶端的時候,可以直接將對象存儲,這些客戶端在內部實現上幫助我們進行了序列化。所以第一步就是需要定義實體模型:
首先來看User實體:
public class User { public User() { this.BlogIds = new List<long>(); } public long Id { get; set; } public string Name { get; set; } public List<long> BlogIds { get; set; } }
User實體中,包含了用戶的Id,Name以及博客的Id。
然後Blog實體:
public class Blog { public Blog() { this.Tags = new List<string>(); this.BlogPostIds = new List<long>(); } public long Id { get; set; } public long UserId { get; set; } public string UserName { get; set; } public List<string> Tags { get; set; } public List<long> BlogPostIds { get; set; } }
包含了標籤Tag,以及文章Id列表。
文章BolgPost實體:
public class BlogPost { public BlogPost() { this.Categories = new List<string>(); this.Tags = new List<string>(); this.Comments = new List<BlogPostComment>(); } public long Id { get; set; } public long BlogId { get; set; } public string Title { get; set; } public string Content { get; set; } public List<string> Categories { get; set; } public List<string> Tags { get; set; } public List<BlogPostComment> Comments { get; set; } }
包含了一篇文章的基本信息,如文章分類,文章標籤,文章的評論。
最後看評論BlogPostComment實體:
public class BlogPostComment { public string Content { get; set; } public DateTime CreatedDate { get; set; } }
具體實現
實體定義好了之後,我們就可以開始具體實現了。爲了演示,這裏通過單元測試的方式實現具體功能:
首先要把Redis的服務端啓動起來,然後在工程中新建一個Redis客戶端,之後的所有操作都通過這個客戶端進行。
[TestFixture, Explicit, Category("Integration")] public class BlogPostExample { readonly RedisClient redis = new RedisClient("localhost"); [SetUp] public void OnBeforeEachTest() { redis.FlushAll(); InsertTestData(); } }
在單元測試的SetUp中,我們插入一些模擬數據,插入數據的方法爲InsetTestData方法:
public void InsertTestData() { var redisUsers = redis.As<User>(); var redisBlogs = redis.As<Blog>(); var redisBlogPosts = redis.As<BlogPost>(); var yangUser = new User { Id = redisUsers.GetNextSequence(), Name = "Eric Yang" }; var zhangUser = new User { Id = redisUsers.GetNextSequence(), Name = "Fish Zhang" }; var yangBlog = new Blog { Id = redisBlogs.GetNextSequence(), UserId = yangUser.Id, UserName = yangUser.Name, Tags = new List<string> { "Architecture", ".NET", "Databases" }, }; var zhangBlog = new Blog { Id = redisBlogs.GetNextSequence(), UserId = zhangUser.Id, UserName = zhangUser.Name, Tags = new List<string> { "Architecture", ".NET", "Databases" }, }; var blogPosts = new List<BlogPost> { new BlogPost { Id = redisBlogPosts.GetNextSequence(), BlogId = yangBlog.Id, Title = "Memcache", Categories = new List<string> { "NoSQL", "DocumentDB" }, Tags = new List<string> {"Memcache", "NoSQL", "JSON", ".NET"} , Comments = new List<BlogPostComment> { new BlogPostComment { Content = "First Comment!", CreatedDate = DateTime.UtcNow,}, new BlogPostComment { Content = "Second Comment!", CreatedDate = DateTime.UtcNow,}, } }, new BlogPost { Id = redisBlogPosts.GetNextSequence(), BlogId = zhangBlog.Id, Title = "Redis", Categories = new List<string> { "NoSQL", "Cache" }, Tags = new List<string> {"Redis", "NoSQL", "Scalability", "Performance"}, Comments = new List<BlogPostComment> { new BlogPostComment { Content = "First Comment!", CreatedDate = DateTime.UtcNow,} } }, new BlogPost { Id = redisBlogPosts.GetNextSequence(), BlogId = yangBlog.Id, Title = "Cassandra", Categories = new List<string> { "NoSQL", "Cluster" }, Tags = new List<string> {"Cassandra", "NoSQL", "Scalability", "Hashing"}, Comments = new List<BlogPostComment> { new BlogPostComment { Content = "First Comment!", CreatedDate = DateTime.UtcNow,} } }, new BlogPost { Id = redisBlogPosts.GetNextSequence(), BlogId = zhangBlog.Id, Title = "Couch Db", Categories = new List<string> { "NoSQL", "DocumentDB" }, Tags = new List<string> {"CouchDb", "NoSQL", "JSON"}, Comments = new List<BlogPostComment> { new BlogPostComment {Content = "First Comment!", CreatedDate = DateTime.UtcNow,} } }, }; yangUser.BlogIds.Add(yangBlog.Id); yangBlog.BlogPostIds.AddRange(blogPosts.Where(x => x.BlogId == yangBlog.Id).Map(x => x.Id)); zhangUser.BlogIds.Add(zhangBlog.Id); zhangBlog.BlogPostIds.AddRange(blogPosts.Where(x => x.BlogId == zhangBlog.Id).Map(x => x.Id)); redisUsers.Store(yangUser); redisUsers.Store(zhangUser); redisBlogs.StoreAll(new[] { yangBlog, zhangBlog }); redisBlogPosts.StoreAll(blogPosts); }
在方法中,首先在Redis中創建了三個強類型的IRedisTypedClient類型的對象redisUsers,redisBlogs,redisBlogPosts來保存用戶信息,博客信息,和文字信息。
var yangUser = new User { Id = redisUsers.GetNextSequence(), Name = "Eric Yang" };
在新建用戶的時候,因爲Id是自增字段,所以直接調用redisUsers這個client的GetNextSequence()方法就可以獲得一個自增的Id。
創建完用戶之後,接着創建博客信息:
var yangBlog = new Blog { Id = redisBlogs.GetNextSequence(), UserId = yangUser.Id, UserName = yangUser.Name, Tags = new List<string> { "Architecture", ".NET", "Databases" }, };
該博客有幾個標籤。
在接着創建該博客上發表的若干篇文章:
var blogPosts = new List<BlogPost> { new BlogPost { Id = redisBlogPosts.GetNextSequence(), BlogId = yangBlog.Id, Title = "Memcache", Categories = new List<string> { "NoSQL", "DocumentDB" }, Tags = new List<string> {"Memcache", "NoSQL", "JSON", ".NET"} , Comments = new List<BlogPostComment> { new BlogPostComment { Content = "First Comment!", CreatedDate = DateTime.UtcNow,}, new BlogPostComment { Content = "Second Comment!", CreatedDate = DateTime.UtcNow,}, } } }
每一篇文章都有分類和標籤,以及評論。
然後需要給user的BlogsIds和blog的BlogPostIds賦值
yangUser.BlogIds.Add(yangBlog.Id);
yangBlog.BlogPostIds.AddRange(blogPosts.Where(x => x.BlogId == yangBlog.Id).Map(x => x.Id));
最後需要把這些信息保存到redis中。
//保存用戶信息 redisUsers.Store(yangUser); redisUsers.Store(zhangUser); //保存博客信息 redisBlogs.StoreAll(new[] { yangBlog, zhangBlog }); //保存所有的文章信息 redisBlogPosts.StoreAll(blogPosts);
現在,利用Redis Desktop Manager,可以查看Reidis中存儲的數據:
數據準備好了之後,可以實現前面列出的一系列方法了:
顯示所有博客
該方法在GetAllBlogs中,實現如下:
[Test] public void Show_a_list_of_blogs() { var redisBlogs = redis.As<Blog>(); var blogs = redisBlogs.GetAll(); blogs.PrintDump(); }
只需要調用GetAll方法即可獲取內存中的所有指定類型的對象。
輸出結果爲:
[
{
Id: 1,
UserId: 1,
UserName: Eric Yang,
Tags:
[
Architecture,
.NET,
Databases
],
BlogPostIds:
[
1,
3
]
},
{
Id: 2,
UserId: 2,
UserName: Fish Zhang,
Tags:
[
Architecture,
.NET,
Databases
],
BlogPostIds:
[
2,
4
]
}
]
顯示最近發表的文章和評論
實現如下:
[Test] public void Show_a_list_of_recent_posts_and_comments() { //Get strongly-typed clients var redisBlogPosts = redis.As<BlogPost>(); var redisComments = redis.As<BlogPostComment>(); { //To keep this example let's pretend this is a new list of blog posts var newIncomingBlogPosts = redisBlogPosts.GetAll(); //Let's get back an IList<BlogPost> wrapper around a Redis server-side List. var recentPosts = redisBlogPosts.Lists["urn:BlogPost:RecentPosts"]; var recentComments = redisComments.Lists["urn:BlogPostComment:RecentComments"]; foreach (var newBlogPost in newIncomingBlogPosts) { //Prepend the new blog posts to the start of the 'RecentPosts' list recentPosts.Prepend(newBlogPost); //Prepend all the new blog post comments to the start of the 'RecentComments' list newBlogPost.Comments.ForEach(recentComments.Prepend); } //Make this a Rolling list by only keep the latest 3 posts and comments recentPosts.Trim(0, 2); recentComments.Trim(0, 2); //Print out the last 3 posts: recentPosts.GetAll().PrintDump(); recentComments.GetAll().PrintDump(); } }
方法中定義了兩個key爲urn:BlogPost:RecentPosts 和 urn:BlogPostComment:RecentComments的 List對象來保存最近發表的文章和評論:recentPosts.Prepend(newBlogPost)方法表示將新創建的文章插到recentPosts列表的最前面。
Trim方法表示僅保留n個在集合中。
顯示博客的標籤雲
顯示博客的標籤雲方法如下:
[Test] public void Show_a_TagCloud() { //Get strongly-typed clients var redisBlogPosts = redis.As<BlogPost>(); var newIncomingBlogPosts = redisBlogPosts.GetAll(); foreach (var newBlogPost in newIncomingBlogPosts) { //For every tag in each new blog post, increment the number of times each Tag has occurred newBlogPost.Tags.ForEach(x => redis.IncrementItemInSortedSet("urn:TagCloud", x, 1)); } //Show top 5 most popular tags with their scores var tagCloud = redis.GetRangeWithScoresFromSortedSetDesc("urn:TagCloud", 0, 4); tagCloud.PrintDump(); }
顯示標籤雲的實現,用到了redis中的SortedSet,IncrementItemInSortedSet表示如果有相同的話,值加一,GetRangeWithScoresFromSortedSetDesc方法,獲取某一key的前5個對象。
顯示所有的分類
顯示所有的分類用到了Set對象。
[Test] public void Show_all_Categories() { var redisBlogPosts = redis.As<BlogPost>(); var blogPosts = redisBlogPosts.GetAll(); foreach (var blogPost in blogPosts) { blogPost.Categories.ForEach(x => redis.AddItemToSet("urn:Categories", x)); } var uniqueCategories = redis.GetAllItemsFromSet("urn:Categories"); uniqueCategories.PrintDump(); }
顯示文章以及其評論
實現如下:
[Test] public void Show_post_and_all_comments() { //There is nothing special required here as since comments are Key Value Objects //they are stored and retrieved with the post var postId = 1; var redisBlogPosts = redis.As<BlogPost>(); var selectedBlogPost = redisBlogPosts.GetById(postId.ToString()); selectedBlogPost.PrintDump(); }
只需要把postId傳進去就可以通過GetById的方法獲取內存中的對象.
添加評論
首先根據PostId獲取BlogPost,然後在Comment屬性中添加一個BlogPostComment對象,然後在保存改BlogPost.
[Test] public void Add_comment_to_existing_post() { var postId = 1; var redisBlogPosts = redis.As<BlogPost>(); var blogPost = redisBlogPosts.GetById(postId.ToString()); blogPost.Comments.Add( new BlogPostComment { Content = "Third Post!", CreatedDate = DateTime.UtcNow }); redisBlogPosts.Store(blogPost); var refreshBlogPost = redisBlogPosts.GetById(postId.ToString()); refreshBlogPost.PrintDump(); }
顯示分類以及分類對應的文章
[Test] public void Show_all_Posts_for_the_DocumentDB_Category() { var redisBlogPosts = redis.As<BlogPost>(); var newIncomingBlogPosts = redisBlogPosts.GetAll(); foreach (var newBlogPost in newIncomingBlogPosts) { //For each post add it's Id into each of it's 'Cateogry > Posts' index newBlogPost.Categories.ForEach(x => redis.AddItemToSet("urn:Category:" + x, newBlogPost.Id.ToString())); } //Retrieve all the post ids for the category you want to view var documentDbPostIds = redis.GetAllItemsFromSet("urn:Category:DocumentDB"); //Make a batch call to retrieve all the posts containing the matching ids //(i.e. the DocumentDB Category posts) var documentDbPosts = redisBlogPosts.GetByIds(documentDbPostIds); documentDbPosts.PrintDump(); }
這裏首先把所有的文章按照標籤新建Set,把相同的分類的文章放到一個Set中,最後根據key即可查找到相應的集合。
總結
本文利用一個簡單的博客系統,簡要介紹瞭如何利用Redis存儲和獲取複雜的數據。由於本文主要爲了演示如何與Redis進行交互,所以實體設計的很簡陋,沒有按照DDD的思想進行設計,在某些設計方面沒有遵循前文淺談依賴注入中使用的原理和方法,後面會寫文章對該系統進行重構以使之更加完善。
希望本文對您瞭解如何利用Redis存儲複雜對象有所幫助。
轉自 http://www.cnblogs.com/yangecnu/p/Introduct-Redis-in-DotNET-Part2.html