快速查找和持久化的最佳数据结构存储方式

10

场景

我有以下这些方法:

public void AddItemSecurity(int itemId, int[] userIds)
public int[] GetValidItemIds(int userId)

最初我考虑的是形式上的存储:

itemId -> userId, userId, userId

并且

userId -> itemId, itemId, itemId
AddItemSecurity 是基于我从第三方 API 获取数据的方式,GetValidItemIds 是我希望在运行时使用的方式。
潜在有 2000 个用户和 1000 万个项目。项目 ID 的格式为:2007123456、2010001234(10 位数字,前四位表示年份)。 AddItemSecurity 不需要执行得特别快,但 GetValidIds 需要在子秒级别完成。此外,如果对现有的 itemId 进行更新,则需要将该 itemId 从不再存在于列表中的用户中移除。
我正在考虑如何以最优方式存储此数据。最好是在磁盘上(带缓存),但我希望代码易于维护和清晰。
如果项目 ID 从 0 开始,我考虑为每个用户创建一个字节数组,长度为 MaxItemId / 8,并设置一个 true/false 位来指示是否存在该项目。这将限制每个用户的数组长度略大于 1MB,并且提供快速查找以及一种更新用户���表的简单方法。通过将其持久化为 .Net 4 框架下的 内存映射文件,我认为我还能获得良好的缓存(如果机器有足够的 RAM),而无需自己实现缓存逻辑。解析 ID、剥离年份并存储按年分类的数组可能是一种解决方案。
ItemId -> UserId[] 列表可以直接序列化到磁盘,并使用普通的 FileStream 来读写以保持列表并在更改时进行差异比较。
每次添加新用户时,所有列表都必须更新,但可以在每晚完成此操作。
问题:我应该继续尝试这种方法,还是还有其他需要探索的方法?我认为 SQL Server 不会执行得足够快,并且它会产生一些开销(至少如果它托管在不同的服务器上),但我的假设可能是错误的。对此问题的任何想法或见解都将不胜感激。并且我希望尽量少地添加硬件 :)
[更新 2010-03-31] 我现在已在以下条件下测试了 SQL Server 2008。
  • 两列表(userid,itemid),均为整数
  • 这两列拥有聚集索引
  • 为180个用户添加了约800,000个项目 - 总共有1.44亿行
  • 已为SQL服务器分配了4GB的RAM
  • 双核2.66GHz笔记本电脑
  • SSD硬盘
  • 使用SqlDataReader将所有itemid读入列表
  • 循环遍历所有用户

如果我运行一个线程,平均时间为0.2秒。当我添加第二个线程时,它会上升到0.4秒,这仍然可以接受。从那时起结果是下降的。添加第三个线程会使很多查询时间达到2秒。第四个线程,则是4秒,第五个则会将一些查询峰值提高至50秒。

即使只有一个线程,CPU也会在进行此操作时繁忙。我的测试应用程序需要一些时间来处理快速循环,而sql则需要其余时间。

这导致我得出结论,它在性能方面不会很好地扩展。至少在我测试的硬件上是如此。是否有优化数据库的方法,例如为每个用户存储整数数组,而不是每个项目一条记录。但这样做会使删除项目更加困难。

[更新2010-03-31 #2]

我使用相同的数据进行了快速测试,将其作为内存映射文件中的位。它执行得更好。六个线程之间的访问时间介于0.02秒和0.06秒之间。纯粹受内存限制。映射文件由一个进程映射,同时由六个其他进程访问。由于sql基础占用了4GB,因此磁盘上的文件占用了23mb。

3个回答

5
经过多次测试,我最终使用了带有稀疏位的内存映射文件(NTFS),使用了来自在C#中处理NTFS稀疏文件的代码。
Wikipedia对稀疏文件进行了解释。
使用稀疏文件的好处是,我不必关心我的id范围。如果我只写入2006000000和2010999999之间的id,文件将仅从文件中的偏移量250,750,000分配625,000个字节。该偏移量之前的所有空间在文件系统中未分配。每个id都存储为文件中的一组位。有点像一个位数组。如果id序列突然改变,则会在文件的另一个部分进行分配。
为了检索哪些id已设置,我可以执行一个操作系统调用以获取稀疏文件的已分配部分,然后检查这些序列中的每个位。还要检查特定id是否已设置非常快速。如果它超出了已分配块的范围,则不存在;如果它在范围内,则只需读取一个字节并进行位掩码检查以查看是否设置了正确的位。
因此,对于您想要尽可能快地检查许多id的特定场景,这是我迄今为止找到的最优方法。
而且好处是,内存映射文件也可以与Java共享(这是必要的)。Java在Windows上也支持内存映射文件,并且实现读/写逻辑相当简单。

我知道你正在使用C#,而我不知道内存映射文件在那里是如何实现的,但你可能想看看这个Java链接:http://download.oracle.com/javase/6/docs/api/java/nio/channels/FileChannel.html#map(java.nio.channels.FileChannel.MapMode, long, long) - user183037
对生成的缓冲区所做的更改最终会传播到文件中;它们可能会或可能不会显示给已映射同一文件的其他程序。如果您正在使用多个线程,则需要谨慎处理此部分。 - user183037
1
我在多线程或多进程访问同一文件时没有遇到任何问题。如果我没记错的话,如果访问相同的数据,则两个线程/进程将访问OS中的同一内存页,并且OS将负责缓存/分页/排队请求。话虽如此,我不是专家,在我的场景中,我有一个写入者和多个读取者,一次未命中不是什么大问题。如果您需要对事件序列百分之百确定,那么您可能不想使用mmf。但我非常信任这个,因为mmf是在应用程序之间共享数据的推荐方式之一。 - Mikael Svenson

1

在你做出决定之前,我真的认为你应该尝试一下一个好的数据库。像这样的东西在长期维护上会是一个挑战。你的用户群实际上相当小。SQL Server应该能够轻松处理你所需的一切。


我现在正在创建一个简单的数据库,以填充值进行测试。 - Mikael Svenson
我已经完成了我的SQL测试,您有什么建议可以帮助我提高吗? - Mikael Svenson
你正在使用Sql Server 2008 Express吗?这肯定可以解释为什么增加线程后性能会下降。(虽然Express完全有能力,但由于是免费版本,因此受到限制,性能要差得多。它还有一个数据库大小的上限,我相信是4GB。) - Paul Sasik
我正在使用SQL 2008开发版。我需要我的管理工具 :) - Mikael Svenson

0

2000个用户还好,但如果有1000万个相关项目,您真的应该考虑将其放入数据库中。数据库可以处理所有存储、持久化、索引、缓存等需求,并且它们的性能非常出色。

此外,它们还允许更好地扩展到未来。如果您突然需要处理200万个用户和数十亿个设置,拥有一个良好的数据库将使扩展成为不成问题的事情。


更新了问题,附带了一些SQL数字。 - Mikael Svenson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接