消息队列
RabbitMQ、Kafka、MassTransit。
异步消息与事件驱动(对照 Spring AMQP)
这一课你将能做到
- 把「提交代码 → 判题」从同步 HTTP 改造为消息驱动的异步链路(削峰 + 解耦)
- 用 MassTransit 声明消息契约、发布事件、写消费者,并配置重试与死信
- 说清 at-least-once 语义下幂等消费的实现套路,对照 Spring 的
@RabbitListener体系
为什么要学
判题是慢操作(编译 + 沙箱执行可能几秒),如果提交接口同步等结果,高峰期线程池瞬间被占满、QPS 崩掉。把「提交」和「判题」拆开:接口只做「落库 + 发消息」立即返回 PENDING,判题 Worker 从队列消费慢慢干——这就是削峰;判题引擎换语言实现、扩缩容,都不动 API 代码——这就是解耦。
Java 侧这套是 RabbitMQ + @RabbitTemplate/@RabbitListener;.NET 侧的主流抽象是 MassTransit(类库级的消息总线,类似 Spring Cloud Stream 的定位),底层接 RabbitMQ/Azure Service Bus/Kafka。
先建立图像
用户提交 API 实例 RabbitMQ 判题 Worker
│ POST /submissions │ │ │
│ ────────────────────→ 落库(PENDING) │ │
│ ←─ 201 PENDING ────────│ ── publish ─────────→ [judge.queue] │
│ │ │ ── consume ───────→ 沙箱判题
│ │ │ ←─ ack ──────────── 更新DB
│ ←══ SignalR 推送 AC ══════════════════════════════════════════════════│
交付语义:RabbitMQ 默认 at-least-once——Worker 处理完才 ack,处理到一半宕机消息会重投。所以消费逻辑必须幂等(同一提交判两次不产生副作用)。
跟着做
步骤 1:定义消息契约
dotnet add package MassTransit.RabbitMQ
// 独立 Contracts 项目,发布方与消费方共享(版本演进靠加字段不靠改字段)
public record SubmissionSubmitted(long SubmissionId, long ProblemId, string Language, string Code);
public record SubmissionJudged(long SubmissionId, string Status, int TimeMs, string UserName);
步骤 2:API 侧发布事件
// Program.cs
builder.Services.AddMassTransit(x =>
{
x.SetKebabCaseEndpointNameFormatter();
x.UsingRabbitMq((ctx, cfg) =>
{
cfg.Host("localhost", "/", h => { h.Username("guest"); h.Password("guest"); });
cfg.ConfigureEndpoints(ctx); // 自动按消费者创建交换机/队列
});
});
// 提交接口:落库 + publish,双成功才返回 201
api.MapPost("/", async (SubmitDto dto, OjDbContext db, IPublishBus bus) =>
{
var submission = new Submission { ProblemId = dto.ProblemId, Language = dto.Language, Status = "PENDING" };
db.Submissions.Add(submission);
await db.SaveChangesAsync();
await bus.Publish(new SubmissionSubmitted(submission.Id, dto.ProblemId, dto.Language, dto.Code));
return Results.Created($"/api/submissions/{submission.Id}", new { submission.Id, submission.Status });
});