很多实时产品在后端阶段会同时撞上四件事:HTTP API、长连接、缓存/消息、关系型权威状态。若边界划不清,常见结局是:缓存与数据库双写不一致、WebSocket 连接散落在各实例上无法扇出、故障时只会重启进程而说不清哪一层坏了。
下文按「可部署单元内的模块边界 → 数据与一致性 → 实时通道 → 运维与演进」组织,讨论一套在中小规模生产环境仍站得住的组合:Spring Boot 承载业务与接入,MySQL 作真相源,Redis 作低延迟协调层。不鼓吹微服务拆分,但会为后续拆分留下接缝。
1. 先画清边界:什么进单体,什么必须外置
1.1 推荐的分层(逻辑模块,而非文件夹堆砌)
| 层 | 职责 | 禁止做的事 | |----|------|------------| | 接入层 | REST/gRPC、WebSocket 握手、鉴权、限流、DTO 校验 | 直接写复杂 SQL 或操作 Redis 细节 | | 应用层 | 用例编排、事务边界、领域规则 | 感知 HTTP 状态码或 WS 帧格式 | | 领域层 | 实体、聚合、领域事件(纯 Java) | 依赖 Spring、JPA、Redis 客户端 | | 基础设施层 | Repository 实现、消息发布、外部 API 适配 | 承载业务规则 |
Spring Boot 的价值在于:用同一进程把接入与应用跑通,同时用包结构与接口把「以后可拆成独立服务」的接缝留住。过早拆成五个微服务,往往只是把网络调用的不确定性提前引入。
1.2 何时仍用「模块化单体」
满足以下多数条件时,单体 + 清晰模块通常优于微服务:
- 团队规模小,跨服务联调成本高于代码耦合成本;
- 峰值 QPS 在单机垂直扩展 + 读写分离可覆盖范围;
- 领域边界尚未稳定,每周仍在改核心用例。
拆分信号往往是:某条链路需要独立扩缩容(例如 WS 网关)、或某子域发布节奏与主 API 完全脱节——而不是「代码行数超过 X 万行」。
2. MySQL:权威状态与一致性接缝
2.1 什么必须落库
凡涉及 钱、权限、审计、不可丢失的业务事实,应以 MySQL(或同等 ACID 存储)为 单一真相源(SSOT)。Redis 中的会话、在线状态、限流计数都是 派生状态,必须能由库内数据重建或容忍短期过期。
2.2 事务边界要「短而硬」
典型反模式:在 @Transactional 方法里调用外部 HTTP、发 Kafka、或 Thread.sleep。正确做法:
- 事务内只做 DB 读写;
- 提交后通过 领域事件 / Outbox 表 触发副作用。
Transactional Outbox 简述:业务表与 outbox 表同事务写入;独立轮询进程读取 outbox 并投递到 MQ 或调用下游。这样避免「库已提交、消息没发出」或相反的幽灵状态。
2.3 连接池与慢查询
- HikariCP:
maximumPoolSize不是越大越好,公式需结合 CPU 核数与查询类型(OLTP 常取cores * 2量级作起点,再压测); - 慢查询日志 +
EXPLAIN常态化;对热路径建立覆盖索引,避免 ORM 隐式 N+1(@EntityGraph/ fetch join 要有意识使用); - 读多写少场景:读写分离 或只读副本承接报表;写路径仍走主库,避免应用在副本上写。
3. Redis:不是缓存插件,而是协调层
3.1 按数据结构选型
| 场景 | 结构 | 注意点 |
|------|------|--------|
| 会话 / Token 元数据 | String + TTL | 密钥命名空间 session:{userId};轮换密钥时双写窗口 |
| 限流 | String INCR + EXPIRE 或 Redis Cell | 区分用户级、IP 级、全局限流;超限返回 429 与 Retry-After |
| 在线状态 / Presence | Hash 或 ZSET | 必须 TTL + 心跳续期;断线清理依赖过期而非只靠 disconnect 事件 |
| 跨实例 WS 扇出 | Pub/Sub 或 Streams | Pub/Sub 无持久化,订阅者离线即丢;Streams 可消费组,适合「至少一次」投递 |
| 分布式锁 | Redisson / SET NX PX | 锁必须带超时;临界区尽量短;能不用锁就不用(用 DB 乐观锁) |
3.2 缓存策略写进设计文档
- Cache-Aside:读 miss 回源 DB 再写入;写路径先更新 DB 再删缓存(或延迟双删防脏读);
- 禁止「默认缓存一切对象」:明确 哪些键、TTL、失效条件、是否允许击穿(布隆 / 单飞 singleflight);
- 热 key:本地 Caffeine 一级缓存 + Redis 二级;或大促前预热。
Presence 过期在 UI 上表现为「好友明明在线却显示离线」——这是产品缺陷,应归类为数据模型问题,而不是「缓存命中率低」。
4. WebSocket:接入模式与水平扩展
4.1 不要在业务线程里阻塞 IO
Spring 场景下常见组合:
- Spring WebFlux + Reactor Netty(全链路非阻塞),或
- Servlet 容器 + 独立 Netty/Socket.IO 网关进程,与 Spring MVC API 解耦。
若在 Tomcat 线程模型里塞满长连接,线程池会被占满,HTTP API 与 WS 互相拖死。
4.2 推荐网关职责
Client ──WS──► Gateway 实例 ──pub/sub──► Redis ◄──pub/sub── 其他 Gateway
│ ▲
└── 鉴权 / 游标 / 心跳 ──────────┘
Business API (Spring) ── 写 DB + 发布领域事件 ──► 推送到 Redis channel
- 连接建立:校验 JWT(建议短效 access + 刷新分离);绑定
userId、deviceId、协议版本; - 订阅模型:按
room:{id}或user:{id}频道订阅 Redis;业务服务不持有连接句柄; - 游标 / 增量:每条下行消息带
seq或revision;客户端 ACK 或上报lastSeq,重连时走 补拉 HTTP API 对齐缺口,而非让客户端猜; - 背压:每连接发送队列设上限;队列满则丢弃低优先级(正在输入 > 已读回执 > 消息体),并下发
degraded状态。
4.3 水平扩展与粘性
多实例部署时:
- 有状态连接 必须 sticky(负载均衡基于 cookie/ip hash),或
- 彻底 无状态网关(连接在任意实例,扇出只依赖 Redis),推荐后者。
滚动发布时:graceful shutdown——停止新连接、等待 DRAIN 周期、再杀进程,避免大量同时断线重连打穿 API。
5. 异步与集成:别把慢依赖绑在请求线程上
- @Async 仅适合非关键路径;关键链路用 MQ(RabbitMQ / Kafka)+ 消费幂等(业务键去重表);
- 调用 LLM / 第三方 HTTP:独立线程池 + 超时 + 熔断(Resilience4j);主 API 返回 202 + 任务 ID,结果通过 WS 或轮询推送;
- 定时任务(
@Scheduled)考虑 分布式调度(ShedLock / Quartz 集群),避免多实例重复执行。
6. 可观测性:要能回答「慢在哪一段」
6.1 三大信号
- Metrics:QPS、P99 延迟、池使用率(Hikari active/idle)、Redis 命中率、WS 连接数、每频道推送延迟;
- Logs:结构化 JSON,字段至少含
trace_id、span_id、userId(脱敏)、route; - Traces:OpenTelemetry 贯穿 HTTP → DB → Redis → 下游 HTTP;WS 网关单独 instrumentation。
HTTP 与 WS 必须共用 trace_id(握手时由客户端带 X-Trace-Id 或网关生成并下发),否则排障断链。
6.2 健康检查分层
| 探针 | 检查项 | |------|--------| | Liveness | 进程存活 | | Readiness | DB ping、Redis ping、下游依赖抽样 | | 深度检查(可选) | 只读查询、Redis 读写测试键 |
Readiness 失败应从负载均衡摘除,而不是反复重启 Pod。
7. 安全与配置
- 密钥 只走环境变量 / Secret 管理(K8s Secret、Vault),禁止入库;
- Redis、MySQL 启用 TLS(内网亦建议);最小权限账号(业务账号无 DDL);
- WebSocket 同样走 WSS;防范 跨站 WebSocket 劫持(校验 Origin);
- 限流 + 鉴权失败次数告警,防撞库与滥用。
8. 演进路径(仍保持务实)
- 阶段 A:模块化单体 + 单 Redis + 主从 MySQL;WS 网关可与 API 同进程或旁路进程;
- 阶段 B:WS 网关独立扩缩容;引入 Outbox + MQ;读副本承接查询;
- 阶段 C:按域拆服务(仅当指标与组织真的需要);引入服务网格处理 mTLS 与重试策略。
每一阶段都应有 回滚方案 与 容量压测报告(HTTP + WS 混合场景),而不是凭直觉加机器。
小结
现代服务端基础设施设计的核心不是「选了 Spring Boot 和 Redis」,而是:
- 真相源唯一,派生状态可重建或可控过期;
- 实时通道与业务解耦,扇出走协调层,连接状态不进业务库;
- 事务短、副作用异步、观测可串联;
- 先模块化单体,后为真实瓶颈拆分。
把上述接缝在设计阶段写进 ADR(Architecture Decision Record),比堆更多框架更能降低生产事故概率。