想想像 SQL Server 这样的传统数据库实际发生了什么。
- 当从表中创建、更新或删除项目时,与表关联的任何索引也必须更新。
- 表上的索引越多,写入操作的速度就越慢。
- 如果您在现有表上创建新索引,则在完全构建之前根本不会使用它。如果没有其他索引可以回答查询,则会发生慢表扫描。
- 如果其他人在修改现有索引时尝试从现有索引进行查询,则读取器将阻塞直到修改完成,因为对持久性的要求
C
高于A
可用性的优先级。
- 这通常会导致读取缓慢、超时和死锁。
“最终一致性”的 NoSQL 概念旨在缓解这些担忧。它通过优先考虑A
可用性而不是持续性来优化读取C
。RavenDB 在这方面并不是独一无二的,但它有些特殊之处在于它仍然具有保持一致的能力。如果您正在检索单个文档,例如查看订单或查看其个人资料的最终用户,这些操作符合 ACID,并且不受“最终一致性”设计的影响。
要了解“最终一致性”,请考虑查看您网站上产品列表的典型用户。与此同时,贵公司的销售人员正在修改目录、添加新产品、更改价格等。有人可能会争辩说,与这些变化完全一致的清单可能并不重要。毕竟,几秒钟前访问该站点的用户无论如何都会收到没有更改的数据。最重要的是快速交付产品结果。由于正在进行写入而阻止查询将意味着对客户的响应时间较慢,因此您的网站体验较差,并且可能会失去销售。
因此,在 RavenDB 中:
- 针对文档存储进行写入。
- 单个
Load
操作直接进入文档存储。
- 针对索引存储发生查询
- 在写入文档时,对于那些已经定义的索引,数据正在从文档存储复制到索引存储。
- 无论何时查询索引,您都会得到该索引中已经存在的任何内容,而不管后台正在进行的复制状态如何。这就是为什么有时索引是“陈旧的”。
- 如果您在未指定索引的情况下进行查询,并且 Raven 需要一个新索引来回答您的查询,它将开始动态构建索引并立即返回其中一些结果。它只会阻塞足够长的时间来为您提供一页结果。然后它会继续在后台构建索引,以便下次查询时您将获得更多可用数据。
所以现在让我们举一个例子来说明这种方法的缺点。
- 销售人员转到按字母顺序排序的“产品列表”页面。
- 在第一页上,他们看到“Apples”目前没有出售。
- 所以他们点击“添加产品”,然后进入一个新页面,在那里他们输入“Apples”。
- 然后他们返回到“产品列表”页面,他们仍然看不到任何苹果,因为索引是陈旧的。WTF - 对吧?
解决此问题需要了解并非所有数据查看者都应被视为平等。那个特定的销售人员可能会要求查看新添加的产品,但客户不会以同样的紧迫程度了解或关心它。
因此,在销售人员正在查看的“产品列表”页面上,您可能会执行以下操作:
var results = session.Query<Product>()
.Customize(x => x.WaitForNonStaleResultsAsOfLastWrite())
.OrderBy(x=> x.Name)
.Skip((pageNumber-1) * pageSize).Take(pageSize);
在客户查看目录时,您不希望添加该定制行。
如果你想获得超级精确,你可以使用稍微优化的策略:
- 当从“添加产品”页面返回到“列出产品”页面时,传递刚刚添加的 ProductID。
就在您在该页面上查询之前,如果传入了 ProductID,则将您的查询代码更改为:
var product = session.Load(productId);
var etag = session.Advanced.GetEtagFor(product);
var results = session.Query<Product>()
.Customize(x => x.WaitForNonStaleResultsAsOf(etag))
.OrderBy(x=> x.Name)
.Skip((pageNumber-1) * pageSize).Take(pageSize);
这将确保您只等待绝对必要的时间,以便将结果列表中包含的一个产品的更改与索引中的其他结果一起包含在结果列表中。
您可以通过传回 etag 而不是 ProductId 来稍微优化这一点,但这可能无法从应用程序的其他地方重用。
但请记住,如果列表按字母顺序排序,并且我们添加了“Plums”而不是“Apples”,那么您可能不会立即看到这些结果。当用户跳到包含该产品的页面时,它可能已经在那里了。