Administrator
发布于 2022-05-18 / 8169 阅读
137

R2DBC 响应式数据库访问实践

读源码时的一句注释,让我重新审视响应式

前阵子读 Spring Data R2DBC 的源码,看到 R2dbcEntityTemplate 里一句话:"JDBC blocks the calling thread; R2DBC does not." 我当时手头正有个网关服务,Tomcat 的线程池经常被慢 SQL 占满,于是想认真试一次 R2DBC,把整条链路做成非阻塞。

R2DBC 是什么:非阻塞的"JDBC"

R2DBC(Reactive Relational Database Connectivity)是套响应式关系型数据库连接规范,对标 JDBC,但底层是事件循环 + Netty 那一套,SQL 执行不占用业务线程。它最早的驱动力就是配合 Reactor/WebFlux 实现全栈响应式:从 HTTP 请求进来,到查库、拼装、写回响应,全程不阻塞线程。

接入 Spring Boot 2.7,把 spring-boot-starter-data-r2dbc 换掉 JDBC 的 starter,连接池用 R2DBC 版的(比如 r2dbc-pool + r2dbc-mysql):

spring:
  r2dbc:
    url: r2dbc:mysql://10.0.1.12:3306/order_db?useSSL=false
    username: app
    password: ${DB_PWD}
    pool:
      initial-size: 10
      max-size: 50

一段全栈响应式的代码

Controller 用 WebFlux 的 Mono,Repository 用 ReactiveCrudRepository,中间没有任何 .block()

@GetMapping("/orders/{uid}")
public Flux<OrderView> list(@PathVariable Long uid) {
    return orderRepo.findByUserId(uid)
            .flatMap(o -> userClient.name(o.getUserId())
                    .map(name -> OrderView.from(o, name)))
            .subscribeOn(Schedulers.boundedElastic());
}

public interface OrderRepo extends ReactiveCrudRepository<OrderPO, Long> {
    Flux<OrderPO> findByUserId(Long userId);
}

压测对比很有意思:同样 4 核 8G,JDBC + Tomcat(200 线程)在慢 SQL(平均 120ms)下,到 180 并发就大量 503;换成 R2DBC + WebFlux,事件循环线程只有少量,800 并发时数据库连 50 个连接都没打满,吞吐反而更高。

现实中的权衡:我踩的三个坑

但全栈响应式不是免费午餐,上线前我踩了三个坑,每一个都够写一页复盘。

坑一:一行 blocking 调用毁掉整条链

我在某个 map 里调了一个老工具类的 Thread.sleep 做限流,结果整个事件循环被卡住,其他请求全饿死。响应式里任何阻塞调用都要丢到 Schedulers.boundedElastic(),否则就是灾难:

// 错误:在事件循环线程里同步阻塞
.map(o -> blockingLegacyCall(o));

// 正确:挪到弹性调度器
.flatMap(o -> Mono.fromCallable(() -> blockingLegacyCall(o))
        .subscribeOn(Schedulers.boundedElastic()))

坑二:事务边界很别扭

R2DBC 的事务是 TransactionalOperator@Transactional 配合响应式事务管理器,跨多个 Publisher 时要用 transactionalOperator.transactional(...) 包裹。比起 JDBC 的声明式事务,心智负担明显更重,复杂多表写入我最后还是退回了 JDBC。

坑三:调试与排查变难

线程栈里全是 reactor.core.publisher 的回调,出异常时堆栈被拉得很长,定位"到底哪一步失败了"要花比 JDBC 多一倍的时间。新人上手曲线陡。

我的结论:它适合特定场景

维度JDBC + WebMvcR2DBC + WebFlux
慢依赖下的并发能力受限于线程池大小事件循环,抗阻塞
代码心智负担
事务/调试友好度
适用多数 CRUD 业务网关、聚合查询、高扇出 IO

小结

  • R2DBC 解决的是"SQL 阻塞线程"这个具体问题,配合 WebFlux 能做出真正非阻塞的链路。
  • 它不是 JDBC 的替代品,而是另一种编程模型,迁移成本集中在思维转换。
  • 全链路里只要有一处阻塞没处理好,响应式带来的好处就归零,甚至更糟。
  • 我们最终只在网关和报表聚合服务用 R2DBC,核心交易链路仍保留 JDBC。

那句注释我记到现在。R2DBC 把"不阻塞"这件事做对了,但工程落地时,人能不能全程不写 blocking 代码,才是真问题。

参考