使用 MongoDB 和 ASP.NET MVC 实现高性能搜索






4.92/5 (14投票s)
使用 MongoDB 和 ASP.NET MVC 实现高性能搜索
引言
在本篇博文中,我们将讨论一种解决方案,用基于 MongoDB 的搜索替换基于 SQL Server 的搜索。我们将以早期博文中介绍的 Footloose Financial Services ASP.NET MVC 应用程序为基础:https://codeproject.org.cn/Articles/770341/Embedded-Application-Identity-Part-Basic-Identit。
MongoDB 是面向对象的文档数据库的一种实现,它是一种NoSQL 数据库。NoSQL 是传统关系型数据库管理系统 (RDBMS) 的替代方案,它解决了关系型数据库的若干局限性,尽管通常是以牺牲 DBMS 级别的规范化或引用完整性为代价。文档数据库是一种 NoSQL 数据库,它将键与称为文档的复杂数据结构配对。一个文档可以包含一个或多个键值对。NoSQL 数据库的结构更简单,并且没有强制执行引用完整性的开销。它们比关系型数据库更具可伸缩性,并且在搜索性能方面表现更优。
在文章的其余部分,我们将引用以下 github 地址中的 Footloose Financial Services Visual Studio 2013 项目:https://github.com/pcarrasco23/FootlooseFinancialServices。
我们将使用标记为“Modifications for MongoDB”的提交中的代码。
MongoDB 安装和服务器架构
MongoDB 应安装在托管 ASP.NET 应用程序的 Web 服务器可以访问的网络中的服务器上。在我个人的安装中,我在运行于 VMWarePlayer 虚拟机中的 Ubuntu 14.0.4 LTS 64 位上安装了 MongoDB,该虚拟机与 Windows 域控制器和 IIS 服务器位于同一虚拟网络上。
MongoDB 在线手册(http://docs.mongodb.org/manual)提供了相当不错的关于如何在你的计算机上安装 MongoDB 的分步说明。
由于此 MongoDB 实例与 ASP.NET Web 服务器不在同一台服务器上,因此我们需要配置该实例以允许远程连接。默认情况下,MongoDB 服务器进程仅允许来自本地回环的连接。在 Linux 上,MongoDB 服务器配置文件是/etc/mongodb.conf。打开该文件并注释掉指定bind_ip
值的行。此值将按 IP 地址过滤 MongoDB 允许的远程连接。
#bind_ip = 127.0.0.1
#port = 27017
目前,通过注释掉bind_ip
值,我们将允许所有远程连接到 MongoDB 服务器。在后续的文章中,我将讨论如何为我们的 MongoDB 实例添加安全性,包括在bind_ip
值中指定 ASP.NET Web 服务器的 IP 地址。
数据架构
在 Visual Studio 项目中,我们将把客户端搜索页面的后端数据源从 SQL Server 更改为 MongoDB。但是,第一步是决定 MongoDB 数据库中文档的模式。我们希望显著提高搜索性能,以便即使是具有大型结果集的复杂搜索也能在亚秒级响应时间内从服务器返回数据。在 SQL Server 数据库中,客户端(个人)具有姓名和电子邮件地址等多个属性。此人可以有一个或多个电话号码、一个或多个地址和一个或多个账户。所有这些数据都存在于多个表中。对于个人搜索,我们只关心客户端的主要电话号码、地址和账户号码。因此,为了利用 MongoDB 的优势(即搜索扁平文档内的属性),我们将每个人的分层数据压缩到 MongoDB 文档中的单个记录中。代替关系数据库的分层模式,我们将有一个扁平的文档,如下所示。
MongoDB 数据库中的这个扁平化文档将为我们的客户端搜索页面提供改进的搜索性能。这将在 ETL 过程中完成,ETL 过程将在稍后详细讨论。
MongoDB C# 驱动程序和服务层
MongoDB C# 驱动程序可以使用 NuGet 在 Visual Studio 中下载。使用 C# 驱动程序,你可以连接到 MongoDb 实例,并使用 C# 和 LINQ 操作文档存储中的数据。
决定模式后的下一步是根据文档的模式创建物理模型。在我的例子中,我在PersonDocument.cs中创建了一个PersonDocument
,我将将其用作服务层代码和 MongoDB C# 驱动程序之间的DTO(数据传输对象)。驱动程序会将此对象序列化为 MongoDB 文档存储的 BSON 格式。
public class PersonDocument
{
public int PersonID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string EmailAddress { get; set; }
public string PhoneNumber { get; set; }
public string StreetAddress { get; set; }
public string City { get; set; }
public string County { get; set; }
public string State { get; set; }
public string Zip { get; set; }
}
然后,我为 MongoDB 数据库实现了工作单元模式,并分别在FootlooseFSDocUnitOfWork.cs和DocumentRepository.cs中为 MongoDB 文档实现了Repository<T>
接口,以便客户端代码可以使用熟悉的Repository
接口在 MongoDB 数据库中对Person
文档存储执行 CRUD 操作。
搜索服务方法在FootlooseFSService.cs的SearchPersonDocument
方法中使用IQueryable
接口来搜索Person
文档。
IQueryable<PersonDocument> personsQueryable = unitOfWork.Persons.GetQueryable();
ETL 过程使用DocumentRepository
的AddBatch
方法将记录添加到Person
文档存储中。
var docUnitOfWork = new FootlooseFSDocUnitOfWork();
docUnitOfWork.Persons.AddBatch(personDocuments);
回到 MongoDB 数据库的工作单元类,有两个应用程序配置文件键对于任何使用 MongoDB 工作单元和DocumentRepository
的客户端(ASP.NET Web 和 ETL 进程)都是必需的。对于 ASP.NET Web 应用程序,它们会在web.config中;对于独立的 ETL 程序,它们会在App.config中。MongoDBConnectionString
键包含 MongoDB 连接字符串,它本质上是网络上 MongoDB 主计算机的 IP 地址或 DNS。MongoDBDatabaseName
是包含一个或多个文档存储的 MongoDB 数据库的名称。
<add key="MongoDBConectionString" value="mongodb://192.168.1.6" />
<add key="MongoDBDatabaseName" value="footloosefs"/>
MongoDB 的工作单元类包含一个private MongoDatabase
变量,该变量包含到数据库的连接以及连接初始化代码,我们在其中利用配置值。DocRepository<PersonDocument>
对象包含对 person 文档存储执行 CRUD 操作的代码。在Init
方法中,我们指定了PersonDocument
中我们希望序列化到 person MongoDB 文档的字段。默认情况下,MongoDB 会在每个文档中创建一个对象 ID 字段作为主键,除非我们另有说明。MapIdProperty
字段允许我们将 DTO 中的一个字段指定为主键,就像我使用PersonID
字段所做的那样。
public class FootlooseFSDocUnitOfWork
{
private MongoDatabase _database;
protected DocRepository<PersonDocument> _persons;
public FootlooseFSDocUnitOfWork()
{
var connectionString = ConfigurationManager.AppSettings["MongoDBConectionString"];
var client = new MongoClient(connectionString);
var server = client.GetServer();
var databaseName = ConfigurationManager.AppSettings["MongoDBDatabaseName"];
_database = server.GetDatabase(databaseName);
}
public IRepository<PersonDocument> Persons
{
get
{
if (_persons == null)
_persons = new DocRepository<PersonDocument>(_database, "persons");
return _persons;
}
}
public static void Init()
{
BsonClassMap.RegisterClassMap<PersonDocument>(cm =>
{
cm.MapIdProperty(p => p.PersonID);
cm.MapProperty(p => p.FirstName);
cm.MapProperty(p => p.LastName);
cm.MapProperty(p => p.EmailAddress);
cm.MapProperty(p => p.PhoneNumber);
cm.MapProperty(p => p.StreetAddress);
cm.MapProperty(p => p.City);
cm.MapProperty(p => p.County);
cm.MapProperty(p => p.State);
cm.MapProperty(p => p.Zip);
});
}
}
ETL 过程
既然我们已经决定了文档的布局并将 MongoDB 接口添加到服务层,我们需要用 SQL Server 中的相应数据填充 MongoDB person 文档集合。ETL 代表提取-转换-加载。FootlooseFSDocDBETL
项目包含该过程的代码,该过程将从Person
和关联表(PersonAccount
、PersonAddress
、Phone
、Address
、Account
、Login
)中提取每个人的必要数据(由dbo.Person
中的记录表示),并将数据转换为单个对象,然后将该对象加载到 MongoDB 数据源的Person
文档存储中。
后续步骤
运行FootlooseFSDocDBETL
项目中的程序后,我们将有一个完全加载的Person
文档,我们的 ASP.NET 应用程序可以在其上运行查询。如果你已做到这一步,你将注意到 MongoDB 查询的性能明显优于 SQL Server 查询。对于包含 100 万条记录的Person
文档存储,搜索、分页和排序的性能都在一秒钟之内。现在想象一下 100 或可能成千上万的用户同时进行搜索!MongoDB 可以轻松地横向扩展以满足用户需求。
下一步——这不属于本文的范围——将是创建一个进程,在应用程序通过 SQL 更新Person
或关联记录时,更新 MongoDB 中的Person
文档存储。这很可能涉及将更新写入队列(如 MSMQ)的消息,然后由轮询队列的 Windows 服务拾取并更新Person
文档存储。