架构风格:分层、六边形、CQRS 与事件驱动
这一课你将能做到
- 说出分层、六边形、CQRS、事件驱动四种架构风格的核心结构和适用场景
- 在判题平台里画出四层分层架构图,明确每层能调谁、不能调谁
- 判断"读写分离"和"事件总线"什么时候适合用,什么时候不适合
为什么要学
一个判题平台刚开始可能只是个单体应用,但随着用户量增长,读多写少、判题异步、多终端接入,架构风格的选择决定了扩展路径。分层架构是入门必备,六边形让核心业务不依赖框架,CQRS 让查询性能飞起,事件驱动让异步流程解耦。本课给你一张架构地图,知道什么时候走哪条路。
先建立图像
分层架构 六边形架构(端口适配器)
────────── ──────────────────────
Controller 外部 HTTP/API 适配器
↓ 核心业务(领域)
Service 外部 DB/MQ 适配器
↓
Repository
↓
Database
CQRS 事件驱动
────── ────────
写模型: Submission SubmissionSubmitted → EventBus
↓保存 ↓
读模型: SubmissionView JudgeEngine 消费 → 判题
↓查询 ↓
Dashboard JudgeResultEvent → 通知/更新
跟着做
步骤 1:四层分层架构——判题平台版
┌──────────────┐
│ 用户界面层 │ Controller、Filter、WebSocketHandler
├──────────────┤
│ 应用层 │ Service、CommandHandler、事件发布
├──────────────┤
│ 领域层 │ Entity、ValueObject、DomainService
├──────────────┤
│ 基础设施层 │ RepositoryImpl、MessageQueue、外部API
└──────────────┘
规则:上层可调用下层,同层可调用,下层不能调上层。领域层是核心,不依赖 Spring、不依赖 MyBatis。
// 领域层:纯 POJO,不依赖框架
public class Submission {
private SubmissionId id;
private UserId userId;
private LessonId lessonId;
private CodeSnippet code;
private SubmissionStatus status;
public void judge(JudgeResult result) {
this.status = result.isAccepted() ? ACCEPTED : REJECTED;
}
}
// 基础设施层:依赖领域层,实现 Repository 接口
@Repository
public class MybatisSubmissionRepository implements SubmissionRepository {
private final SubmissionMapper mapper;
@Override
public void save(Submission submission) { /* 转换并写入 DB */ }
}
步骤 2:六边形架构——端口与适配器
六边形也叫"端口适配器",核心思想:业务逻辑不依赖外部世界,外部世界通过"端口"接入。
// 输入端口(驱动端口)
public interface SubmitUseCase {
SubmissionId handle(SubmitCommand command);
}
// 输出端口(被驱动端口)
public interface SubmissionRepository {
void save(Submission submission);
Optional<Submission> findById(SubmissionId id);
}
// 核心业务在六边形内部
@Service
public class SubmitService implements SubmitUseCase {
private final SubmissionRepository repository;
public SubmitService(SubmissionRepository repository) {
this.repository = repository;
}
public SubmissionId handle(SubmitCommand command) {
Submission submission = Submission.create(command);
repository.save(submission);
return submission.getId();
}
}
// HTTP 适配器(六边形外部)
@RestController
public class SubmissionController {
private final SubmitUseCase submitUseCase; // 依赖端口,不依赖实现
@PostMapping("/submissions")
public ResponseEntity<SubmissionId> submit(@RequestBody SubmitRequest req) {
return ResponseEntity.ok(submitUseCase.handle(req.toCommand()));
}
}
换 WebSocket 入口?新建一个 WebSocketSubmitAdapter 实现 SubmitUseCase 端口,核心业务不动。换存储?新建一个 RedisSubmissionRepository 实现 SubmissionRepository 端口。
步骤 3:CQRS——读写分离
判题平台里,写(提交代码)和读(查提交列表、排行榜)的模型完全不同。
// 写模型:命令
@Service
public class SubmitService {
public SubmissionId submit(SubmitCommand cmd) {
Submission s = Submission.create(cmd);
repository.save(s);
eventBus.publish(new SubmissionSubmitted(s.getId(), s.getUserId(), s.getLessonId()));
return s.getId();
}
}
// 读模型:专门为查询优化
@Service
public class SubmissionQueryService {
public List<SubmissionView> listByLesson(long lessonId, int page, int size) {
return submissionViewMapper.selectByLesson(lessonId, page, size); // 宽表/ES
}
public LeaderboardView getLeaderboard(long lessonId) {
return redisLeaderboard.get(lessonId); // Redis 排序集合
}
}
写走 MySQL(事务、一致),读走 Redis/ES/宽表(性能、聚合)。两者通过领域事件同步。
步骤 4:事件驱动——异步判题流
// 提交完成事件
public record SubmissionSubmitted(SubmissionId submissionId, UserId userId, LessonId lessonId) {}
// 判题完成事件
public record JudgeFinished(SubmissionId submissionId, JudgeResult result) {}
@Service
public class JudgeEventListener {
@EventListener
public void onSubmissionSubmitted(SubmissionSubmitted event) {
judgeEngine.judge(event.submissionId());
}
@EventListener
public void onJudgeFinished(JudgeFinished event) {
submissionRepository.findById(event.submissionId())
.ifPresent(s -> {
s.judge(event.result());
submissionRepository.save(s);
});
}
}
提交→事件→判题→事件→更新,全程异步,主线程不阻塞。
讲清楚要点
- 分层架构:经典四层(UI/应用/领域/基础设施),适合 CRUD 项目,入门友好。
- 六边形架构:核心不依赖外部,通过端口适配器接入,适合测试驱动、框架迁移。
- CQRS:命令和查询模型分离,适合读多写少、查询模型复杂(排行榜、报表)。
- 事件驱动:通过事件解耦服务,适合异步流程、削峰填谷、跨服务协作。
- 风格可以混合:判题平台单体阶段用分层 + 事件驱动;微服务阶段用六边形 + CQRS。
你可能会懵的地方
- 分层和六边形什么区别? 分层是"上下关系",六边形是"内外关系"。分层关心 Controller→Service→Repository 的调用链;六边形关心"核心业务不依赖框架"。两者不冲突,可以六边形内部分层。
- CQRS 是不是必须两个数据库? 不是。小项目可以同库不同表;大项目才分 MySQL + Redis/ES。关键是"模型分离",不是"物理分离"。
- 事件驱动怎么保证最终一致? 用本地事务表 + 可靠投递(Outbox),或直接用 MQ 事务消息。下一课会详细讲 Saga 和 Outbox。
- 事件驱动会不会导致代码难追踪? 会。需要给事件加 traceId,链路追踪工具(如 Sleuth + Zipkin)要跟上。事件流图要文档化。
- 初学者该用哪种风格? 先用分层架构把关注点分离做好;业务复杂了再上六边形;读多写少再上 CQRS;异步多了再上事件驱动。不要一步到位。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 所有代码都在 Controller | 没有分层 | 至少拆出 Service 和 Repository |
| Service 直接 new RedisTemplate | 依赖具体框架 | 六边形:抽象 CacheRepository,实现放基础设施层 |
| 写模型里塞查询逻辑 | 模型职责混乱 | CQRS:写用 Command,查用 Query |
| 所有调用都改成异步事件 | 调试困难,一致难保证 | 只在需要解耦/削峰的地方用事件 |
| 六边形里直接依赖 Spring | 违背核心独立原则 | 领域层用纯 POJO,Spring 注解只在应用层 |
卡点急救
- 不知道分层该分几层:先四层(UI/应用/领域/基础设施),业务简单可以 UI 和应用合并,但领域层必须独立。
- 六边形里的端口不知道怎么抽:先写一个纯 POJO 的 Service,问自己"它需要什么外部能力?"每个能力就是一个端口。
- CQRS 的事件同步怎么落地:先写本地事务表(Outbox),定时任务扫描并投递 MQ,保证最终一致。
面试常问
- 分层架构和六边形架构的区别? 答:分层是上下调用关系,六边形是内外依赖反转。分层里底层可能被框架绑定;六边形要求核心业务不依赖任何框架,通过端口接入。两者可结合:六边形内部分层。
- CQRS 什么时候用? 答:读多写少、查询模型复杂且与写入模型差异大(如排行榜、报表、搜索)。判题平台里提交代码是写,查排行榜是读,两者模型不同,适合 CQRS。
- 事件驱动的优缺点? 答:优点是解耦、异步、削峰;缺点是调试难、一致性难保证。适合跨服务协作、非关键路径。判题异步执行适合事件驱动,但用户登录不适合。
自测三问
- 判题平台为什么适合用事件驱动?如果不异步,会有什么问题?
- 六边形架构里,核心业务依赖什么?外部框架怎么接入?
- CQRS 写模型和读模型如果不同步,应该怎么保证最终一致性?
小结一句话
分层是基础,六边形让核心独立,CQRS 让查询飞起,事件驱动让流程解耦;风格是地图,不是终点。
延伸
- 《实现领域驱动设计》Vaughn Vernon,第 2/3 章
- 《架构整洁之道》Robert C. Martin,第 15-17 章
- CQRS 模式 - Microsoft Azure
- 下一课:架构模式——Saga、Outbox、Strangler Fig。