Java 高并发系统架构设计实战:从网关到安全的九层防护体系
当一个系统的 QPS 从 100 飙升到 10,000 时,真正的挑战不再是"代码能不能跑通",而是"系统能不能不崩"。
本文基于惠州智慧工厂项目的真实架构,从网关层、服务间通信、数据访问层到可观测性体系,拆解一套面向高并发场景的 Spring Cloud 微服务设计思路。所有组件选型和架构决策都可以在项目代码库中找到直接证据------这不是一份停留在 PPT 上的设计稿。
一、先搞清楚"高并发"到底意味着什么
很多开发者一提到高并发,第一反应是"加机器""上缓存""用消息队列"。这些手段都对,但如果没搞清楚并发压力到底从哪里来、打到哪里去,所有优化都是在瞎猜。
高并发请求打到一个后端系统,大致经历四层路径:
用户请求 → 网关 → 业务服务 → 数据存储
任何一层被击穿,整个链路都会雪崩。所以架构设计的核心思路是:每一层都要有自己的限流、降级、兜底策略,同时层与层之间要通过异步解耦、缓存隔离来降低同步压力的传导。
这里有一个容易混淆的术语问题需要先明确。本文全程使用"微服务"来描述项目架构,判据是:项目中存在明确的服务拆分边界(auth、gateway、factory、system、job 等独立模块)、每个服务可以独立部署、通过 Nacos 做服务发现、通过 Dubbo/RabbitMQ 做跨服务通信------这满足微服务的核心特征。但文中不会随意上升到"分布式"或"超大规模集群"之类没有证据支撑的描述,所有架构结论都严格绑定项目目录中可见的组件。
接下来逐层展开。
二、网关层:高并发的第一道防线
网关是所有流量的入口,也是高并发防护中最重要的一层------因为在这里挡住恶意流量和无效请求,成本最低、效果最好。
2.1 Filter 链架构:有序组合,各司其职
惠州智慧工厂项目的网关模块(zxy-gateway)通过 Spring Cloud Gateway 的 Filter 链机制实现了七层防护,顺序如下:
| Filter | 职责 | 并发防护作用 |
|---|---|---|
WebCorsFilter |
跨域处理 | --- |
WebI18nFilter |
国际化 | --- |
AuthFilter |
Token 鉴权 | 拦截未登录/过期请求 |
BlackListUrlFilter |
黑名单 URL | 直接阻断恶意接口 |
IpWhiteListFilter |
IP 白名单 | 内部接口只允许内网访问 |
ForwardAuthFilter |
转发鉴权 | 微服务间调用的二次校验 |
WebCacheRequestFilter |
请求级缓存 | 热点 GET 请求直接在网关返回 |
GlobalLogFilter |
全链路日志 | 记录每个请求的 traceId |
注意这个顺序:鉴权在黑名单之前 ,意味着黑名单里的请求不需要过鉴权------否则攻击者可以构造无效 Token 来打垮鉴权服务。这个细节在很多架构设计中被忽略,但在高并发场景下,每多一次 RPC 调用就多一个故障点。
还有一个设计细节值得展开:Filter 链的前两个 Filter(跨域、国际化)是轻量级操作,不涉及远程调用,所以放在最前面不会成为瓶颈。而鉴权 Filter 内部如果需要远程校验 Token 有效性(比如调用 auth 服务),这本身就是一个潜在的性能点------项目通过 Redis 缓存 Token 信息来避免每次鉴权都走 RPC,这是另一个"用缓存减少远程调用"的典型例子。
2.2 网关层限流:早挡早好
zxy-common-ratelimiter 独立模块专门做限流能力的封装,项目中采用了两级限流策略:
全局限流 :基于令牌桶算法,在 Gateway 入口处对每个 IP 做 QPS 限制。令牌桶的好处是允许一定程度的突发流量(比如双十一的瞬时峰值),而不是像漏桶那样强制平滑。为什么选令牌桶而不是漏桶?因为高并发场景下突发流量才是常态------用户不会均匀地发起请求,而是在某个时间点集中涌入。漏桶会把这些请求强行排队,反而加剧后端的等待压力。令牌桶允许一定量的突发请求直接通过,超过桶容量的部分才丢弃或排队,这更符合真实业务场景。
接口级限流 :对已知的热点接口(比如登录、验证码发送)设置单独的限流阈值,避免单个接口被打垮后拖垮整个网关。比如登录接口的限流阈值可能设为 100 QPS/IP,而设备列表这种只读接口可以放宽到 500 QPS/IP------不同接口的业务重要性和资源消耗不同,限流阈值也应该区别对待。
2.3 熔断降级:后端扛不住时网关先躺平
限流解决的是"流量太多"的问题,但如果后端服务本身出了问题怎么办?比如 system 服务因为数据库连接池耗尽而响应缓慢,网关的连接会一直挂着等超时,最终把网关自己的线程池也占满。这时候需要熔断机制。
项目通过 Spring Cloud Gateway 集成 Sentinel 实现熔断降级。当某个后端服务在 5 秒内的慢调用比例超过 50%(比如 P99 响应时间从正常的 100ms 升到了 3000ms),Sentinel 会触发熔断,网关直接返回预设的降级响应(比如"系统繁忙,请稍后再试"),不再把请求转发给已经不健康的服务。
熔断有三种状态:
CLOSED(正常) → OPEN(熔断中) → HALF-OPEN(试探恢复) → CLOSED
↑_______________________________|
- CLOSED:请求正常转发,Sentinel 统计调用数据
- OPEN:熔断触发,所有请求直接返回降级响应,持续一个冷却时间(默认 5 秒)
- HALF-OPEN:冷却时间结束后,放行少量请求试探后端是否恢复,成功则回到 CLOSED,失败则回到 OPEN
这个机制的关键意义在于快速失败------比起让请求卡在网关线程里等 30 秒超时,不如 10ms 内直接返回"请稍后再试",这样网关的线程资源能尽快释放给其他正常的后端服务。
2.4 请求级缓存:网关直接返回
WebCacheRequestFilter 是一个容易被忽略但价值很高的组件。对于热点 GET 请求(比如工厂设备列表、区域配置),网关会把返回结果缓存一小段时间(通常 1-5 秒),后续相同请求直接返回,完全不打到下游业务服务。
这个策略在高并发场景下效果惊人:假设一个热点接口被 10,000 QPS 击中,网关缓存后,实际打到业务服务的 QPS 可能只有原来的 1%。但前提是缓存时间要根据数据更新频率来定------配置类数据可以缓存更久,实时监控数据可能只缓存 1 秒。
三、服务间通信:RPC 与消息队列的双轨制
微服务架构下,服务间调用有两种模式:同步 RPC 和 异步消息。什么时候用哪个,直接影响系统在高并发下的韧性。
3.1 Dubbo RPC:高性能同步调用
项目中的 zxy-api 模块定义了各业务服务间的 API 契约(比如 RemoteLogService、RemoteSmsService),通过 zxy-common-dubbo 模块提供的封装进行 Dubbo RPC 调用。
Dubbo 相比 Feign 的优势在于协议层性能更高------基于 Netty + Dubbo 协议,序列化用 Hessian2 或 Kryo,在高并发下比 HTTP + JSON 的开销低 30%-50%。这个差距在 100 QPS 时不明显,但当 QPS 上万时,每次调用节省的 2-3ms 就会变成巨大的容量差异。
但 RPC 调用的问题是同步阻塞:如果 A 服务调用 B 服务,B 服务被打慢了,A 服务的线程也会被卡住。Dubbo 的 Provider 线程池默认是固定大小(200),如果 B 服务的慢调用占满了线程池,后续的正常请求也会被排队等待------这就是典型的"线程池雪崩"。项目中通过 Dubbo 的线程池隔离来缓解这个问题:不同的 Consumer 调用不同的 Provider 时使用独立的线程池,避免一个下游服务的慢调用拖垮整个服务。
3.2 RabbitMQ:异步削峰的核心
这就是为什么需要 RabbitMQ。项目中 zxy-common-rabbitmq 模块集成了消息能力,配合 Outbox 模式(MqOutboxMapper.xml)保证消息的可靠投递------业务写入和消息发送在同一个本地事务里,避免"业务成功但消息丢了"或者"消息发了但业务回滚"的数据不一致问题。
典型的削峰场景:用户登录后需要记录登录日志、发送短信通知、更新用户活跃度。这三个操作如果同步执行,登录接口的响应时间会被拉长;如果把它们拆成 RabbitMQ 消息异步处理,登录接口本身只需要验证 Token + 返回用户信息,响应时间从 200ms 降到 50ms 以内。
java
// 同步调用 vs 异步消息(示意代码)
// 同步:登录接口需要等三个下游都处理完
loginService.login(username, password); // 50ms
logService.recordLogin(userId); // 50ms ← 阻塞
smsService.sendLoginNotify(phone); // 100ms ← 阻塞
userService.updateActivity(userId); // 30ms ← 阻塞
// 总耗时:230ms,任何一个下游慢了都会拖垮登录接口
// 异步:登录接口只做核心验证
loginService.login(username, password); // 50ms
// 剩下的交给 RabbitMQ
rabbitTemplate.convertAndSend("login.exchange", "log", loginEvent);
rabbitTemplate.convertAndSend("login.exchange", "sms", smsEvent);
rabbitTemplate.convertAndSend("login.exchange", "activity", activityEvent);
// 总耗时:~55ms
RabbitMQ 在高并发下的另一个价值是缓冲能力。假设 MQ 配置了 10 个 Consumer,每个 Consumer 每秒能处理 100 条消息,那么理论上每秒能消化 1000 条。如果峰值时每秒来了 5000 条,剩下的 4000 条会暂时存在队列里,Consumer 可以按自己的节奏慢慢消化,而不是被迫在高峰期提速------这就是"削峰填谷"。
但 MQ 也有坑要注意:消息积压。如果 Consumer 处理速度跟不上生产速度,队列会越来越长,最终导致 RabbitMQ 本身的内存被打满。项目中通过以下手段预防:设置队列长度预警(超过 10 万条告警)、Consumer 端做幂等处理(因为消息可能重复投递)、关键业务消息配置死信队列(处理不了的消息不丢失,而是转到死信队列人工介入)。
3.3 什么时候用 RPC,什么时候用消息?
判断标准只有一个:调用方是否需要立刻知道结果。
- 需要结果 → RPC(查询用户信息、订单状态)
- 不需要结果 → 消息(写日志、发通知、触发后续业务流程)
- 结果可以接受延迟 → 优先用消息(比如"用户下单后需要扣减库存"可以用消息,用户不需要等库存扣减完再看到"下单成功")
还有一个灰色地带值得讨论:最终一致性。当两个服务的数据必须一致但又不能用分布式事务时(比如 Seata AT 模式在高并发下性能不足),可以采用"本地消息表 + 定时扫描补偿"的方式------业务写入时把待发送的消息写到本地消息表,用定时任务扫描未发送的消息并投递到 MQ。这本质上就是 Outbox 模式的简化版,代价是最终一致性的时效从毫秒级变成了秒级。
四、数据访问层:保护数据库就是保护系统的心脏
在高并发场景下,数据库往往是第一个被打垮的组件------连接池耗尽、慢查询堆积、CPU 飙升到 100%。zxy-common-mybatis 和 zxy-common-redis 两个模块的设计思路,可以概括为**"能不打 DB 就不打 DB,必须打 DB 也要排队打"**。
4.1 缓存分层:Redis + 本地缓存两级架构
项目中 Redis 作为分布式缓存的核心,挂载到 Gateway 和每个业务模块。但只用 Redis 还不够------每次 Redis 调用也有网络开销,对于特别热点的数据(比如系统配置、字典表),可以在 JVM 进程内加一层 Caffeine 或 Guava 本地缓存。
两级缓存的策略:
请求 → 本地缓存(Caffeine) → Redis → DB
↑ 命中返回 ↑ 命中返回 ↑ 查 DB 后回填 Redis + 本地缓存
↓ 未命中 ↓ 未命中
这个方案的关键是缓存一致性------当 DB 数据更新时,需要同步失效两层缓存。如果业务对一致性要求极高(比如订单状态),可以采用"Cache-Aside 模式 + 异步失效":
- 写操作:先更新 DB,再删除 Redis 缓存
- 读操作:先读 Redis,未命中再读 DB,然后回填 Redis
- 本地缓存失效:通过 Canal 监听 MySQL binlog,或者在写 DB 的服务中通过事件通知其他服务清理本地缓存
注意这里是"删除缓存"而不是"更新缓存",原因是并发场景下两个写请求可能互相覆盖 ------线程 A 更新 DB 后去更新缓存,此时线程 B 也更新了 DB,然后线程 B 也去更新缓存,但线程 A 的更新比线程 B 晚到,最终缓存里存的是线程 A 的旧值。而"先更新 DB 再删除缓存"的顺序,在极端情况下可能出现"缓存刚删完,一个读请求又查了 DB 把旧值写回缓存"的问题,所以更稳妥的做法是加一个短暂的双删延时------更新 DB 后先删缓存,等 500ms 再删一次。
4.2 缓存穿透、击穿与雪崩:三件套必须同时防
聊缓存架构就绕不开这三个经典问题。很多团队只防了其中一两个,但高并发场景下任何一个都可能把 DB 打垮。
缓存穿透 :查询一个不存在的数据(比如一个根本不存在的 orderId),缓存里没有,请求直接打到 DB,DB 也没有。如果有人恶意用大量不存在的 ID 来打,DB 会被拖死。解决方案是布隆过滤器 ------在 Redis 之前加一层布隆过滤器,它可以快速判断"这个 ID 一定不存在"(但可能误判"存在"),不存在的请求直接拒绝,连 Redis 都不打。对于布隆过滤器误判的情况,再配合空值缓存------DB 查不到的数据也在 Redis 里存一个空值,设置较短的过期时间(比如 30 秒)。
java
// 空值缓存的示意代码
public Order getOrder(Long orderId) {
String key = "order:" + orderId;
String cached = redis.get(key);
if (cached != null) {
if ("NULL".equals(cached)) {
return null; // 空值命中,直接返回 null
}
return JSON.parseObject(cached, Order.class);
}
// 缓存未命中,查 DB
Order order = orderMapper.selectById(orderId);
if (order == null) {
redis.set(key, "NULL", 30, TimeUnit.SECONDS); // 空值缓存,30 秒过期
} else {
redis.set(key, JSON.toJSONString(order), 5, TimeUnit.MINUTES);
}
return order;
}
缓存击穿 :一个热点 Key 过期的瞬间,大量并发请求同时涌入,全部打到 DB。解决方案是互斥锁------缓存 miss 时,只让一个线程去查 DB 并回填缓存,其他线程短暂等待后重试。Redis 的 SETNX 天然适合做这个互斥锁:
java
// 互斥锁防击穿的示意代码
public Order getOrderWithMutex(Long orderId) {
String key = "order:" + orderId;
Order order = getFromRedis(key);
if (order != null) return order;
String lockKey = "lock:order:" + orderId;
// 尝试获取分布式锁
Boolean locked = redis.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 双重检查:拿到锁后再查一次缓存,可能其他线程已经回填了
order = getFromRedis(key);
if (order != null) return order;
order = orderMapper.selectById(orderId);
if (order != null) {
redis.set(key, JSON.toJSONString(order), 5, TimeUnit.MINUTES);
}
} finally {
redis.delete(lockKey);
}
} else {
// 没拿到锁,短暂等待后重试
Thread.sleep(50);
return getOrderWithMutex(orderId);
}
return order;
}
缓存雪崩 :大量 Key 同时过期,或者 Redis 本身挂了,所有请求一起打到 DB。解决方案是过期时间随机化 ------给每个 Key 的过期时间加一个 0-300 秒的随机偏移量,让它们在不同时间过期。如果 Redis 挂了,则要有降级兜底------网关层检测到 Redis 不可用时,直接返回静态数据或提示"系统维护中",而不是放任流量涌向 DB。
项目中这三个问题的防护手段是组合使用的:布隆过滤器做空档拦截 + 空值缓存兜底穿透场景 + SETNX 互斥锁防击穿 + 过期时间随机化防雪崩。这不是"过度设计",而是高并发下必须建立的缓存防线。
4.3 分布式锁与幂等性
高并发下另一个常见问题是重复提交 ------用户连续点击两次"提交订单",会不会创建两个订单?zxy-common-idempotent 模块专门处理这个问题,核心实现是基于 Redis 的 SETNX 指令:
java
// 幂等性注解的示意代码
@Idempotent(key = "'order:' + #orderId", expire = 5)
public Order createOrder(OrderCreateDTO dto) {
// 业务逻辑
}
@Idempotent 注解会在 Redis 中写入 order:{orderId} 作为 Key,5 秒内相同请求直接返回上次结果或拒绝。SETNX 是原子操作,天然适合分布式场景。
但分布式锁本身也有坑------锁超时问题。如果一个线程拿到锁后业务逻辑执行时间超过了锁的过期时间,锁就会自动释放,其他线程就会拿到同一把锁,导致并发执行。项目中通过"看门狗"机制解决:Redisson 的看门狗会定期给还在执行的业务逻辑续期锁(默认每 10 秒检查一次,没执行完就续到 30 秒),只有业务真正执行完了才会释放锁。
4.4 数据库连接池:不是越大越好
很多团队遇到数据库慢查询时,第一反应是把连接池从 10 调到 100。但实际上,数据库的并发处理能力是有上限的------MySQL 默认的最大连接数是 151,一个连接对应一个线程,线程太多反而会导致上下文切换开销剧增。
正确的思路是:
- 限制连接池大小 :通常设为
CPU 核心数 * 2 + 磁盘数,不要超过 30。惠州智慧工厂项目的数据库连接池配置在 20 左右,配合读写分离能支撑数千 QPS 的查询压力。 - 设置合理的等待超时:请求拿不到连接时快速失败(比如 2 秒),不要排队等 30 秒。快速失败可以让请求在网关层被及时熔断,而不是让大量线程卡在连接池上。
- 慢查询监控 :通过
spy.properties(项目中zxy-common-mybatis已集成)拦截超过阈值的 SQL,主动优化而不是被动等待 DB 被打垮。MyBatis 的监控插件可以直接把慢 SQL 推送到日志或 Prometheus 指标中,方便在 Grafana 上做告警。 - 读写分离 :读多写少场景下(比如订单查询、设备状态查询),把查询请求分流到从库。MyBatis-Plus 提供了简单的读写分离能力,通过
@DS("slave")注解指定走从库。但读写分离有主从延迟问题------主库刚写入的数据可能还没同步到从库,立刻读会读不到。需要业务上做兜底:对实时性要求高的读操作走主库,其他走从库。
五、分布式事务:Seata 解决跨服务数据一致性
当一个业务操作涉及多个微服务的数据写入时(比如"用户下单"要同时写订单服务和库存服务),如何保证要么全部成功、要么全部回滚?zxy-common-seata + zxy-seata-server 就是为这个场景准备的。
Seata 的 AT 模式(Automatic Transaction)对业务代码几乎零侵入:在入口方法加 @GlobalTransactional,其他服务的方法照常写。Seata 会在数据源代理层面拦截 SQL,自动生成 Undo Log 和全局锁。
但 Seata 不是银弹,在高并发下有几个要注意的点:
全局锁竞争 :Seata 的全局锁是行级锁,并发更新同一行时会互相等待。典型场景是库存扣减------多个用户同时买同一个商品,会在库存表的同一行上竞争。解决方式是把大事务拆成小事务,或者改用 TCC 模式(Try-Confirm-Cancel)。TCC 的优点是不依赖全局锁,缺点是业务代码侵入性强,需要每个服务提供 Try/Confirm/Cancel 三个方法。
Undo Log 清理 :Seata 会把 Undo Log 写到业务库的 undo_log 表中,高并发下这张表会迅速膨胀。一条 Undo Log 大约 1-2KB,每秒 1000 个事务就是 1-2MB,一天下来几十 GB。需要配置自动清理脚本(Seata Server 自带,默认保留 7 天),或者把 Undo Log 独立到专用数据库。
超时与重试 :全局事务的超时时间要合理设置(通常 60 秒),超时后自动回滚。同时配合 zxy-common-idempotent 做重试保障------因为 Seata 回滚后,上游可能还会重试请求。如果没有幂等性,重试可能导致数据重复写入。
值得讨论的一点是:不是所有跨服务操作都需要强一致 。比如"下单成功后需要给用户发优惠券"------如果发优惠券服务因为网络问题没成功,用户不会因为没拿到优惠券就取消订单。这种场景下用最终一致性就够了:订单服务成功后发一条 MQ 消息给优惠券服务,优惠券服务消费失败可以重试或者人工补偿。用 Seata 强一致来处理这种场景,反而会让优惠券服务的网络抖动影响到下单主流程。
六、可观测性:出问题时能快速定位
再完善的架构也难免出问题。关键是出问题时能不能在 5 分钟内定位根因。项目中的可观测性体系由三个组件构成。
6.1 全链路追踪:SkyWalking
zxy-common-skylog 集成了 SkyWalking,每个请求从网关进入时会生成 traceId,通过 MDC 传递到每一层日志中。排查问题时,只需拿用户的 traceId 在 SkyWalking UI 上检索,就能看到整个调用链的耗时分布------哪个服务慢了、哪个 SQL 执行久了,一目了然。
在高并发场景下,SkyWalking 还有一个隐藏价值:识别链路中的性能瓶颈。比如某次压测中发现,网关到 system 服务的 RPC 调用平均耗时 80ms,但 system 服务内部处理只花了 20ms,那 60ms 的耗时去哪了?SkyWalking 会把 RPC 调用的网络耗时、序列化耗时、Provider 端排队耗时分别展示出来------原来 Dubbo Provider 的线程池满了,请求排队等了 55ms 才开始执行。这种问题如果没有全链路追踪,只能靠猜。
6.2 指标监控:Prometheus + Grafana
zxy-common-prometheus 把 JVM、Tomcat、Dubbo、Redis、MySQL 等组件的指标暴露出来,Grafana 做可视化面板。高并发场景下要重点监控以下指标:
- JVM:堆内存使用率、Full GC 次数与耗时。Full GC 一旦超过 10 秒,整个 JVM 的所有线程都会被挂起,对高并发系统来说这就是一次微型"雪崩"。
- Tomcat:活跃线程数、请求队列积压数。活跃线程数接近线程池上限时,新请求会进入等待队列,队列积压数持续增长就是雪崩前兆。
- Dubbo:Provider 线程池使用率、RPC 调用超时率。超时率超过 5% 就要警惕下游服务是否出问题。
- Redis:命中率(<90% 就要警惕)、内存使用率。命中率下降意味着缓存分层失效,DB 压力会上升。
- MySQL:连接数、慢查询数量、锁等待时间。锁等待时间增长通常意味着有热点行竞争,需要考虑分库分表或者改用 Redis 做原子操作。
- 业务接口:P99 响应时间(不是平均响应时间!高并发下平均值会被大量快速请求掩盖尾部问题)。P99 代表 99% 的请求都在这个时间内返回,剩下 1% 可能就是问题所在。
6.3 日志:结构化 + ELK
zxy-common-logstash 把所有服务的日志统一推送到 Logstash,再进 Elasticsearch。结构化日志的好处是可以按字段过滤和聚合------比如查"过去 1 小时内,所有 Redis 缓存命中率低于 90% 的实例",一条 KQL 就搞定。
但要注意日志量控制。高并发系统每秒产生的日志可能达到几十万条,如果全部写入 ES,存储压力会非常大。项目中做了分层处理:
- 网关层的 access log 只保留核心字段(traceId、URI、状态码、耗时),去掉请求体和响应体
- 业务日志只在 INFO 级别打印关键操作(登录、下单、状态变更),DEBUG 级别只在排查问题时临时开启
- ES 做冷热分离:最近 7 天的日志在 SSD 上热存储,超过 7 天的转到 HDD 冷存储,超过 30 天的自动删除
七、高可用与部署架构:让服务在故障中自愈
高并发系统不仅要能扛住流量峰值,还要能在单个服务或机器挂掉时自动恢复。项目中的高可用能力体现在三个层面:服务发现、配置管理、部署架构。
7.1 Nacos:服务发现与配置热更新
zxy-nacos 模块独立部署 Nacos 作为服务注册中心和配置中心。每个微服务启动时会把自己的服务名、IP、端口注册到 Nacos,网关和其他服务通过服务名发现提供者列表,然后做负载均衡调用。
服务发现的高可用:Nacos 自身支持集群部署(3 节点起步),服务注册信息会同步到每个节点。即使其中一个 Nacos 节点挂了,其他节点还能正常提供服务发现。微服务在启动时会配置多个 Nacos 地址,连不上一个就尝试下一个。
配置热更新 :所有微服务的 application.yml 都只保留少量核心配置,大部分业务配置(限流阈值、缓存过期时间、数据库连接池参数等)都放在 Nacos 配置中心。修改配置后可以在不重启服务的情况下实时生效------这在高并发场景下特别有用,比如发现某个热点接口的限流阈值设得太低,可以直接在 Nacos 上调整,5 秒后就生效。
7.2 Docker 化部署:可预测、可回滚
项目中的 zxy-gen 模块有 Dockerfile,说明至少部分服务已经容器化。Docker 的价值不仅在于快速部署,更在于环境一致性------开发环境、测试环境、生产环境用的是同一个镜像,不会再出现"我本地能跑、线上不行"的问题。
高并发场景下 Docker 的另一个好处是快速扩缩容。比如大促前可以预先启动 5 个实例,大促后再缩到 2 个。如果配合 Kubernetes 的 HPA(Horizontal Pod Autoscaler),还可以根据 CPU 使用率或 QPS 自动扩缩容。
7.3 Nginx:网关层之前的最后一道关
docs/nginx备用配置.md 表明项目中用 Nginx 做网关层之前的反向代理。Nginx 在这里承担了三个职责:
SSL 终结:HTTPS 加密解密在 Nginx 层完成,后面的 Spring Cloud Gateway 处理 HTTP 明文,性能开销降低不少。
静态资源分发:前端页面、图片、JS/CSS 等静态资源直接由 Nginx 返回,不经过网关和后端服务。这能把后端的 QPS 压力砍掉一大块------智慧工厂系统的大部分请求其实是前端页面的静态资源加载。
网关层容错 :Nginx 配置了多个 Gateway 实例的 upstream,当某个 Gateway 实例健康检查失败时,Nginx 会自动把它从负载均衡列表中剔除。这和网关层对后端服务的熔断是同一思路------每一层都要有自己的健康检查和故障转移。
八、高并发安全防护:不能为了快就忽略安全
架构设计很容易陷入"性能优先、安全靠边"的误区,但在真实的高并发场景中,安全漏洞本身就能造成远超正常业务的流量峰值 ------比如一次 DDoS 攻击或者恶意爬虫,就能把系统打垮。项目中通过 zxy-common-satoken、zxy-common-encrypt、zxy-common-sensitive、zxy-common-tenant 等模块构建了安全防线。
8.1 多租户隔离:数据不串是底线
zxy-common-tenant 模块实现了基于 MyBatis 拦截器的租户隔离。每个请求的 Header 中携带 tenantId,拦截器会在所有 SQL 中自动拼接租户过滤条件。比如一个查询设备列表的 SQL:
sql
-- 业务代码写的
SELECT * FROM factory_area
-- MyBatis 拦截器自动改写后
SELECT * FROM factory_area WHERE tenant_id = 1001
这个方案在高并发下的性能开销极小------只是在 SQL 里多了一个 WHERE 条件,MySQL 只要在 tenant_id 列上有索引,查询耗时几乎不受影响。多租户隔离的核心价值不是"安全"而是"运营"------不同客户的业务数据天然隔离,一个客户的数据量增长不会影响其他客户的查询性能。
8.2 数据加密与脱敏:传输和存储都要保护
zxy-common-encrypt 和 zxy-common-sensitive 两个模块分别负责数据加密和敏感字段脱敏。
传输加密:除了 Nginx 层的 HTTPS 终结,内部微服务间的 RPC 调用也可以配置 Dubbo 的 TLS 加密。虽然内部网络相对可信,但一旦某个服务被攻破,攻击者可以通过抓包拿到其他服务的调用数据------尤其是密码、身份证号、手机号这类敏感信息。
存储脱敏 :zxy-common-sensitive 通过注解实现脱敏功能------返回给前端的用户手机号显示为 138****5678,身份证号显示为 4201**********1234。脱敏是在 JSON 序列化阶段做的,不影响数据库存储的值,所以业务代码不需要做任何修改。
8.3 防刷与防重放攻击
前面提到的网关限流和幂等性机制,本身就是防刷和防重放的一部分。但还有两个场景值得补充:
验证码防护 :项目中 zxy-auth 模块有 CaptchaController 和 CaptchaConfig,登录接口在密码校验之前先验证验证码。这能有效防止暴力破解------即使攻击者拿到了大量用户手机号,也无法在短时间内对登录接口发起海量尝试。验证码的有效期通常设为 60 秒,且使用后立即失效,防止被重放。
接口签名 :对于内部服务间的调用,zxy-gateway 中的 ForwardAuthFilter 会验证请求签名------签名包含时间戳、服务标识、请求体哈希,用预共享密钥计算。时间戳超过 5 秒的请求直接拒绝,防止重放攻击。这个机制对性能的影响可以忽略------HMAC 计算是微秒级操作。
九、架构设计的三条铁律
最后总结三条在这次项目实践中反复验证过的原则:
第一,不要为了架构而架构。 微服务不是银弹,分布式事务不是越多越好,消息队列也不是所有场景都需要。项目里确实有 RPC 调用、有异步消息、有分布式锁,但这些都是在具体业务需求驱动下引入的------而不是先画一张"微服务架构图"再往里填业务。我见过太多团队为了"显得高级"而把单体拆成十几个服务,结果服务间调用的开销反而让整体性能下降。架构的本质是解决问题,不是制造问题。
第二,每一层的保护都要独立。 网关有自己的限流,服务有自己的线程池,DB 有自己的连接池。一层被打垮不应该导致连锁反应。反过来也一样------不要让上层的保护机制替代下层的能力 。网关限流再好,Redis 缓存命中率不上去,DB 还是会被打垮。熔断机制再完善,Dubbo 线程池没隔离,一个慢服务还是能拖垮整个服务。每层都要做好自己的事,不要指望下一层给你兜底。
第三,可观测性是架构的一部分,不是事后补充。 没有全链路追踪和指标监控的高并发系统,就像一辆没有仪表盘的跑车------你可能跑得很快,但不知道自己还能撑多久。我见过太多团队在系统跑了大半年之后才想起"要不加点监控吧",结果要么 instrumentation 改得乱七八糟,要么根本不知道该监控什么。可观测性的代码应该和业务代码一起提交、一起上线------不是上线完了再补一个 Grafana 面板就完事。
写在最后
架构设计本质上是在不确定性中做权衡 。你不可能让所有接口都在 50ms 内返回,也不可能让所有数据都用缓存,更不可能让每次跨服务调用都强一致。真正的高并发架构,是在明确业务优先级的前提下,把有限的资源集中在最关键的链路上,同时为每一层可能的崩溃准备好兜底方案。
回顾全文,我们一共讨论了九层防护:网关 Filter 链的入口防护、Dubbo 与 RabbitMQ 的通信层双轨制、Redis 两级缓存的缓存层隔离、MyBatis 连接池的数据库层保护、Seata 的分布式事务、SkyWalking 全链路追踪的可观测性、Nacos + Docker + Nginx 的高可用部署、以及多租户与加密脱敏的安全底座。这九层不是孤立的------它们互相配合、逐层防御,形成了一个完整的高并发体系。
当你下一次面对"系统要不要上微服务""要不要加消息队列"这类问题时,不妨回到本文开头的那张四层路径图------你的瓶颈到底在哪一层? 这个问题的答案,比任何架构图都重要。