数据库操作的经济效益

MongoDB

面对不断上升的成本以及不明朗的经济形势,许多组织都在探索各种方式,以期取得事半功倍的效果。数据库也不例外。所幸,转为使用文档数据库并实施恰当的数据建模技术,就有机会提高效率、节省资金。 文档数据库从两个方面为公司节省资金:

  1. 以对象为中心的跨语言 SDK 和架构灵活性,让开发人员能够加速创建和迭代生产代码,从而降低开发成本。

  2. 减少实现规定事务吞吐量所必备的硬件,能够大幅降低运营成本。

开发人员效率

所有现代开发都用到了对象概念。对象定义了一系列相关值,以及如何读取、修改值及从值中推导结果的各种方法。顾客、发票和火车时间表都体现出对象概念。与所有程序变量一样,对象也是临时的。因此,必须将它们保留到磁盘存储,将其变为持久化对象。

我们不再采用 Windows 桌面开发人员在 20 世纪 90 年代的的做法,不会再手动将对象序列化成本机文件。目前,数据并不会存储在执行应用程序的电脑上,而是存储在由多个应用程序或多个应用程序实例可以访问的一个集中位置。共用访问位置之后,我们既需要通过网络高效地读写数据;也需要实施机制来确保对该数据的并发更改不会导致一个流程覆盖另一个流程的更改。

关系数据库推出的时间早于已经广泛使用和实施的面向对象的编程。在关系数据库中,数据结构是内含值的多张数学表。与数据的交互需要通过专门语言 SQL,而该语言经过过去 40 年的演变,能够与存储的数据进行所有类型的交互:筛选及重新调整自身、将自身从已去重的相关扁平化模型转变为向应用程序呈现的表格化、重叠、联接结果。然后再大费周章地将数据从这么多行的冗余值再转换为程序需要的对象。

这个环节需要开发人员投入大量工时、技能和专业知识。开发人员必须理清表格之间的关系。他们还需要了解如何检索不同的信息集合,再利用这些数据行来重建数据对象。人们假定了开发人员在入行前就已经学了相关技能,只需要在工作中调用技能就行。这个假定毫无根据。即便开发人员接受过正规的 SQL培训,他们也不大可能懂得如何高效写入有用示例。 保留对象的概念催生了文档数据库。有了文档数据库后,只需用非常少的代码或转换就能将强类型对象保留到数据库;只需用范例对象来筛选、重新调整及聚合结果,不必费劲使用名为 SQL 的蹩脚英文来表达。

假设我们想要为拥有一系列重复属性的客户存储客户对象,例如,存储地址。此时,地址是不会在客户之间分享的弱实体。如果使用了 C# 中的代码/类似 Java 的伪代码:

class Address : Object {
  
   Integer number;
   String street, town, type;
 
   Address(number, street, town, type) {
       this.number = number
       this.street = street
       this.town = town,
       this.type = type
   }
 
   //Getters and setters or properties as required
}
class Customer :  Object {
   GUID customerId;
   String name, email
   Array < Address > addresses;
 
   Customer(id, name, email) {
       this.name = name;
       this.email = email;
       this.customerId = id
       this.addresses = new Array < Address > ()
   }
   //Getters and setters or properties as required
}
  
Customer newCustomer = new Customer(new GUID(),
   "Sally Smith", "sallyport@piratesrule.com")
  
Address home = new Address(62, 'Swallows Lane', 'Freeport', 'home')
newCustomer.addresses.push(home)

要将这个客户对象存储在关系数据库管理系统 (RDBMS) 后再在规定位置检索所有客户,我们需要下列代码或类似内容:

//Connect
RDBMSClient rdbms = new RDBMSClient(CONNECTION_STRING)
rdbms.setAutoCommit(false);
 
 
// Add a customer
 
insertAddressSQL = "INSERT INTO Address (number,street,town,type,customerId) values(?,?,?,?,?)"
preparedSQL = rdbms.prepareStatement(insertAddressSQL)
for (Address address of newCustomer.addresses) {
   preparedSQL.setInt(1, address.number)
   preparedSQL.setString(2, address.street)
   preparedSQL.setString(3, address.town)
   preparedSQL.setString(4, address.type)
   preparedSQL.setObject(5, customer.customerId)
   preparedStatement.executeUpdate()
}
 
insertCustomerSQL = "INSERT INTO Customer (name,email,customerId) values(?,?,?)"
preparedSQL = rdbms.prepareStatement(insertCustomerSQL)
preparedSQL.setString(1, customer.name)
preparedSQL.setString(2, customer.email)
preparedSQL.setObject(3, customer.customerId)
preparedStatement.executeUpdate()
rdbms.commit()
 
 
//Find all the customers with an address in freeport
 
 
freeportQuery = "SELECT ct.*, ads.* FROM address ad
INNER JOIN address ads ON ad.customerId=ads.customerId AND ad.town=?
INNER JOIN customer ct ON ct.customerId = ad.customerId"
 
preparedSQL = rdbms.prepareStatement(freeportQuery)
preparedSQL.setString(1, 'Freeport')
ResultSet rs = preparedSQL.executeQuery()
String CustomerId = ""
Customer customer; 
 
//Convert rows back to objects
 
while (rs.next()) {
   //New CustomerID value
   if rs.getObject('CustomerId').toString != Customerid) {
       if (customerId != "") { print(customer.email) }
       customer = new Customer(rs.getString("ct.name"),   
                               rs.getString('ct.email'), 
                               rd.getObject('CustomerId')
   
  }
   customer.addresses.push(new Address(rs.getInteger('ads.number'), 
                           rs.getString("ads.street"),
                           rs.getString('ads.town'),         
                           rs.getString("ads.type")))
}
if (customerId != "") { print(customer.email) }

这个代码冗长且会随着对象所在字段深度或数量的增加而愈发复杂,而添加新字段则需要执行大量相关更改。

相比之下,有了文档数据库后,可供使用的代码如下所示,向对象添加新字段或深度时也不必更改数据库交互:

//Connect
mongodb = new MongoClient(CONNECTION_STRING)
customers = mongodb.getDatabase("shop").getCollection("customers",Customer.class)
 
//Add Sally with her addresses
customers.insertOne(newCustomer)
 
//Find all the customers with an address in freeport
FreeportCustomer = new Customer()
FreeportCustomer.set("addresses.town") = "Freeport"
 
FindIterable < Customer > freeportCustomers = customers.find(freeportCustomer)
for (Customer customer : freeportCustomers) {
   print(customer.email) //These have the addresses populated too
}

开发人员遇到编程模型(对象)和存储模型(行)之间断开连接的情况时,也能快速创建出同事和未来的自己看不到的抽象层。能够自动在对象和表格之间来回转换的代码称为对象关系映射 (ORM)。遗憾的是,ORM 往往使用特定语言,将开发团队与该语言绑定,使得将其他工具和技术用于该数据的难度增大。

要执行更复杂操作时,即便使用了 ORM,也避免不了 SQL 的负担。此外,由于基础数据库不会识别对象,ORM 通常无法在数据库存储和处理环节提供足够效率。

类似 MongoDB 的文档数据库会保留开发人员已经熟悉的对象,因此,不需要类似 ORM 的抽象层。此外,只要学会使用其中一个语言版本的 MongoDB,使用其他语言版本也就不在话下。因此,再也不必为了使用伪英文 SQL 查询而将对象移回。

PostgreSQL 和 Oracle 的确支持 JSON 数据类型,但是靠 JSON 来摆脱 SQL 并不可行。RDBMS 中的 JSON 适用于非托管、非结构化数据,是使用了糟糕的附加查询语法、经过美化的字符串类型。JSON 不适合数据库结构。因此,实际的文档数据库才能满足需求。

减少特定工作负载所需的硬件

现代文档数据库的内部结构非常类似 RDBMS。标准化关系模型中的架构要求所有请求都得到公平对待,而与其不同的是,文档数据库会牺牲其他工作负载来优化特定工作负载的架构。文档模型不仅会将相关行都放在同一个关系模型中,也会将可能要用于特定任务的全部数据放在同一个位置,以这种方式将按索引组织的表格和聚集索引提高到一个新的水平。此处体现的理念是,若拥有一阶数组类型,则关系的重复子属性就不需要放在单独的表(以及类似存储)中。或者,换句话说,可以拥有列类型的“嵌入式表”。

这种所谓的协同位置,即,对弱实体表的隐式联接能够降低从存储区检索数据的成本,因为通常只需读取单一缓存或磁盘位置就可以将对象传回客户端或为其应用筛选条件。

与前述做法不同的是,另一种做法需要识别、找出并读取许多行才能传回相同数据和必要的客户端硬件,从而利用这些行来重构对象。后面这种做法的成本非常高,以至于开发人员会优先考虑让次要且更简单的键值存储(而不是主要数据库)充当缓存。

这些开发人员知道主要数据库无法独自以合理方式满足工作负载需求。文档数据库不需要提前配备外部缓存就能够达成性能目标,但仍能够执行 RDBMS 的全部任务,且效率更高。

效率提升了多少?我按步骤打造了测试装置来确定与使用标准关系数据库相比,使用文档数据库可以提升的效率和节省的成本。在这些测试中,我想要量化为打造同类最佳云托管 RDBMS 与云托管文档数据库 (尤其是 MongoDB Atlas),每美元的事务吞吐量分别为几何。

我选择的用例代表了常见、真实的应用程序,其中的数据集会定期更新,且读取频率更高:以已发布的数据为基础实施英国的汽车检测 (MOT) 系统,及其公共界面和私有界面。

测试结果显示,在 MongoDB Atlas 中创建、更新和读取操作的速度大大提高。总体而言,在实例成本类似的类似指定服务器实例上,MongoDB Atlas 每秒管理的事务大约多 50%。而关系结构越复杂,这个差值就越大,导致联接的成本也越高。 除了基础的实例成本外,在这些测试中,因为利用磁盘的额外费用,关系数据库的每小时运行成本是 Atlas 成本的 200% 到 500% 不等。利用 Atlas 托管系统的成本整体低 3 到 5 倍,非常适合达成特定性能目标。简单地说,Atlas 每美元可推进的事务多得多。

独立测试也证实了文档模型的高效率。总部设于瑞士的软件公司 Temenos 受到全球大型银行和金融机构的青睐,在执行基准测试方面,其拥有超过 15 年的经验。在其最近的测试中,该公司通过 MongoDB Atlas 达到 74,000 的事务处理速率 (TPS)。

这次的测试实现的每核心吞吐量比三年前类似测试的吞吐量高 4 倍,与此同时,使用的基础设施减少 20%。执行这个测试所使用的是生产级基准架构,搭载能够反映生产系统的配置,如高可用性、安全性和专用链接等非功能性要求。

在此测试期间,MongoDB 读取了 74,000 TPS,响应时间为 1 毫秒,此外还引入了另外的 24,000 TPS。此外,由于 Temenos 也使用了文档数据库,过程中没有缓存。所有查询都是直接参照数据库运行。

总结

除非想要在组织中配置供全部应用程序使用的单一数据库,否则建议将工作负载从关系模型移到文档模型。因为这样能够让组织用更少时间、相同数量的开发人员打造出更多数据库,以及大幅减少将数据库投入生产的成本。您所在组织不太可能还没开始使用面向对象的编程。那么,为何您还不开始试试面向对象的文档数据库呢?可注册 阿里云版MongoDB https://free.aliyun.com/pipCode=mongodb&utm_content=m_1000371601 进行试用。

文档数据库能手 John Page 在加入 MongoDB 前,已在完整堆栈文档数据库技术领域拥有 18 年的经验。目前,他也参与机器人构建,但属于玩票性质。他也测试数据库并撰写相关文章,获得的酬劳用于支撑机器人项目。 阅读 John Page 的更多文章→