【Kotlin + Spring Boot 4 从零到架构师】第 51 篇:Kotlin 协程在 Spring Boot 中的应用

本系列定位:零基础入门,从 Kotlin 语法一路到 Spring Boot 4 高级架构(DDD + Modulith),适合 Java 开发者转型,也适合纯新手系统学习。


本篇你将学到

  • Spring Boot 4 对 Kotlin 协程的支持
  • suspend Controller 函数
  • 协程与 JDBC / JPA 的桥接
  • JDK 21 虚拟线程 vs Kotlin 协程的对比

学完本篇,你将能在 mini-shop 中运用协程处理并发请求,提升吞吐量。


一、Spring Boot 协程支持

1.1 suspend Controller

Spring MVC 从 5.3 开始支持 Kotlin 协程。Controller 方法可以直接声明为 suspend

@RestController
@RequestMapping("/api/products")
class ProductController(
    private val productService: ProductService
) {
    // suspend 函数——Spring 会自动在协程线程池中执行
    @GetMapping("/{id}")
    suspend fun getById(@PathVariable id: Long): ApiResponse<ProductResponse> {
        val result = productService.findByIdAsync(id)
        return ApiResponse.success(result)
    }

    // 多个 suspend 调用可以并发
    @GetMapping("/{id}/detail")
    suspend fun getDetail(@PathVariable id: Long): ApiResponse<ProductDetail> {
        // 并发获取商品信息和评价
        val productDeferred = async { productService.findByIdAsync(id) }
        val reviewsDeferred = async { reviewService.findByProductIdAsync(id) }

        val product = productDeferred.await()
        val reviews = reviewsDeferred.await()

        return ApiResponse.success(ProductDetail(product, reviews))
    }
}

1.2 为什么用 suspend Controller

传统(阻塞):
  请求线程 → 查数据库(阻塞等待)→ 返回
  100 个并发请求 = 100 个线程被阻塞

协程(非阻塞):
  请求线程 → 查数据库(挂起,释放线程)→ 数据返回后恢复
  100 个并发请求可以用少量线程处理

Spring Boot 在底层把 suspend 函数转换为响应式处理,但你写的是同步风格的代码——这就是协程的魅力。


下面是传统阻塞模型与协程非阻塞模型的流程对比图:

协程非阻塞模型

请求1

协程1

数据库查询
(挂起,释放线程)

数据返回后恢复

返回响应

请求2

协程2

数据库查询
(挂起,释放线程)

数据返回后恢复

返回响应

传统阻塞模型

请求1

线程1

数据库查询
(阻塞等待)

返回响应

请求2

线程2

数据库查询
(阻塞等待)

返回响应

二、协程 Service 层

2.1 suspend Service 方法

@Service
class ProductService(
    private val productRepository: ProductRepository
) {
    // suspend 方法
    suspend fun findByIdAsync(id: Long): ProductResponse {
        // JPA 是阻塞的,需要用 withContext 切换到 IO 线程
        return withContext(Dispatchers.IO) {
            val product = productRepository.findById(id)
                .orElseThrow { ResourceNotFoundException("商品", id) }
            product.toResponse()
        }
    }

    // 并发查询多个商品
    suspend fun findByIdsAsync(ids: List<Long>): List<ProductResponse> {
        return ids.map { id ->
            async { findByIdAsync(id) }
        }.awaitAll()
    }
}

2.2 为什么需要 withContext(Dispatchers.IO)

JDBC 是阻塞式 API——调用时会占住线程等待数据库响应。如果在协程中直接调用,会阻塞协程线程。

withContext(Dispatchers.IO) 把阻塞操作切换到专门的 IO 线程池,不阻塞协程线程:

suspend fun query(id: Long): Product = withContext(Dispatchers.IO) {
    // 这里的阻塞代码在 IO 线程池执行
    productRepository.findById(id).orElseThrow()
}

三、R2DBC 响应式数据库(进阶)

3.1 JDBC vs R2DBC

维度 JDBC R2DBC
模型 阻塞(一个连接占一个线程) 响应式(非阻塞)
协程支持 需要 withContext 桥接 原生支持 suspend
Spring Data Spring Data JPA Spring Data R2DBC
成熟度 非常成熟 较新

3.2 R2DBC Repository

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
    runtimeOnly("org.postgresql:r2dbc-postgresql")
}
// R2DBC Repository —— 原生 suspend 支持
interface ProductRepository : R2dbcRepository<Product, Long> {

    // 所有方法自动支持协程
    suspend fun findById(id: Long): Product?

    suspend fun findByCategory(category: String): List<Product>
}

// Service 直接用,不需要 withContext
@Service
class ProductService(
    private val productRepository: ProductRepository
) {
    suspend fun findById(id: Long): ProductResponse? {
        val product = productRepository.findById(id)    // ← 原生 suspend
        return product?.toResponse()
    }
}

选择建议

  • 新项目、追求极致性能:R2DBC
  • 已有 JPA 项目、功能丰富:JPA + withContext 桥接
  • mini-shop 系列:用 JPA(前面全部代码),协程部分作为可选方案

四、JDK 21 虚拟线程

4.1 什么是虚拟线程

JDK 21 引入了虚拟线程(Virtual Thread / Project Loom)——JVM 级别的轻量级线程:

传统线程(平台线程):1 个线程 = 1 个 OS 线程(~1MB 内存)
虚拟线程:N 个虚拟线程 → 1 个载体线程(~KB 级内存)

4.2 启用虚拟线程

spring:
  threads:
    virtual:
      enabled: true            # Spring Boot 自动用虚拟线程处理请求

一行配置!Spring Boot 会把 Tomcat 的请求处理线程换成虚拟线程。

4.3 虚拟线程 vs 协程

维度 虚拟线程 Kotlin 协程
语言级别 JVM(Java/Kotlin 都能用) Kotlin 语言特性
编程模型 看起来和普通线程一样 suspend 函数
学习成本 极低(几乎零改动) 需要理解协程概念
生态 兼容所有阻塞 API(JDBC/JPA) 需要协程友好的库
性能 优秀 优秀
调试 堆栈追踪可能很长 结构化并发,调试友好

4.4 选择建议

场景 推荐
已有 Spring Boot + JPA 项目 虚拟线程(零代码改动)
纯 Kotlin 新项目 协程(suspend 风格更优雅)
需要复杂异步编排 协程(async/await + 结构化并发)
想要最简单的方案 虚拟线程

mini-shop 的建议:如果你不想改动已有代码,直接启用虚拟线程(一行配置),就能获得大部分协程的吞吐量提升。


下面是虚拟线程与 Kotlin 协程的选择决策流程图:

已有 Spring Boot + JPA 项目

纯 Kotlin 新项目

需要复杂异步编排

想要最简单的方案

开始选择

项目类型?

虚拟线程
(零代码改动)

Kotlin 协程
(suspend 风格更优雅)

Kotlin 协程
(async/await + 结构化并发)

虚拟线程

获得高吞吐量

五、性能对比

5.1 模拟高并发场景

// 阻塞式 Controller(传统)
@GetMapping("/blocking/{id}")
fun getByIdBlocking(@PathVariable id: Long): ProductResponse {
    Thread.sleep(100)            // 模拟 IO 延迟
    return productService.findById(id)
}

// 协程式 Controller
@GetMapping("/coroutine/{id}")
suspend fun getByIdCoroutine(@PathVariable id: Long): ProductResponse {
    delay(100)                   // 挂起 100ms(不阻塞线程)
    return withContext(Dispatchers.IO) {
        productService.findById(id)
    }
}

100 个并发请求的性能对比(概念值):

方式 线程数 总耗时 说明
阻塞 200 线程 ~100ms 每个请求占一个线程
协程 少量线程 ~100ms 挂起不占线程
虚拟线程 100 虚拟线程 ~100ms JVM 调度

实际提升取决于 IO 操作的占比。IO 密集型应用(大量数据库查询/HTTP 调用)受益最大。


下面是 100 个并发请求下,阻塞、协程、虚拟线程三种方式的处理时序图:

数据库 Tomcat 线程池 客户端 数据库 Tomcat 线程池 客户端 阻塞模式(200 线程) 100 个线程被占用 协程模式(少量线程) 少量线程处理所有请求 虚拟线程模式(100 虚拟线程) JVM 调度虚拟线程 100 个并发请求 分配 100 个线程 查询(阻塞等待) 返回数据 返回响应 100 个并发请求 创建 100 个协程 查询(挂起,释放线程) 数据返回后恢复 返回响应 100 个并发请求 创建 100 个虚拟线程 查询(阻塞等待) 返回数据 返回响应

本篇小结

知识点 核心内容
suspend Controller Spring MVC 原生支持协程
withContext(Dispatchers.IO) 桥接阻塞式 JDBC
R2DBC 响应式数据库,原生 suspend 支持
虚拟线程 JDK 21,一行配置启用
虚拟线程 vs 协程 虚拟线程零改动,协程更灵活
async { }.awaitAll() 协程并发执行多个查询
适用场景 IO 密集型应用受益最大

下篇预告

第 52 篇(终篇):性能调优与生产上线清单

全系列的收官篇!下一篇总结 Spring Boot 性能调优的方方面面,并给出一份完整的生产上线检查清单。


如果本篇内容对你有帮助,欢迎点赞收藏!有任何疑问,欢迎在评论区交流。

Logo

一站式 AI 云服务平台

更多推荐