异步与队列
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 的三个坑
- 同类内部调用失效:
@Async靠代理实现,this.sendSms()(同类方法调用)不走代理,等于同步执行。必须通过注入的 Bean 调用。 - 返回值有限制:
@Async方法返回void或CompletableFuture<T>,不能返回普通对象。 - 别忘了
@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 对照表
| 概念 | Laravel | Spring 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 / DB | RabbitMQ / Kafka / Redis(spring-data-redis Stream) |
实践建议
- 先想清楚要哪种:
- 要「快、可丢、进程内」→
@Async - 要「可靠、跨实例、可重试」→ RabbitMQ / Kafka
- 要「超时自动完成」→ 任务调度(任务调度)
- 要「快、可丢、进程内」→
- 消息体别塞大对象:只放 ID 等轻量数据,消费端再去查(消息队列不是数据库)
- 幂等:消费端逻辑要能重复执行不产生副作用(记录处理过的 ID 或用数据库唯一约束)
- 监控:RabbitMQ 管理台看堆积;
@Async线程池记得配拒绝策略(CallerRunsPolicy)防队列满丢任务
关于 Kafka
Kafka 适合「海量日志 / 流式事件」,和 RabbitMQ 的「任务队列」定位不同。绝大多数业务「发个邮件、算个索引」用 RabbitMQ/Redis 队列就够,别为了「高级」上 Kafka。