很久以前写了一篇文章 .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实体:

  1. public class User
  2. {
  3. public User()
  4. {
  5. this.BlogIds = new List<long>();
  6. }
  7.  
  8. public long Id { get; set; }
  9. public string Name { get; set; }
  10. public List<long> BlogIds { get; set; }
  11. }

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

然后Blog实体:

  1. public class Blog
  2. {
  3. public Blog()
  4. {
  5. this.Tags = new List<string>();
  6. this.BlogPostIds = new List<long>();
  7. }
  8.  
  9. public long Id { get; set; }
  10. public long UserId { get; set; }
  11. public string UserName { get; set; }
  12. public List<string> Tags { get; set; }
  13. public List<long> BlogPostIds { get; set; }
  14. }

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

文章BolgPost实体:

  1. public class BlogPost
  2. {
  3. public BlogPost()
  4. {
  5. this.Categories = new List<string>();
  6. this.Tags = new List<string>();
  7. this.Comments = new List<BlogPostComment>();
  8. }
  9.  
  10. public long Id { get; set; }
  11. public long BlogId { get; set; }
  12. public string Title { get; set; }
  13. public string Content { get; set; }
  14. public List<string> Categories { get; set; }
  15. public List<string> Tags { get; set; }
  16. public List<BlogPostComment> Comments { get; set; }
  17. }

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

最后看评论BlogPostComment实体:

  1. public class BlogPostComment
  2. {
  3. public string Content { get; set; }
  4. public DateTime CreatedDate { get; set; }
  5. }

具体实现

实体定义好了之后,我们就可以开始具体实现了。为了演示,这里通过单元测试的方式实现具体功能:

首先要把Redis的服务端启动起来,然后在工程中新建一个Redis客户端,之后的所有操作都通过这个客户端进行。

  1. [TestFixture, Explicit, Category("Integration")]
  2. public class BlogPostExample
  3. {
  4. readonly RedisClient redis = new RedisClient("localhost");
  5.  
  6. [SetUp]
  7. public void OnBeforeEachTest()
  8. {
  9. redis.FlushAll();
  10. InsertTestData();
  11. }
  12. }

在单元测试的SetUp中,我们插入一些模拟数据,插入数据的方法为InsetTestData方法:

  1. public void InsertTestData()
  2. {
  3. var redisUsers = redis.As<User>();
  4. var redisBlogs = redis.As<Blog>();
  5. var redisBlogPosts = redis.As<BlogPost>();
  6.  
  7. var yangUser = new User { Id = redisUsers.GetNextSequence(), Name = "Eric Yang" };
  8. var zhangUser = new User { Id = redisUsers.GetNextSequence(), Name = "Fish Zhang" };
  9.  
  10. var yangBlog = new Blog
  11. {
  12. Id = redisBlogs.GetNextSequence(),
  13. UserId = yangUser.Id,
  14. UserName = yangUser.Name,
  15. Tags = new List<string> { "Architecture", ".NET", "Databases" },
  16. };
  17.  
  18. var zhangBlog = new Blog
  19. {
  20. Id = redisBlogs.GetNextSequence(),
  21. UserId = zhangUser.Id,
  22. UserName = zhangUser.Name,
  23. Tags = new List<string> { "Architecture", ".NET", "Databases" },
  24. };
  25.  
  26. var blogPosts = new List<BlogPost>
  27. {
  28. new BlogPost
  29. {
  30. Id = redisBlogPosts.GetNextSequence(),
  31. BlogId = yangBlog.Id,
  32. Title = "Memcache",
  33. Categories = new List<string> { "NoSQL", "DocumentDB" },
  34. Tags = new List<string> {"Memcache", "NoSQL", "JSON", ".NET"} ,
  35. Comments = new List<BlogPostComment>
  36. {
  37. new BlogPostComment { Content = "First Comment!", CreatedDate = DateTime.UtcNow,},
  38. new BlogPostComment { Content = "Second Comment!", CreatedDate = DateTime.UtcNow,},
  39. }
  40. },
  41. new BlogPost
  42. {
  43. Id = redisBlogPosts.GetNextSequence(),
  44. BlogId = zhangBlog.Id,
  45. Title = "Redis",
  46. Categories = new List<string> { "NoSQL", "Cache" },
  47. Tags = new List<string> {"Redis", "NoSQL", "Scalability", "Performance"},
  48. Comments = new List<BlogPostComment>
  49. {
  50. new BlogPostComment { Content = "First Comment!", CreatedDate = DateTime.UtcNow,}
  51. }
  52. },
  53. new BlogPost
  54. {
  55. Id = redisBlogPosts.GetNextSequence(),
  56. BlogId = yangBlog.Id,
  57. Title = "Cassandra",
  58. Categories = new List<string> { "NoSQL", "Cluster" },
  59. Tags = new List<string> {"Cassandra", "NoSQL", "Scalability", "Hashing"},
  60. Comments = new List<BlogPostComment>
  61. {
  62. new BlogPostComment { Content = "First Comment!", CreatedDate = DateTime.UtcNow,}
  63. }
  64. },
  65. new BlogPost
  66. {
  67. Id = redisBlogPosts.GetNextSequence(),
  68. BlogId = zhangBlog.Id,
  69. Title = "Couch Db",
  70. Categories = new List<string> { "NoSQL", "DocumentDB" },
  71. Tags = new List<string> {"CouchDb", "NoSQL", "JSON"},
  72. Comments = new List<BlogPostComment>
  73. {
  74. new BlogPostComment {Content = "First Comment!", CreatedDate = DateTime.UtcNow,}
  75. }
  76. },
  77. };
  78.  
  79. yangUser.BlogIds.Add(yangBlog.Id);
  80. yangBlog.BlogPostIds.AddRange(blogPosts.Where(x => x.BlogId == yangBlog.Id).Map(x => x.Id));
  81.  
  82. zhangUser.BlogIds.Add(zhangBlog.Id);
  83. zhangBlog.BlogPostIds.AddRange(blogPosts.Where(x => x.BlogId == zhangBlog.Id).Map(x => x.Id));
  84.  
  85. redisUsers.Store(yangUser);
  86. redisUsers.Store(zhangUser);
  87. redisBlogs.StoreAll(new[] { yangBlog, zhangBlog });
  88. redisBlogPosts.StoreAll(blogPosts);
  89. }

在方法中,首先在Redis中创建了三个强类型的IRedisTypedClient类型的对象redisUsers,redisBlogs,redisBlogPosts来保存用户信息,博客信息,和文字信息。

  1. var yangUser = new User { Id = redisUsers.GetNextSequence(), Name = "Eric Yang" };

在新建用户的时候,因为Id是自增字段,所以直接调用redisUsers这个client的GetNextSequence()方法就可以获得一个自增的Id。

创建完用户之后,接着创建博客信息:

  1. var yangBlog = new Blog
  2. {
  3. Id = redisBlogs.GetNextSequence(),
  4. UserId = yangUser.Id,
  5. UserName = yangUser.Name,
  6. Tags = new List<string> { "Architecture", ".NET", "Databases" },
  7. };

该博客有几个标签。

在接着创建该博客上发表的若干篇文章:

  1. var blogPosts = new List<BlogPost>
  2. {
  3. new BlogPost
  4. {
  5. Id = redisBlogPosts.GetNextSequence(),
  6. BlogId = yangBlog.Id,
  7. Title = "Memcache",
  8. Categories = new List<string> { "NoSQL", "DocumentDB" },
  9. Tags = new List<string> {"Memcache", "NoSQL", "JSON", ".NET"} ,
  10. Comments = new List<BlogPostComment>
  11. {
  12. new BlogPostComment { Content = "First Comment!", CreatedDate = DateTime.UtcNow,},
  13. new BlogPostComment { Content = "Second Comment!", CreatedDate = DateTime.UtcNow,},
  14. }
  15. }
  16. }

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

然后需要给user的BlogsIds和blog的BlogPostIds赋值

  1. yangUser.BlogIds.Add(yangBlog.Id);
  2. yangBlog.BlogPostIds.AddRange(blogPosts.Where(x => x.BlogId == yangBlog.Id).Map(x => x.Id));

最后需要把这些信息保存到redis中。

  1. //保存用户信息
  2. redisUsers.Store(yangUser);
  3. redisUsers.Store(zhangUser);
  4. //保存博客信息
  5. redisBlogs.StoreAll(new[] { yangBlog, zhangBlog });
  6. //保存所有的文章信息
  7. redisBlogPosts.StoreAll(blogPosts);

现在,利用Redis Desktop Manager,可以查看Reidis中存储的数据:

数据准备好了之后,可以实现前面列出的一系列方法了:

显示所有博客

该方法在GetAllBlogs中,实现如下:

  1. [Test]
  2. public void Show_a_list_of_blogs()
  3. {
  4. var redisBlogs = redis.As<Blog>();
  5. var blogs = redisBlogs.GetAll();
  6. blogs.PrintDump();
  7. }

只需要调用GetAll方法即可获取内存中的所有指定类型的对象。

输出结果为:

  1. [
  2. {
  3.  
  4. Id: 1,
  5. UserId: 1,
  6. UserName: Eric Yang,
  7. Tags:
  8. [
  9. Architecture,
  10. .NET,
  11. Databases
  12. ],
  13. BlogPostIds:
  14. [
  15. 1,
  16. 3
  17. ]
  18. },
  19. {
  20. Id: 2,
  21. UserId: 2,
  22. UserName: Fish Zhang,
  23. Tags:
  24. [
  25. Architecture,
  26. .NET,
  27. Databases
  28. ],
  29. BlogPostIds:
  30. [
  31. 2,
  32. 4
  33. ]
  34. }
  35. ]

显示最近发表的文章和评论

实现如下:

  1. [Test]
  2. public void Show_a_list_of_recent_posts_and_comments()
  3. {
  4. //Get strongly-typed clients
  5. var redisBlogPosts = redis.As<BlogPost>();
  6. var redisComments = redis.As<BlogPostComment>();
  7. {
  8. //To keep this example let's pretend this is a new list of blog posts
  9. var newIncomingBlogPosts = redisBlogPosts.GetAll();
  10.  
  11. //Let's get back an IList<BlogPost> wrapper around a Redis server-side List.
  12. var recentPosts = redisBlogPosts.Lists["urn:BlogPost:RecentPosts"];
  13. var recentComments = redisComments.Lists["urn:BlogPostComment:RecentComments"];
  14.  
  15. foreach (var newBlogPost in newIncomingBlogPosts)
  16. {
  17. //Prepend the new blog posts to the start of the 'RecentPosts' list
  18. recentPosts.Prepend(newBlogPost);
  19.  
  20. //Prepend all the new blog post comments to the start of the 'RecentComments' list
  21. newBlogPost.Comments.ForEach(recentComments.Prepend);
  22. }
  23.  
  24. //Make this a Rolling list by only keep the latest 3 posts and comments
  25. recentPosts.Trim(0, 2);
  26. recentComments.Trim(0, 2);
  27.  
  28. //Print out the last 3 posts:
  29. recentPosts.GetAll().PrintDump();
  30. recentComments.GetAll().PrintDump();
  31. }
  32. }

方法中定义了两个key为urn:BlogPost:RecentPosts 和 urn:BlogPostComment:RecentComments的 List对象来保存最近发表的文章和评论:recentPosts.Prepend(newBlogPost)方法表示将新创建的文章插到recentPosts列表的最前面。

Trim方法表示仅保留n个在集合中。

显示博客的标签云

显示博客的标签云方法如下:

  1. [Test]
  2. public void Show_a_TagCloud()
  3. {
  4. //Get strongly-typed clients
  5. var redisBlogPosts = redis.As<BlogPost>();
  6. var newIncomingBlogPosts = redisBlogPosts.GetAll();
  7.  
  8. foreach (var newBlogPost in newIncomingBlogPosts)
  9. {
  10. //For every tag in each new blog post, increment the number of times each Tag has occurred
  11. newBlogPost.Tags.ForEach(x =>
  12. redis.IncrementItemInSortedSet("urn:TagCloud", x, 1));
  13. }
  14.  
  15. //Show top 5 most popular tags with their scores
  16. var tagCloud = redis.GetRangeWithScoresFromSortedSetDesc("urn:TagCloud", 0, 4);
  17. tagCloud.PrintDump();
  18. }

显示标签云的实现,用到了redis中的SortedSet,IncrementItemInSortedSet表示如果有相同的话,值加一,GetRangeWithScoresFromSortedSetDesc方法,获取某一key的前5个对象。

显示所有的分类

显示所有的分类用到了Set对象。

  1. [Test]
  2. public void Show_all_Categories()
  3. {
  4. var redisBlogPosts = redis.As<BlogPost>();
  5. var blogPosts = redisBlogPosts.GetAll();
  6.  
  7. foreach (var blogPost in blogPosts)
  8. {
  9. blogPost.Categories.ForEach(x =>
  10. redis.AddItemToSet("urn:Categories", x));
  11. }
  12.  
  13. var uniqueCategories = redis.GetAllItemsFromSet("urn:Categories");
  14. uniqueCategories.PrintDump();
  15. }

显示文章以及其评论

实现如下:

  1. [Test]
  2. public void Show_post_and_all_comments()
  3. {
  4. //There is nothing special required here as since comments are Key Value Objects
  5. //they are stored and retrieved with the post
  6. var postId = 1;
  7. var redisBlogPosts = redis.As<BlogPost>();
  8. var selectedBlogPost = redisBlogPosts.GetById(postId.ToString());
  9.  
  10. selectedBlogPost.PrintDump();
  11. }

只需要把postId传进去就可以通过GetById的方法获取内存中的对象.

添加评论

首先根据PostId获取BlogPost,然后在Comment属性中添加一个BlogPostComment对象,然后在保存改BlogPost.

  1. [Test]
  2. public void Add_comment_to_existing_post()
  3. {
  4. var postId = 1;
  5. var redisBlogPosts = redis.As<BlogPost>();
  6. var blogPost = redisBlogPosts.GetById(postId.ToString());
  7. blogPost.Comments.Add(
  8. new BlogPostComment { Content = "Third Post!", CreatedDate = DateTime.UtcNow });
  9. redisBlogPosts.Store(blogPost);
  10.  
  11. var refreshBlogPost = redisBlogPosts.GetById(postId.ToString());
  12. refreshBlogPost.PrintDump();
  13. }

显示分类以及分类对应的文章

  1. [Test]
  2. public void Show_all_Posts_for_the_DocumentDB_Category()
  3. {
  4. var redisBlogPosts = redis.As<BlogPost>();
  5. var newIncomingBlogPosts = redisBlogPosts.GetAll();
  6.  
  7. foreach (var newBlogPost in newIncomingBlogPosts)
  8. {
  9. //For each post add it's Id into each of it's 'Cateogry > Posts' index
  10. newBlogPost.Categories.ForEach(x =>
  11. redis.AddItemToSet("urn:Category:" + x, newBlogPost.Id.ToString()));
  12. }
  13.  
  14. //Retrieve all the post ids for the category you want to view
  15. var documentDbPostIds = redis.GetAllItemsFromSet("urn:Category:DocumentDB");
  16.  
  17. //Make a batch call to retrieve all the posts containing the matching ids
  18. //(i.e. the DocumentDB Category posts)
  19. var documentDbPosts = redisBlogPosts.GetByIds(documentDbPostIds);
  20.  
  21. documentDbPosts.PrintDump();
  22. }

这里首先把所有的文章按照标签新建Set,把相同的分类的文章放到一个Set中,最后根据key即可查找到相应的集合。

总结

本文利用一个简单的博客系统,简要介绍了如何利用Redis存储和获取复杂的数据。由于本文主要为了演示如何与Redis进行交互,所以实体设计的很简陋,没有按照DDD的思想进行设计,在某些设计方面没有遵循前文浅谈依赖注入中使用的原理和方法,后面会写文章对该系统进行重构以使之更加完善。

希望本文对您了解如何利用Redis存储复杂对象有所帮助。

参考资料

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

.NET中使用Redis (二)的更多相关文章

  1. 转:.NET中使用Redis (二)

    原文来自于:http://blog.jobbole.com/83824/ 原文出处: 寒江独钓   欢迎分享原创到伯乐头条 很久以前写了一篇文章 .NET中使用Redis 介绍了如何安装Redis服务 ...

  2. 分布式数据存储 之 Redis(二) —— spring中的缓存抽象

    分布式数据存储 之 Redis(二) -- spring中的缓存抽象 一.spring boot 中的 StringRedisTemplate 1.StringRedisTemplate Demo 第 ...

  3. (二)如何在.net中使用Redis

    Step1:使用NuGet工具安装Redis C# API,这里有多个API我们可以使用其中一个:

  4. .NET中使用Redis(二)

    很久以前写了一篇文章 .NET中使用Redis 介绍了如何安装Redis服务端,以及如何在.NET中调用Redis读取数据.本文简单介绍如何设计NoSQL数据库,以及如何使用Redis来存储对象. 和 ...

  5. Redis学习笔记之二 :在Java项目中使用Redis

    成功配置redis之后,便来学习使用redis.首先了解下redis的数据类型. Redis的数据类型 Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set( ...

  6. C#中使用Redis学习二 在.NET4.5中使用redis hash操作

    上一篇>> 摘要 上一篇讲述了安装redis客户端和服务器端,也大体地介绍了一下redis.本篇着重讲解.NET4.0 和 .NET4.5中如何使用redis和C# redis操作哈希表. ...

  7. 【转】C#中使用Redis学习二 在.NET4.5中使用redis hash操作

    摘要 上一篇讲述了安装redis客户端和服务器端,也大体地介绍了一下redis.本篇着重讲解.NET4.0 和 .NET4.5中如何使用redis和C# redis操作哈希表.并且会将封装的一些代码贴 ...

  8. ABP中使用Redis Cache(1)

    本文将讲解如何在ABP中使用Redis Cache以及使用过程中遇到的各种问题.下面就直接讲解使用步骤,Redis环境的搭建请直接网上搜索. 使用步骤: 一.ABP环境搭建 到http://www.a ...

  9. 在java中使用redis

    在java中使用redis很简单,只需要添加jedist.jar,通过它的api就可以了.而且,api和redis的语法几乎完全相同.以下简单的测试: 参考:http://www.runoob.com ...

随机推荐

  1. System.FormatException: GUID 应包含带 4 个短划线的 32 位数(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)。

    在NHibernate数据库查询中出现了这个错误,由于是数据库是mysql的,当定义的字段为char(36)的时候就会出现这个错误. [解决方法] 将char(36) 改成varchar(40)就行了 ...

  2. git 命令

    切换仓库地址: git remote set-url origin xxx.git切换分支:git checkout name撤销修改:git checkout -- file删除文件:git rm  ...

  3. DOM、BOM 操作超级集合

    本章内容: 定义 节点类型 节点关系 选择器 样式操作方法style 表格操作方法 表单操作方法 元素节点ELEMENT 属性节点attributes 文本节点TEXT 文档节点 Document 位 ...

  4. 在你的ASP.NET MVC中使用查找功能

    在程序中,使用查找功能是少之不了.今天在ASP.NET环境下演示一回. 在cshtml视图中,有三个文本框,让用户输入关键词,然后点击最右连的“搜索”铵钮,如果有结果将显示于下面. Html: 表格放 ...

  5. bash字符串操作

    参考 http://www.cnblogs.com/chengmo/archive/2010/10/02/1841355.html 问题:bash怎么提取字符串的最后一位?例如python中strin ...

  6. ViewController respondsToSelector 错误的解决方法

    原因解析:(来自别人博客分析)某个公共类或系统提供的控件,存在delegate方法,当创建此公共控件的容器类已经销毁,而这个控件对应的服务是在其它run loop中进行的,控件销毁或者需要进行状态通知 ...

  7. git

    CMD命令:git initgit add . [添加文件至暂存区]git commit -m '描述性语句 随意写即可'git branch gh-pages [创建仓库分支]git checkou ...

  8. MongoDB基础

    1.概念及特点 说明:由于部分语句中$ 符号无法正常显示,使用¥代表 概念 MongoDB是一个基于文档的分布式的开源的NoSQL数据库,文档的结构为BSON形式,每一个文档都有一个唯一的Object ...

  9. 周末聊聊IT人员的人脉观:关于帮妹子找兼职有感

    背景: 前几天,有个认识了好几年的网友,现在是大学生,在厦门读大一,说和她同学要一起到广州找兼职,看我有没有介绍. 像我这么积极热心善良的人,就说帮她找找看,结果问了几次,没消息,只好诚实的回复人家, ...

  10. ASP.Net MVC 5 in Xamarin Studio 5.2

    Xamarin Studio 是一个Mono的跨平台 IDE(Integrated Development Environment),支持Wiindow和Mac,最新发布的5.2 版本支持ASP.NE ...