在“14 | 响应式全栈:响应式编程能为数据访问过程带来什么样的变化”中我们提到了全栈响应式编程的概念,知道了数据访问层同样需要处理响应式场景。而在日常开发过程中被广泛采用的关系型数据库,采用的都是非响应式的数据访问机制。那么,关系型数据库能否具备响应式数据访问特性呢?这就是这一讲将要探讨的话题。

JDBC 规范与 Spring Data

在讨论如何让关系型数据访问也能具备响应式特性之前,我们有必要回顾一下 Java 领域中使用最广泛的 JDBC 规范,以及 Spring Data 中包含的实现方案。

阻塞式 JDBC

JDBC 是 Java DataBase Connectivity 的全称,它的设计初衷是提供一套能够应用于各种数据库的统一标准。而不同的数据库厂家共同遵守这套标准,并提供各自的实现方案供应用程序调用。作为统一标准,JDBC 规范具有完整的架构体系,如下所示。

JDBC 规范整体架构图

正如上图所示,JDBC 中通过驱动管理器 DriverManager 加载各种针对不同数据库的驱动程序 Driver;而应用程序通过调用 JDBC API 来实现对数据库的操作,包括初始化和执行 SELECT、CREATE、INSERT、UPDATE、DELETE 等 SQL 语句。

基于驱动程序的数据库通信过程,与调用 Web 服务的通信过程,本质上没有什么太大的差别。但是,任何的数据访问操作都必须包含在 JDBC 级别的同步阻塞调用中,也就是说,JDBC 是一款被设计成同步和阻塞式的规范,并没有提供类似 WebFlux 这样的响应式编程组件。

我们把讨论的范围再扩大一些,无论是 Spring 框架,还是其他 Java 领域的关系型数据持久化方案,所有的工具库本身都是同步和阻塞的。这里面讨论的对象包括 JDBC、JPA、Hibernate、EclipseLink、Spring JDBC 和 Spring Data JDBC 等。这些工具库在使用时都会涉及通过网络调用完成数据访问,但它们不允许非阻塞交互。向数据库发出查询的 Java 线程注定要被阻塞,直到第一个数据到达或发生超时。因此,这些工具库都与响应式编程的理念相冲突。

Spring Data 与关系型数据访问

尽管我们已经知道 Spring Data 并没有提供对关系型数据库的响应式组件,但还是有必要对其展开讨论,看看有没有办法让它具备响应性。Spring Data 中针对关系型数据库提供的技术体系比较丰富,下面我就针对其中的 JdbcTemplate、JPA 等核心组件进行一一说明。

首先,因为 JDBC 是一种比较偏向底层的开发规范,需要处理连接获取、资源释放、结果集处理等一系列烦琐而又重复的工作,所以并不太适合直接应用到业务开发过程中。为了简化原生 JDBC 的开发复杂性,Spring 提供了一个 JdbcTemplate 模板工具类,它有助于执行查询并将数据结果集映射到实体对象。它还能自动处理资源的创建和释放,有助于避免忘记关闭连接等常见错误。

JdbcTemplate 模板工具类诞生已久,但开发起来也还是有一定的复杂度,例如需要手工对结果集进行处理,一定程度上也属于偏中底层的开发技术。为此,Spring Data JDBC 就诞生了,它是 Spring Data 家族中一个比较新的模块。与 Spring Data 中的其他组件一样,Spring Data JDBC 旨在简化基于 JDBC 的 Repository 的实现。我们已经在第 14 讲中介绍了 Spring Data 中的 CrudRepository 接口,基于这个接口同样可以实现对关系型数据库的有效访问。

1
2
3
4
5
@Repository
public interface AccountRepository extends CrudRepository<Account, String> {

    List<Account> getAccountByAccountCode(String accountCode);
}

Spring Data JDBC 是一个非常小的模块,它的设计目标是简便性,而不是面向 ORM 的缓存、实体延迟加载和复杂实体关系等需求。如果想要实现这些功能,Java 生态系统有一个单独的规范,称为 JPA(Java Persistence API,Java 持久化 API),也存在 Hibernate 和 EclipseLink 等一批主流的实现框架。Spring Data 同样提供了 JpaRepository 接口,允许我们像 Spring Data JDBC 那样构建 Repository,但在内部它使用了更强大的基于 JPA 的实现。使用 JpaRepository 的方式也非常简单。

1
2
3
4
5
@Repository
public interface AccountRepository extends JpaRepository<Account, String> {

    List<Account> getAccountByAccountCode(String accountCode);
}

我们可以用一张图来展示 Spring Data 中与关系型数据访问相关的技术组件。

Spring Data JDBC 阻塞式技术体系

使关系型数据访问具有响应性

从技术体系而言,越偏向底层的技术越容易完成改造和集成。但不幸的是,没有简单的解决方案可以用来调整 JDBC 并使它具备响应式访问特性。

对应的,针对 JPA 这种高级开发框架,可以说很难对其开展异步或响应性的改造工作。一方面,这样的工作同样需要建立 JDBC 的异步或响应式组件;另一方面,JPA 中的实体关系映射、实体缓存或延迟加载等功能丰富且复杂,各个实现框架中的巨大代码库使得响应式重构困难重重。

如果你想深入了解 JPA,我推荐你去看看拉勾教育的另一门专栏《Spring Data JPA 原理与实战》。

我们回到 Spring 家族,JdbcTemplate 以及 Spring Data JDBC 都需要用到 JDBC,因此同样不适用于响应式技术栈。但是,Spring Data 团队还是做出了很多尝试工作,并最终开发了 R2DBC 规范。R2DBC 是 Reactive Relational Database Connectivity 的全称,即响应式关系型数据库连接,该规范允许驱动程序提供与关系型数据库之间的响应式和非阻塞集成。

Spring Data 中同样采用了 R2DBC 规范,并开发了另一个独立模块 Spring Data R2DBC。下图展示了 JDBC 规范与 R2DBC 规范的对应关系,以及所涉及的技术栈。

Spring Data JDBC 和 Spring Data R2DBC

接下来的内容,我们将重点讨论 R2DBC 规范以及 Spring Data R2DBC,看看如何让关系型数据库能够具备响应式访问特性。

Spring Data R2DBC

R2DBC 核心组件

R2DBC 是由 Spring Data 团队领导的一项探索响应式数据库访问的尝试。R2DBC 的目标是定义具有背压支持的响应式数据库访问 API,该项目包含了三个核心组件。

R2DBC SPI:定义了实现驱动程序的简约 API。该 API 非常简洁,以便彻底减少驱动程序实现者必须遵守的 API。SPI 并不是面向业务开发人员的 API,不适合在应用程序代码中直接使用;相反,它面向的是框架开发人员,用来设计并实现专用的客户端库。任何人都可以直接使用 SPI 或通过 R2DBC SPI 实现自己的客户端库。

R2DBC 客户端:提供了一个人性化的 API 和帮助类,可将用户请求转换为 SPI,也就是说面向业务开发人员提供了对底层 SPI 的访问入口。

R2DBC 驱动:截至目前,为 PostgreSQL、H2、Microsoft SQL Server、MariaDB 以及 MySQL 提供了 R2DBC 驱动程序。

引入 Spring Data R2DBC

想要在应用程序中引入 Spring Data R2DBC,需要在 Maven 的 pom 文件中添加如下依赖项。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!-- spring data r2dbc -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
 
<!-- r2dbc 连接池 -->
<dependency>
     <groupId>io.r2dbc</groupId>
     <artifactId>r2dbc-pool</artifactId>
</dependency>
 
<!--r2dbc mysql 库 -->
<dependency>
     <groupId>dev.miku</groupId>
     <artifactId>r2dbc-mysql</artifactId>
</dependency>

我们知道在 Spring Data Reactive 中,存在一个 ReactiveCrudRepository 接口用于实现响应式数据访问。而 Spring Data R2DBC 也提供了一个专门的 R2dbcRepository,定义如下。

1
public interface R2dbcRepository<T, ID> extends ReactiveCrudRepository<T, ID> {}

可以看到,R2dbcRepository 接口实际上只是直接继承了 ReactiveCrudRepository 的现有方法而已。而 Spring Data R2DBC 提供了一个 SimpleR2dbcRepository 实现类,该实现类使用 R2DBC 规范实现了 R2dbcRepository 中的接口。值得注意的是,SimpleR2dbcRepository 类不使用默认的 R2DBC 客户端,而是定义自己的客户端以使用 R2DBC SPI。

同时,Spring Data R2DBC 也提供了一个 @Query 注解,这个注解的功能与 Spring Data 中通用的 @Query 注解类似,用于指定需要执行的 SQL 语句。我们可以基于方法名衍生查询机制定义各种数据访问操作。

使用 Spring Data R2DBC 实现数据访问

在引入 Spring Data R2DBC 之后,我们来使用该组件完成一个示例应用程序的实现。让我们先使用 MySQL 数据库来定义一张 ACCOUNT 表。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
USE `r2dbcs_account`;
 
DROP TABLE IF EXISTS `ACCOUNT`;
CREATE TABLE `ACCOUNT`(
  `ID` bigint(20) NOT NULL AUTO_INCREMENT,
  `ACCOUNT_CODE` varchar(100) NOT NULL,
  `ACCOUNT_NAME` varchar(100) NOT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
INSERT INTO `account` VALUES ('1', 'account1', 'name1');
INSERT INTO `account` VALUES ('2', 'account2', 'name2');

然后,基于该数据库表定义一个实体对象。请注意,这里使用了一个 @Table 注解指定了目标表名,如下所示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
 
@Table("account")
public class Account {
    @Id
    private Long id;
    private String accountCode;
    private String accountName;
    //省略 getter/setter
}

基于 Account 对象,我们可以设计如下所示的 Repository。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
 
public interface ReactiveAccountRepository extends R2dbcRepository<Account, Long> {
 
    @Query("insert into ACCOUNT (ACCOUNT_CODE, ACCOUNT_NAME) values (:accountCode,:accountName)")
    Mono<Boolean> addAccount(String accountCode, String accountName);

    @Query("SELECT * FROM account WHERE id =:id")
    Mono<Account> getAccountById(Long id);
}

可以看到,ReactiveAccountRepository 扩展了 Spring Data R2DBC 所提供的 R2dbcRepository 接口,然后使用 @Query 注解分别定义了一个查询和插入方法。

为了访问数据库,最后要做的一件事情就是指定访问数据库的地址,如下所示。

1
2
3
4
5
spring:
   r2dbc:
     url: r2dbcs:mysql://127.0.0.1:3306/r2dbcs_account
     username: root
     password: root

这里要注意的是 spring.r2dbc.url 的格式,需要根据数据库类型来指定,在示例中我们使用的是 MySQL 数据库。

最后,我们构建一个 AccountController 来对 ReactiveAccountRepository 进行验证。为了简单起见,这里直接在 Controller 中嵌入 Repository,如下所示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@RestController
@RequestMapping(value = "accounts")
public class AccountController {
 
    @Autowired
    private ReactiveAccountRepository reactiveAccountRepository;
 
    @GetMapping(value = "/{accountId}")
    public Mono<Account> getAccountById(@PathVariable("accountId") Long accountId) {

        Mono<Account> account = reactiveAccountRepository.getAccountById(accountId);
        return account;
    }
 
    @PostMapping(value = "/")
    public Mono<Boolean> addAccount(@RequestBody Account account) {

        return reactiveAccountRepository.addAccount(account.getAccountCode(), account.getAccountName());
    }
}

分别访问这两个 HTTP 端点,就能正确查询和插入数据库中的数据了。你可以自己做一些测试,相关代码我也放在了https://github.com/lagoueduCol/ReactiveSpring中。

这个示例介绍到这里就结束了,应该说,R2DBC 目前仍处于试验阶段,还不是很明确是否或何时可能成为面向生产的软件,让我们一起期待吧。

小结与预告

JDBC 规范是 Java EE 领域中进行关系型数据库访问的标准规范,在业界应用非常广泛,但它却是阻塞式的。如何让关系型数据库具备响应式数据访问特性是一大技术难题,在今天的课程中,我们对这个难题进行了分析,并引出了 Spring Data 中专门用于实现响应式关系型数据访问的 Spring Data R2DBC 组件,我们讨论了这个组件的核心功能并给出了相关的代码示例。

这里给你留一道思考题:R2DBC 中包含了哪些核心组件,分别有什么作用?

在讨论完响应式数据访问机制之后,下一讲我们来说说消息驱动架构。消息驱动是实现系统弹性的核心手段,而 Spring 家族中的 Spring Cloud Stream 框架则为实现消息驱动架构提供了友好的开发支持。下一讲不见不散。

点击链接,获取课程相关代码 ↓↓↓https://github.com/lagoueduCol/ReactiveProgramming-jianxiang.git

-– ### 精选评论 ##### **川: > 老师,r2dbc访问数据库变成非阻塞式后,事务怎么处理呢?传统的阻塞式访问,使用ThreadLocal保存当前connection,以确保整个事务使用同一个连接 ######     讲师回复: >     Reactor 3提供了一种叫做Context的数据结构用来替代Threadlocal ##### **友: > R2DBC老师有没有方法可以像JPA一样自动创建表结构? ######     讲师回复: >     这个倒没试过,这种属于附加功能,无伤大雅