在前面的部分中,我们得出结论,每个服务器最终都将成为某些不同服务的客户机。另一个有趣的发现是,几乎所有我们有机会使用的计算机系统都是分布式的。当两台机器用网络电缆分开时,需要相互通信,它们已经在空间上分布了。将其应用到极致,您甚至可以将每台计算机视为一个分布式系统,独立的CPU内核的缓存并不总是一致的,并且必须通过消息传递协议与另一个服务器进行同步。但让我们继续使用应用服务器和数据库服务器架构。
在Java中,关系数据库访问的长期存在的标准称为Java数据库连接(JDBC)。从使用者的角度来看,JDBC提供了一组API,用于与任何关系数据库(如PostgreSQL、Oracle数据库等)进行通信。核心抽象是Connection(TCP / IP)、Statement(数据库查询)和ResultSet(数据库查询结果)。今天,开发人员很少直接使用这个API,因为有更轻量级的抽象,从轻量级的Spring框架的JdbcTemplate,通过代码生成库,如jOOQ,到像JPA这样的对象关系映射解决方案。JDBC有一个臭名昭著的伴随着检查异常的异常处理(从Java 7开始,try-with-resources就简单多了)
import java.sql.*;
try (
Connection conn = DriverManager.getConnection("jdbc:h2:mem:");
Statement stat = conn.createStatement();
ResultSet rs = stat.executeQuery("SELECT 2 + 2 AS total")
) {
if (rs.next()) {
System.out.println(rs.getInt("total"));
assert rs.getInt("total") == 4;
}
}
前面的示例使用嵌入的H2数据库,通常在集成测试期间使用。但是在生产中,很少看到数据库实例运行在与应用程序相同的机器上。每个与数据库的交互都需要一个网络往返。JDBC的核心部分是API,每个数据库供应商都必须实现它。
当向JDBC API请求一个新连接时,实现必须通过打开一个客户端套接字、授权等等来实现对数据库的物理连接。数据库具有不同的有线协议(几乎是普遍的是二进制),而JDBC的实现(也称为Driver)的职责是将这个低级网络协议转换为一致的API。这中方式工作的很好(将不同的SQL方言放到一边),不幸的是,在1997年的时候,当使用JDK 1.1发布JDBC标准时,没有人预测在20年后,响应式和异步编程有多么重要。当然,API也经过了许多版本,但它们所有都是固有阻塞的,即等待每个数据库操作完成。
这和HTTP的问题完全一样。您的应用程序中必须有与活跃的数据库操作(查询)一样多的线程。JDBC是唯一一种能够以可移植的方式访问各种关系数据库的成熟标准(又一次,将SQL方言区别放在一边)。servlet规范几年前在version 3.0通过引入HttpServletRequest.startAsync()方法做了显著改。但是JDBC标准仍然保存着经典模型,这太糟糕了。
我们来说JDBC仍然存在阻塞的原因。Web服务器可以轻松地处理成千上万的开放连接;例如,如果他们只是偶尔地流一小段数据。另一方面,数据库系统对每个客户端查询执行如下几个类似的步骤:
Query parsing(查询解析)(CPU限制的)将包含查询的字符串转换为解析树
Query optimizer(查询优化器)(CPU限制的)对各种规则和统计数据进行查询,试图构建一个执行计划
Query executor(查询执行器)(I/O限制的)遍历数据库存储并找到合适的元组返回
Result set(结果集)(network限制的)序列化并被推回到客户端
显然,每个数据库都需要大量的资源来执行查询。通常,大多数时间实际上花费了执行查询和磁盘(磁盘旋转或SSD)的设计上不是很并行。因此,数据库系统可以并且应该执行的并发查询数量有限,直到它饱和为止。这个限制很大程度上取决于实际使用的数据库引擎和它运行的硬件。还有许多其他不太明显的方面,如锁、上下文切换和CPU缓存线路耗尽。您应该预期每秒大约有几百个查询。您应该预期每秒大约有几百个查询。相比之下,这与成千上万的开放HTTP连接相比是非常少的,可以轻松地通过非阻塞API实现。
知道数据库的吞吐量受到硬件的严重限制,具有完全和彻底响应式的驱动程序终究没有意义(不知道作者到底想表达什么)。从技术上讲,您可以在Netty或RxNetty上实现一个有线协议,它不会阻塞客户机线程。事实上,有许多非标准的、独立开发的方法(见postgresql-async, postgres-async-driver, adbcj, 和finagle-mysql),它们都试图用非阻塞的网络堆栈实现特定数据库的有线协议。但是知道JVM可以处理成百上千个线程而不需要太多麻烦(参见329页的“Thread per Connection”),从底层重写已建立的JDBC API似乎没有多少好处。即使是使用Akka toolkit提供的Slick,也使用JDBC。还有一些社区主导的项目在RxJava和JDBC之间架桥,比如RxJava - JDBC。
:
与关系数据库交互的建议是,实际上会有一个专用的、调优的线程池,并将阻塞代码隔离在那里。您的应用程序的其余部分可以高度具有响应性,并仅在少数几个线程进行操作(我们前几节介绍的,响应式使得我们可以只需要很少的线程就可以完成上完的并发连接),但从实用主义的角度来看,还是使用JDBC,因为试图用更有响应性的东西来代替它只会带来很多痛苦,而且没有明显的收获。