|
很久以前寫了一篇文章 .NET中使用Redis 介紹了如何安裝Redis服務(wù)端,以及如何在.NET中調(diào)用Redis讀取數(shù)據(jù)。本文簡單介紹如何設(shè)計(jì)NoSQL數(shù)據(jù)庫,以及如何使用Redis來存儲對象。
和傳統(tǒng)的關(guān)系型數(shù)據(jù)庫不同,NoSQL大部分都是以鍵值對存儲在內(nèi)存中的,我們不能直接把RDBMS里面的一些做法直接移植到NoSQL中來,一個(gè)最主要的原因是,在NoSQL中缺少RDBMS中的一些諸如join ,union以及一些在關(guān)系型數(shù)據(jù)庫中效率很高的執(zhí)行語句,這些在NoSQL不能很好的支持,或者說效率低。
下文首先通過例子介紹在SQLServer中設(shè)計(jì)一個(gè)DB系統(tǒng)以及與NoSQL環(huán)境中設(shè)計(jì)一個(gè)DB的區(qū)別,最后演示如何在Redis中對數(shù)據(jù)進(jìn)行讀寫操作。
一個(gè)簡單的博客系統(tǒng)
假設(shè)我們要設(shè)計(jì)一個(gè)簡單的博客系統(tǒng),用戶可以注冊一個(gè)博客(Blog),然后可以在上面寫文章(Post),文章可以分類(Category)以及添加標(biāo)簽(Tag),用戶可以對文章進(jìn)行評論(Comment)。
在該系統(tǒng)中,我們需要實(shí)現(xiàn),如下基本功能:
如果在SQLServer中,相信很簡單就可以設(shè)計(jì)出這樣一個(gè)DB了。

在NoSQL環(huán)境中,我們不能直接將上面的結(jié)構(gòu)搬進(jìn)來,所以需要根據(jù)需求重新設(shè)計(jì)我們的模型。
定義實(shí)體 在NoSQL環(huán)境下,所有的數(shù)據(jù)其實(shí)都是以key和value的形式存儲在內(nèi)存中的,value通常是序列化為字符串保存的。我們使用redis客戶端的時(shí)候,可以直接將對象存儲,這些客戶端在內(nèi)部實(shí)現(xiàn)上幫助我們進(jìn)行了序列化。所以第一步就是需要定義實(shí)體模型:
首先來看User實(shí)體:
public class User { public User() { this.BlogIds = new List(); } public long Id { get; set; } public string Name { get; set; } public List BlogIds { get; set; }
}
User實(shí)體中,包含了用戶的Id,Name以及博客的Id。
然后Blog實(shí)體:
public class Blog { public Blog() { this.Tags = new List(); this.BlogPostIds = new List(); } public long Id { get; set; } public long UserId { get; set; } public string UserName { get; set; } public List Tags { get; set; } public List BlogPostIds { get; set; }
}
包含了標(biāo)簽Tag,以及文章Id列表。
文章BolgPost實(shí)體:
public class BlogPost { public BlogPost() { this.Categories = new List(); this.Tags = new List(); this.Comments = new List(); } public long Id { get; set; } public long BlogId { get; set; } public string Title { get; set; } public string Content { get; set; } public List Categories { get; set; } public List Tags { get; set; } public List Comments { get; set; }
}
包含了一篇文章的基本信息,如文章分類,文章標(biāo)簽,文章的評論。
最后看評論BlogPostComment實(shí)體:
public class BlogPostComment { public string Content { get; set; } public DateTime CreatedDate { get; set; }
}

具體實(shí)現(xiàn) 實(shí)體定義好了之后,我們就可以開始具體實(shí)現(xiàn)了。為了演示,這里通過單元測試的方式實(shí)現(xiàn)具體功能:
首先要把Redis的服務(wù)端啟動起來,然后在工程中新建一個(gè)Redis客戶端,之后的所有操作都通過這個(gè)客戶端進(jìn)行。
[TestFixture, Explicit, Category('Integration')] public class BlogPostExample { readonly RedisClient redis = new RedisClient('localhost'); [SetUp] public void OnBeforeEachTest() { redis.FlushAll(); InsertTestData(); }
}
在單元測試的SetUp中,我們插入一些模擬數(shù)據(jù),插入數(shù)據(jù)的方法為InsetTestData方法:
public void InsertTestData()
{ var redisUsers = redis.As(); var redisBlogs = redis.As(); var redisBlogPosts = redis.As(); 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 { 'Architecture', '.NET', 'Databases' }, }; var zhangBlog = new Blog { Id = redisBlogs.GetNextSequence(), UserId = zhangUser.Id, UserName = zhangUser.Name, Tags = new List { 'Architecture', '.NET', 'Databases' }, }; var blogPosts = new List { new BlogPost { Id = redisBlogPosts.GetNextSequence(), BlogId = yangBlog.Id, Title = 'Memcache', Categories = new List { 'NoSQL', 'DocumentDB' }, Tags = new List {'Memcache', 'NoSQL', 'JSON', '.NET'} , Comments = new List { 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 { 'NoSQL', 'Cache' }, Tags = new List {'Redis', 'NoSQL', 'Scalability', 'Performance'}, Comments = new List { new BlogPostComment { Content = 'First Comment!', CreatedDate = DateTime.UtcNow,} } }, new BlogPost { Id = redisBlogPosts.GetNextSequence(), BlogId = yangBlog.Id, Title = 'Cassandra', Categories = new List { 'NoSQL', 'Cluster' }, Tags = new List {'Cassandra', 'NoSQL', 'Scalability', 'Hashing'}, Comments = new List { new BlogPostComment { Content = 'First Comment!', CreatedDate = DateTime.UtcNow,} } }, new BlogPost { Id = redisBlogPosts.GetNextSequence(), BlogId = zhangBlog.Id, Title = 'Couch Db', Categories = new List { 'NoSQL', 'DocumentDB' }, Tags = new List {'CouchDb', 'NoSQL', 'JSON'}, Comments = new List { 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中創(chuàng)建了三個(gè)強(qiáng)類型的IRedisTypedClient類型的對象redisUsers,redisBlogs,redisBlogPosts來保存用戶信息,博客信息,和文字信息。
var yangUser = new User { Id = redisUsers.GetNextSequence(), Name = 'Eric Yang' };
在新建用戶的時(shí)候,因?yàn)镮d是自增字段,所以直接調(diào)用redisUsers這個(gè)client的GetNextSequence()方法就可以獲得一個(gè)自增的Id。
創(chuàng)建完用戶之后,接著創(chuàng)建博客信息:
var yangBlog = new Blog { Id = redisBlogs.GetNextSequence(), UserId = yangUser.Id, UserName = yangUser.Name, Tags = new List { 'Architecture', '.NET', 'Databases' },
};
該博客有幾個(gè)標(biāo)簽。
在接著創(chuàng)建該博客上發(fā)表的若干篇文章:
var blogPosts = new List { new BlogPost { Id = redisBlogPosts.GetNextSequence(), BlogId = yangBlog.Id, Title = 'Memcache', Categories = new List { 'NoSQL', 'DocumentDB' }, Tags = new List {'Memcache', 'NoSQL', 'JSON', '.NET'} , Comments = new List { new BlogPostComment { Content = 'First Comment!', CreatedDate = DateTime.UtcNow,}, new BlogPostComment { Content = 'Second Comment!', CreatedDate = DateTime.UtcNow,}, } }
}
每一篇文章都有分類和標(biāo)簽,以及評論。
然后需要給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);
現(xiàn)在,利用Redis Desktop Manager,可以查看Reidis中存儲的數(shù)據(jù):

數(shù)據(jù)準(zhǔn)備好了之后,可以實(shí)現(xiàn)前面列出的一系列方法了: 顯示所有博客
該方法在GetAllBlogs中,實(shí)現(xiàn)如下:
[Test] public void Show_a_list_of_blogs() { var redisBlogs = redis.As(); var blogs = redisBlogs.GetAll(); blogs.PrintDump();
}
只需要調(diào)用GetAll方法即可獲取內(nèi)存中的所有指定類型的對象。
輸出結(jié)果為:
[ { 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 ] }
]
顯示最近發(fā)表的文章和評論
實(shí)現(xiàn)如下:
[Test] public void Show_a_list_of_recent_posts_and_comments() { //Get strongly-typed clients var redisBlogPosts = redis.As(); var redisComments = redis.As(); { //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 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(); }
}
方法中定義了兩個(gè)key為urn:BlogPost:RecentPosts 和 urn:BlogPostComment:RecentComments的 List對象來保存最近發(fā)表的文章和評論:recentPosts.Prepend(newBlogPost)方法表示將新創(chuàng)建的文章插到recentPosts列表的最前面。
Trim方法表示僅保留n個(gè)在集合中。
顯示博客的標(biāo)簽云
顯示博客的標(biāo)簽云方法如下:
[Test] public void Show_a_TagCloud() { //Get strongly-typed clients var redisBlogPosts = redis.As(); 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();
}
顯示標(biāo)簽云的實(shí)現(xiàn),用到了redis中的SortedSet,IncrementItemInSortedSet表示如果有相同的話,值加一,GetRangeWithScoresFromSortedSetDesc方法,獲取某一key的前5個(gè)對象。
顯示所有的分類
顯示所有的分類用到了Set對象。
[Test] public void Show_all_Categories() { var redisBlogPosts = redis.As(); 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();
}
顯示文章以及其評論
實(shí)現(xiàn)如下:
[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(); var selectedBlogPost = redisBlogPosts.GetById(postId.ToString()); selectedBlogPost.PrintDump();
}
只需要把postId傳進(jìn)去就可以通過GetById的方法獲取內(nèi)存中的對象.
添加評論
首先根據(jù)PostId獲取BlogPost,然后在Comment屬性中添加一個(gè)BlogPostComment對象,然后在保存改BlogPost.
[Test] public void Add_comment_to_existing_post() { var postId = 1; var redisBlogPosts = redis.As(); 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();
}
顯示分類以及分類對應(yīng)的文章
[Test]
public void Show_all_Posts_for_the_DocumentDB_Category() { var redisBlogPosts = redis.As(); 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();
}
這里首先把所有的文章按照標(biāo)簽新建Set,把相同的分類的文章放到一個(gè)Set中,最后根據(jù)key即可查找到相應(yīng)的集合。
總結(jié) 本文利用一個(gè)簡單的博客系統(tǒng),簡要介紹了如何利用Redis存儲和獲取復(fù)雜的數(shù)據(jù)。由于本文主要為了演示如何與Redis進(jìn)行交互,所以實(shí)體設(shè)計(jì)的很簡陋,沒有按照DDD的思想進(jìn)行設(shè)計(jì),在某些設(shè)計(jì)方面沒有遵循前文淺談依賴注入中使用的原理和方法,后面會寫文章對該系統(tǒng)進(jìn)行重構(gòu)以使之更加完善。
希望本文對您了解如何利用Redis存儲復(fù)雜對象有所幫助。
參考資料
Designing NoSql Database Migrations Using Schemaless NoSql That No SQL Thing: The relational modeling anti pattern in document databases
原文出處:寒江獨(dú)釣 原文鏈接:http://www.cnblogs.com/yangecnu/p/Introduct-Redis-in-DotNET-Part2.html
|