40

我的团队决定通过 ServiceStack.net Redis Client 使用 Redis 作为我们正在开发的新的大容量网站的底层存储库。我不确定在哪里可以找到有关此问题的文档(一般 Redis 文档或特定 ServiceStack.Net 文档或两者兼有) - 实际上是否有关于如何通过 ServiceStack.Net 实现 Redis 的文档的明确来源,其中包括您需要了解 Redis 概念和 ServiceStack.Net 概念的所有内容,还是我们需要分别整合这两个方面的文档才能全面了解?

我只是在努力解决如何在我们模型的对象图中准确存储相关对象。这是我想要使用的一个简单场景:

系统中有两个对象:UserFeed。在 RDBMS 术语中,这两个对象具有一对多的关系,即 aUser有一个Feed对象集合,而一个 feed 只能属于一个User。提要总是通过其用户从 Redis 访问,但有时我们希望通过提要实例访问用户。

所以我的问题是我们应该将相关对象存储为属性还是应该存储Id相关对象的值?为了显示:

方法一

public class User
{
    public User()
    {
        Feeds = new List<Feed>();
    }

    public int Id { get; set; }

    public List<Feed> Feeds { get; set; }

    // Other properties
}

public class Feed
{
    public long Id { get; set; }

    public User User { get; set; }
}

方法 B

public class User
{
    public User()
    {
        FeedIds = new List<long>();
    }

    public long Id { get; set; }

    public List<long> FeedIds { get; set; } 

    public List<Feed> GetFeeds()
    {
        return repository.GetFeeds( FeedIds );
    }
}

public class Feed
{
    public long Id { get; set; }

    public long UserId { get; set; }

    public User GetUser()
    {
        return repository.GetUser( UserId );
    }
}

以上哪种方法效果最好?我已经在各种示例中看到了这两种方法,但我的印象是我看到的一些示例可能不是最佳实践。

几个简单的相关问题:

  • 如果我对对象进行更改,它会自动反映在 Redis 中还是需要保存?我假设是后者,但需要绝对清楚。
  • 如果我(可以)使用方法 A,对用户对象 X 的更新是否会反映在整个对象图中被引用的任何地方,或者是否有必要在整个图上保存更改?
  • 通过其接口存储对象是否存在问题(即使用IList<Feed>而不是List<Feed>?

抱歉,如果这些问题有点基本 - 直到两周前,我什至从未听说过 Redis - 更不用说 ServiceStack - (我的团队中也没有任何人)所以我们真的从头开始......

4

1 回答 1

68

我不会重新散列很多其他的文档,而是列出一些关于 Redis + ServiceStack 的 Redis 客户端的背景信息:

没有魔法 - Redis 是一块空白的画布

首先我要指出的是,使用 Redis 作为数据存储只是提供了一个空白画布,本身并没有任何相关实体的概念。即它只是提供对分布式comp-sci 数据结构的访问。通过使用 Redis 的原始数据结构操作,如何存储关系最终取决于客户端驱动程序(即 ServiceStack C# Redis 客户端)或应用程序开发人员。由于所有主要的数据结构都在 Redis 中实现,因此您基本上可以完全自由地决定如何构建和存储数据。

想想你将如何在代码中构建关系

因此,考虑如何在 Redis 中存储内容的最佳方法是完全忽略数据如何存储在 RDBMS 表中,而考虑数据如何存储在代码中,即使用内存中的内置 C# 集合类 - Redis 在行为上反映了它们的服务器端数据结构。

尽管没有相关实体的概念,但 Redis 内置的SetSortedSet数据结构提供了存储索引的理想方式。例如,Redis 的Set集合只存储最多 1 次出现的元素。这意味着您可以安全地向其中添加项目/键/ID,而不关心该项目是否已经存在,因为如果您调用它 1 次或 100 次最终结果将是相同的 - 即它是幂等的,最终只有 1 个元素仍然存储在集。因此,一个常见的用例是在存储对象图(聚合根)时,每次保存模型时都将子实体 ID(又名外键)存储到 Set 中。

可视化您的数据

为了更好地可视化实体在 Redis 中的存储方式,我建议安装Redis Admin UI,它与 ServiceStack 的 C# Redis 客户端配合得很好,因为它使用下面的键命名约定来提供一个很好的分层视图,将你的类型化实体分组在一起(尽管所有键存在于同一个全局键空间中)。

要查看和编辑实体,请单击编辑链接以查看和修改所选实体的内部 JSON 表示。希望一旦您了解模型的存储方式,您将能够就如何设计模型做出更好的决定。

POCO / 实体的存储方式

C# Redis 客户端适用于任何具有单个主键的 POCO - 默认情况下应该是Id(尽管这个约定可以用ModelConfig覆盖)。本质上,POCO 以序列化 JSON 的形式存储到 Redis 中,其中typeof(Poco).NameId用于形成该实例的唯一键。例如:

urn:Poco:{Id} => '{"Id":1,"Foo":"Bar"}'

C# 客户端中的 POCO 通常使用 ServiceStack 的快速Json 序列化器进行序列化,其中只有具有公共 getter 的属性被序列化(以及公共 setter 以反序列化返回)。

默认值可以用[DataMember]attrs 覆盖,但不推荐,因为它会丑化你的 POCO。

实体是斑点

因此,知道 Redis 中的 POCO 只是 blobbed,您只想将 POCO 上的非聚合根数据保留为公共属性(除非您有意存储冗余数据)。一个好的约定是使用方法来获取相关数据(因为它不会被序列化),但也告诉您的应用程序哪些方法进行远程调用以读取数据。

所以关于提要是否应该与用户一起存储的问题是它是否是非聚合根数据,即您是否想在用户上下文之外访问用户提要?List<Feed> Feeds如果不是,则将属性保留在User类型上。

维护自定义索引

但是,如果您希望所有提要保持独立可访问,即,redisFeeds.GetById(1)您将希望将其存储在用户之外并维护链接 2 个实体的索引。

正如您所注意到的,有很多方法可以存储实体之间的关系,而如何存储在很大程度上取决于您的偏好。对于父>子关系中的子实体,您总是希望将ParentId与子实体一起存储。对于 Parent,您可以选择将ChildId的集合与模型一起存储,然后对所有子实体进行一次提取以重新水合模型。

另一种方法是为每个父实例在其自己的Set中维护父 dto 之外的索引。Redis StackOverflow 演示的C# 源代码中有一些很好的例子,其中和的关系存储在:Users > QuestionsUsers > Answers

idx:user>q:{UserId} => [{QuestionId1},{QuestionId2},etc]
idx:user>a:{UserId} => [{AnswerId1},{AnswerId2},etc]

尽管 C# RedisClient 确实通过其TParent.StoreRelatedEntities()包括对默认父/子约定的支持,TParent.GetRelatedEntities<TChild>()以及TParent.DeleteRelatedEntities()在后台维护索引的 API,如下所示:

ref:Question/Answer:{QuestionId} => [{answerIds},..]

实际上,这些只是您可能的一些选择,其中有许多不同的方法可以达到相同的目的,并且您也可以自由选择自己的方法。

NoSQL 的无模式、松散类型的自由应该被接受,并且您不应该担心尝试遵循您在使用 RDBMS 时可能熟悉的僵化、预定义的结构。

总之,在 Redis 中存储数据没有真正正确的方法,例如,C# Redis 客户端做了一些假设,以便围绕 POCO 提供高级 API,并将 POCO 放在 Redis 的二进制安全字符串值中 - 尽管还有其他客户端会更喜欢将实体属性存储在 Redis 哈希(字典)中。两者都会起作用。

于 2012-01-19T01:24:19.007 回答