使用虚拟线程编写 CRUD 应用程序

上周,我们发布了一个视频,演示了如何在 Quarkus 中使用虚拟线程创建 CRUD 应用程序。这很简单,只需在 HTTP 资源(如果您使用 Spring 兼容层,则为控制器类)上添加 @RunOnVirtualThread 注解即可。

这篇配套文章解释了它在幕后的工作原理。

代码

该应用程序是 Todo Backend 的简单实现。本文的完整代码可在此处找到。

重要的是 TodoResource.java

package org.acme.crud;

import io.quarkus.logging.Log;
import io.quarkus.panache.common.Sort;

import io.smallrye.common.annotation.NonBlocking;
import io.smallrye.common.annotation.RunOnVirtualThread;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import java.util.List;


@Path("/api")
@RunOnVirtualThread
public class TodoResource {

    /**
     * Just print on which thread the method is invoked.
     */
    private void log() {
        Log.infof("Called on %s", Thread.currentThread());
    }

    @GET
    public List<Todo> getAll() {
        log();
        return Todo.listAll(Sort.by("order"));
    }

    @GET
    @Path("/{id}")
    public Todo getOne(@PathParam("id") Long id) {
        log();
        Todo entity = Todo.findById(id);
        if (entity == null) {
            throw new WebApplicationException("Todo with id of " + id + " does not exist.",
                Status.NOT_FOUND);
        }
        return entity;
    }

    @POST
    @Transactional
    public Response create(@Valid Todo item) {
        log();
        item.persist();
        return Response.status(Status.CREATED).entity(item).build();
    }

    @PATCH
    @Path("/{id}")
    @Transactional
    public Response update(@Valid Todo todo, @PathParam("id") Long id) {
        log();
        Todo entity = Todo.findById(id);
        entity.id = id;
        entity.completed = todo.completed;
        entity.order = todo.order;
        entity.title = todo.title;
        entity.url = todo.url;
        return Response.ok(entity).build();
    }

    @DELETE
    @Transactional
    public Response deleteCompleted() {
        log();
        Todo.deleteCompleted();
        return Response.noContent().build();
    }

    @DELETE
    @Transactional
    @Path("/{id}")
    public Response deleteOne(@PathParam("id") Long id) {
        log();
        Todo entity = Todo.findById(id);
        if (entity == null) {
            throw new WebApplicationException("Todo with id of " + id + " does not exist.",
                Status.NOT_FOUND);
        }
        entity.delete();
        return Response.noContent().build();
    }

}

该应用程序使用

  • RESTEasy Reactive - Quarkus 推荐的 REST 堆栈。它支持虚拟线程。

  • Hibernate Validation - 用于验证用户创建的 Todos。

  • Hibernate ORM with Panache - 用于与数据库交互。

  • Agroal 连接池 - 用于管理和回收数据库连接。

  • Narayana 事务管理器 - 用于在事务中运行我们的代码。

  • PostgreSQL 驱动程序 - 因为我们使用 PostgreSQL 数据库

代码与 Quarkus 的常规 CRUD 服务实现类似,除了一行。我们在资源类(第 17 行)上添加了 @RunOnVirtualThread 注解。它指示 Quarkus 在虚拟线程而非常规平台线程上调用这些方法(在上一篇博文中了解更多关于差异的信息),包括 @Transactional 方法。

线程模型

正如我们在代码中所见,开发模型是同步的。与数据库的交互使用阻塞 API:您等待响应。这就是虚拟线程发挥魔力的地方。它不会阻塞平台线程,而只会阻塞虚拟线程。

Threading model of the application

因此,当另一个请求到来时,载体线程可以处理它。这极大地减少了在存在大量并发请求时所需的平台线程数量。结果,在同步和阻塞开发模型中使用的工作线程数量不再是瓶颈。

但是,这并不是说您使用虚拟线程就不会遇到并发限制。存在一个新的瓶颈:数据库连接池。当您与数据库交互时,您会从连接池(在本例中为 Agroal)请求连接。连接的数量不是无限的(默认是 20 个)。一旦所有连接都用完,您必须等到另一个进程完成并释放其连接。您仍然可以并发处理许多请求,但它们将等待数据库连接可用,从而降低响应时间。

关于固定(pinning)的注意事项

正如上一篇博文所述,当虚拟线程无法从载体线程上卸载时,就会发生固定。在这种情况下,阻塞虚拟线程也会阻塞载体线程。

Pinning of the carrier thread

幸运的是,在此应用程序中没有固定。PostgreSQL 驱动程序是少数几个不固定的 JDBC 驱动程序之一。如果您计划使用其他数据库,请先进行检查。我们将在下一篇文章中讨论如何检测固定。Quarkus、Narayana 和 Hibernate 已被修补以避免固定。

固定是可能出现的问题之一。应用程序将受到 Jackson 使用的默认对象池机制的影响。幸运的是,我们为Jackson 贡献了一个 SPI,它将允许我们移除这个内存占用大户。

结论

本文介绍了在 Quarkus 中使用虚拟线程实现 CRUD 应用程序。您现在可以使用命令式开发模型,而不会损害应用程序的并发性。这就像使用 RESTEasy Reactive 并添加一个注解一样简单:在您的资源上添加 @RunOnVirtualThread

我们对 Quarkus 和上游项目(如 Hibernate、Narayana、SmallRye Mutiny 等)进行了定制,使其与虚拟线程兼容。正如我们在其他文章中将看到的,大多数 Quarkus 扩展都已准备好与虚拟线程一起使用。

也就是说,虽然虚拟线程增加了并发性,但您很可能会遇到其他瓶颈,例如连接池中管理的数据库连接数。

在下一篇文章和视频中,我们将介绍如何测试我们的应用程序并检测固定。