读源码时的一句注释,让我重新审视响应式
前阵子读 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 + WebMvc | R2DBC + WebFlux |
|---|---|---|
| 慢依赖下的并发能力 | 受限于线程池大小 | 事件循环,抗阻塞 |
| 代码心智负担 | 低 | 高 |
| 事务/调试友好度 | 高 | 低 |
| 适用 | 多数 CRUD 业务 | 网关、聚合查询、高扇出 IO |
小结
- R2DBC 解决的是"SQL 阻塞线程"这个具体问题,配合 WebFlux 能做出真正非阻塞的链路。
- 它不是 JDBC 的替代品,而是另一种编程模型,迁移成本集中在思维转换。
- 全链路里只要有一处阻塞没处理好,响应式带来的好处就归零,甚至更糟。
- 我们最终只在网关和报表聚合服务用 R2DBC,核心交易链路仍保留 JDBC。
那句注释我记到现在。R2DBC 把"不阻塞"这件事做对了,但工程落地时,人能不能全程不写 blocking 代码,才是真问题。