分享

.NET中使用Redis (二)

 weijianian 2016-08-07

很久以前写了一篇文章 .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();
}

public long Id { get; set; }
public string Name { get; set; }
public List BlogIds { get; set; }

}


User实体中,包含了用户的Id,Name以及博客的Id。


然后Blog实体:


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; }

}


包含了标签Tag,以及文章Id列表。


文章BolgPost实体:


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; }

}


包含了一篇文章的基本信息,如文章分类,文章标签,文章的评论。


最后看评论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();
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中创建了三个强类型的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 { '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,},
}
}

}


每一篇文章都有分类和标签,以及评论。


然后需要给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();
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();
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();
}

}


方法中定义了两个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();
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();
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();
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();
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();
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存储复杂对象有所帮助。

参考资料

Designing NoSql Database
Migrations Using Schemaless NoSql
That No SQL Thing: The relational modeling anti pattern in document databases


原文出处:寒江独钓

原文链接:http://www.cnblogs.com/yangecnu/p/Introduct-Redis-in-DotNET-Part2.html



    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多