文档站点
Skip to content

异步与队列 ​

Laravel 用 dispatch() + Queue 做异步任务,支持 Redis/SQS 驱动、失败重试;Spring Boot 的对应物分两层:@Async(线程池异步,进程内) 和 外部消息队列(RabbitMQ / Kafka,跨进程可靠)。先讲透 @Async 这个和 dispatch() 最接近的入口。

@Async:进程内异步 ​

开启 + 配置线程池 ​

java
@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);          // 核心线程
        executor.setMaxPoolSize(10);          // 最大线程
        executor.setQueueCapacity(200);       // 队列容量
        executor.setThreadNamePrefix("async-");
        executor.initialize();
        return executor;
    }
}

使用 ​

java
@Service
public class NotificationService {

    @Async
    public void sendSms(String phone, String message) {
        // 调用方不等待,直接返回;这里在后台线程执行
        System.out.println("发送短信...");
    }
}
java
notificationService.sendSms("13800000000", "注册成功");   // 立即返回

对照 Laravel

@Async 方法 ≈ 一个进入队列的 Job:调用方不等它完成就返回。区别:

  • Laravel Job 通过队列驱动(Redis 等)持久化,进程崩了任务还在
  • Spring @Async 只是线程池异步,进程崩了任务就丢,不保证可靠 判断:轻量、可丢的任务用 @Async;必须成功执行的任务用消息队列。

@Async 的三个坑

  1. 同类内部调用失效:@Async 靠代理实现,this.sendSms()(同类方法调用)不走代理,等于同步执行。必须通过注入的 Bean 调用。
  2. 返回值有限制:@Async 方法返回 void 或 CompletableFuture<T>,不能返回普通对象。
  3. 别忘了 @EnableAsync:不开启,注解静默无效(和 Laravel 忘配 queue driver 一个性质)。

获取异步结果:CompletableFuture ​

对应 Laravel 的 dispatch()->afterResponse() 不需要结果;要结果时 Java 用 CompletableFuture:

java
@Service
public class ReportService {

    @Async
    public CompletableFuture<String> generate(String type) {
        String result = doHeavyWork(type);
        return CompletableFuture.completedFuture(result);
    }
}

// 调用方
CompletableFuture<String> f = reportService.generate("daily");
String result = f.get(5, TimeUnit.SECONDS);   // 等待结果(可超时)

跨进程可靠队列:Spring AMQP(RabbitMQ) ​

任务不能丢、多实例共享,就用消息队列。以 RabbitMQ + Spring AMQP 为例(对应 Laravel 的 Redis 队列 + Job):

依赖 ​

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

声明队列 + 交换机 ​

java
@Configuration
public class RabbitConfig {

    public static final String QUEUE = "post.created";
    public static final String EXCHANGE = "blog.events";
    public static final String ROUTING_KEY = "post.created";

    @Bean
    public Queue queue() {
        return new Queue(QUEUE, true);            // 持久化
    }

    @Bean
    public TopicExchange exchange() {
        return new TopicExchange(EXCHANGE);
    }

    @Bean
    public Binding binding(Queue queue, TopicExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY);
    }
}

生产消息(≈ 派发 Job) ​

java
@Service
public class PostEventProducer {
    private final RabbitTemplate rabbit;

    public PostEventProducer(RabbitTemplate rabbit) {
        this.rabbit = rabbit;
    }

    public void publishCreated(Long postId) {
        rabbit.convertAndSend(RabbitConfig.EXCHANGE,
                RabbitConfig.ROUTING_KEY,
                Map.of("postId", postId));
    }
}

消费消息(≈ 处理 Job) ​

java
@Component
public class PostCreatedConsumer {

    @RabbitListener(queues = RabbitConfig.QUEUE)
    public void onPostCreated(Map<String, Object> message) {
        Long postId = (Long) message.get("postId");
        System.out.println("后台处理帖子 " + postId + " 的索引...");
    }
}

TIP

@RabbitListener + 交换机/队列 = Laravel 的「Job 入队 + 队列 worker 消费」:消息持久化、可失败重试(@Retryable / 死信队列)、多实例可水平扩展消费。这套才是「队列」的完整形态。

Laravel Queue → Spring 对照表 ​

概念LaravelSpring Boot
同步异步dispatch()@Async
入队dispatch()->onQueue('high')rabbit.convertAndSend(交换机, key, msg)
消费php artisan queue:work@RabbitListener
延迟任务delay(10)MessageProperties.delay(RabbitMQ 插件)
失败重试$tries / backoff@Retryable / RetryTemplate
失败落库failed_jobs 表死信队列
优先级onQueue('high')交换机路由 / 队列优先级
驱动Redis / SQS / DBRabbitMQ / Kafka / Redis(spring-data-redis Stream)

实践建议 ​

  1. 先想清楚要哪种:
    • 要「快、可丢、进程内」→ @Async
    • 要「可靠、跨实例、可重试」→ RabbitMQ / Kafka
    • 要「超时自动完成」→ 任务调度(任务调度)
  2. 消息体别塞大对象:只放 ID 等轻量数据,消费端再去查(消息队列不是数据库)
  3. 幂等:消费端逻辑要能重复执行不产生副作用(记录处理过的 ID 或用数据库唯一约束)
  4. 监控:RabbitMQ 管理台看堆积;@Async 线程池记得配拒绝策略(CallerRunsPolicy)防队列满丢任务

关于 Kafka

Kafka 适合「海量日志 / 流式事件」,和 RabbitMQ 的「任务队列」定位不同。绝大多数业务「发个邮件、算个索引」用 RabbitMQ/Redis 队列就够,别为了「高级」上 Kafka。

面向 PHP / Laravel 开发者的 Spring Boot 中文文档