【问题标题】:How to store a count of Queried RavenDB documents如何存储查询的 RavenDB 文档的计数
【发布时间】:2012-10-25 01:15:31
【问题描述】:

我在 RavenDB 中有一个文档集合,客户端应用程序围绕它执行相对直接的 CRUD。有一个简单的仅地图索引可用于查询集合。

我现在必须向应用程序添加一个功能,该功能将显示单个文档在查询中被检索到的次数,即应用程序“服务”的次数。这样,当您通过应用程序查看单个文档时,您可以看到它已被提供的次数。还需要一个“重置”按钮以将计数设置回零,然后允许计数继续增加。

为了降低应用程序接口的复杂性,故意保持要求很粗略。日期范围报告的选项已经讨论过,被认为是不必要的。

某些情况:某些文档的增量服务计数可能每天超过 10,000。预计每周会发生一次文档服务计数的“重置”。我应该能够在查询操作之外以 CQRS 方式异步实现对数据库的递增写入。

我可以想到三种方法,并想知道最好的选择是什么?

  1. 将整数 Count 属性添加到文档对象,以便在随后调用递增对象时在每个对象上递增。这将使文档大小保持较小。

  2. 将 List 属性添加到当前日期的文档对象,以类似于选项 1 的方式附加到每个对象上。如果/当请求基于日期的报告但文档可以获得大到危险吗?

  3. 添加一个单独的“计数器”文档集合,其中包含提供的文档 ID 和提供它的日期时间。我认为这会从 map/reduce 索引中获益?

还有几个早期的后续问题......

  • 有没有一种面向方面的方法可以通过 RavenDB 实现这一目标?
  • 与插入(选项 3)相比,使用更新(选项 1 + 2)或反之亦然是否有任何性能优势?

感谢您的回答和阅读所有这些内容!

【问题讨论】:

    标签: database-design indexing mapreduce ravendb cqrs


    【解决方案1】:

    使用 RavenDB 实现此目的的面向方面的方法是使用 read trigger。您可以将检索计数存储在文档元数据中。这将是最高效的解决方案,因为不需要有关单个检索的信息。

    更新

    正如 ma​​xbeaudoin 所指出的,元数据是读取计数器的合适位置,因为它是关于数据的数据。此外,由于元数据与文档一起存储,因此它会表现良好。 This 描述了如何在 RavenDB 中使用元数据。

    更新 2

    如果您只存储计数,那么元数据是性能和实用性的最佳选择。如果您需要存储每个视图事件并且您预计每个文档会有数千个潜在的视图事件,那么我会将视图事件存储在单独的文档中,而不是存储在同一个文档或文档元数据中。元数据只是键值对的集合,不适用于大型集合。不存储在同一个文档中的原因是您必须更改文档模型以包含视图事件,这会污染您的模型,而且正如您所指出的,检索具有 10K 视图事件的文档将是 IO起伏并导致性能问题。您可以使用投影仅检索特定的文档字段,但不会跟踪返回的文档的更改。考虑到麻烦,我建议您重新考虑不要存储单个读取事件,除非您绝对需要它们。

    【讨论】:

    • 除了提到可能适合也可能不适合的文档元数据之外,我并没有完全了解我应该如何/在哪里存储数据。但是感谢您提醒我阅读触发器。
    • 元数据的常见定义是:“关于数据的数据”。完美契合。读取触发器是此类逻辑的正确位置。
    • 虽然我同意元数据非常适合,因为查看计数确实可以告诉您有关文档的一些信息,但我更关心的是实用性和性能。我倾向于将每个 View 事件存储为属于文档(或文档元数据)的数组中的一个元素。现在,如果每个文档包含超过 10K 的视图事件,这将如何影响单个文档的加载时间?有了这么大的数字,我是否达到了一个阈值,可以更有效地将 View 事件存储在它自己的集合中?
    • 重要的一点是视图计数不需要在每次文档被访问时返回,而只有在使用某个索引来查询集合时才需要返回。因此,这个效率问题是否可以通过分离索引来解决?即,不映射 View Count 字段的索引不会受到字段计数/向量长度的影响?
    • 元数据作为文档的一部分返回,因此调用session.Advanced.GetMetadataFor 会访问已经是会话对象一部分的元数据,并且不会产生额外的调用。因此,您可以返回文档集合并检索每个文档的元数据,而不会降低性能。
    【解决方案2】:

    也许我在这里有点离谱,但你为什么要每天从数据库中实际加载这个文档 10k 次呢?使用一些输出缓存可能会更好。

    Raven 缓存也会对您有利。我必须承认,如果客户端发生缓存命中,我不确定读取触发器是否仍会在服务器上触发。如果您确实走触发器路径,我会先验证这一点。

    也许在客户端保持一个计数器会更好。即使您获得缓存命中并且不触摸 raven,您也可以增加计数器。然后,您可以定期将计数器刷新回服务器,以更新文档本身或单独的统计文档中的计数属性。

    这确实有助于提高性能。假设您在 5 分钟内有 50 次观看。为什么每次增加 1,每 5 分钟增加 50。嗯,不完全是 50,而是你在那段时间在前端计量的任何东西。即使使用多台服务器,这也可以扩展,如果您只是将新计数添加到现有服务器,则可以通过 raven 的修补 API 应用更改。

    更新

    我整理了一个可能对您有所帮助的示例。除了定期出现的一些计时器外,这具有您在客户端执行此操作所需的一切。希望这对得起你的赏金。

    public class Counter
    {
        // Uses the Multithreaded Singleton pattern
        // See http://msdn.microsoft.com/en-us/library/ff650316.aspx
    
        private Counter() { }
    
        private static volatile Counter _instance;
        private static readonly object SyncRoot = new object();
    
        public static Counter Instance
        {
            get
            {
                if (_instance != null)
                    return _instance;
    
                lock (SyncRoot)
                {
                    if (_instance == null)
                        _instance = new Counter();
                }
                return _instance;
            }
        }
    
        private readonly ConcurrentDictionary<string, long> _readCounts =
                                             new ConcurrentDictionary<string, long>();
    
        public void Increment(string documentId)
        {
            _readCounts.AddOrUpdate(documentId, k => 1, (k, v) => v + 1);
        }
    
        public long ReadAndReset(string documentId)
        {
            lock (SyncRoot)
            {
                long count;
                return _readCounts.TryRemove(documentId, out count) ? count : 0;
            }
        }
    
        public IDictionary<string, long> ReadAndResetAll()
        {
            var docs = _readCounts.Keys.ToList();
            return docs.ToDictionary(x => x, ReadAndReset);
        }
    }
    
    public class Story
    {
        public string Id { get; set; }
        public string Title { get; set; }
        public string Author { get; set; }
        public DateTime Published { get; set; }
        public long ReadCount { get; set; }
        public string Content { get; set; }
    }
    
    [TestClass]
    public class Tests
    {
        [TestMethod]
        public void TestCounter()
        {
            var documentStore = new DocumentStore { Url = "http://localhost:8080" };
            documentStore.Initialize();
    
            documentStore.DatabaseCommands.EnsureDatabaseExists("Test");
    
            using (var session = documentStore.OpenSession("Test"))
            {
                var story = new Story
                    {
                        Id = "stories/1",
                        Title = "A long walk home",
                        Author = "Miss de Bus",
                        Published = new DateTime(2012, 1, 1),
                        Content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
                    };
                session.Store(story);
                session.SaveChanges();
            }
    
            // This simulates many clients reading the document in separate sessions simultaneously
            Parallel.For(0, 1000, i =>
                {
                    using (var session = documentStore.OpenSession("Test"))
                    {
                        var story = session.Load<Story>("stories/1");
                        Counter.Instance.Increment(story.Id);
                    }
                });
    
            // This is what you will need to do periodically on a timer event
            var counts = Counter.Instance.ReadAndResetAll();
            var db = documentStore.DatabaseCommands.ForDatabase("Test");
            foreach (var count in counts)
                db.Patch(count.Key, new[]
                    {
                        new PatchRequest
                            {
                                Type = PatchCommandType.Inc,
                                Name = "ReadCount",
                                Value = count.Value
                            }
                    });
    
            using (var session = documentStore.OpenSession("Test"))
            {
                var story = session.Load<Story>("stories/1");
                Assert.AreEqual(1000, story.ReadCount);
            }
        }
    }
    

    【讨论】:

    • 您对缓存的看法非常正确。我也远离读取触发器,因为我将异步增加计数,因为更新读取计数的需要取决于进一步的条件。然而,问题仍然存在,是存储视图事件的集合(在文档上还是在它自己的集合中)还是仅仅存储一个简单的计数更好。尽管如此,将计数存储在内存中并使用修补 api 还是不错的。
    • 我的另一个想法,假设您的请求来自网络,您可能希望保留一个 ConcurrentDictionary,其中键是文档 ID,值是自上次更新以来的计数。然后,您可以定期获取值,将它们重置为零并将值保存回数据库。将它放在文档中还是单独的视图事件中,更多的是关于您是否希望用户获取文档来查看计数。
    • 我用一个示例更新了我的答案,以向您展示如何有效地完成此操作。只需在您的应用程序中的某个位置添加一个计时器,该计时器会按照我在示例中显示的方式调用 ReadAndResetAll。
    • 我上次更新它是为了在更新读取计数时使用 Raven 的修补 API。这是处理此类更新的最有效方式。
    • 非常感谢您的详细回答。我喜欢修补方法以及异步计数器增量。我已经研究过使用 Phil Haack 的 WebBackgrounder 来实现它。恭喜赏金! :-)
    猜你喜欢
    • 2020-06-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-25
    相关资源
    最近更新 更多