2

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

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

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

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

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

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

  2. 将 List 属性添加到当前日期的文档对象,以类似于选项 1 的方式附加到每个对象上。如果/当请求基于日期的报告但文档可能变得非常大时,这将很有用?

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

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

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

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

4

2 回答 2

3

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

更新

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

更新 2

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

于 2012-10-16T23:45:03.397 回答
3

也许我在这里有点离谱,但你为什么要每天从数据库中实际加载这个文档 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);
        }
    }
}
于 2012-10-19T00:42:58.103 回答