1. 别等线上炸了才懂:高并发治理是微服务绕不过去的坎
我有个习惯,接手一个新项目先不看代码,先看监控面板和告警记录。这两年经手的微服务项目多了,有个很深的感受:大多数系统不是被慢 SQL 拖垮的,也不是被 Bug 搞挂的,而是被“流量”打挂的。尤其是有过一次线上事故之后,你会对“高并发”这三个字有完全不一样的理解。
那次事故发生在一次秒杀活动。系统上线前压测看起来一切正常,QPS 峰值能做到 3000 左右,结果活动一开始,流量瞬间冲到 8000,服务直接卡死,紧接着数据库连接池被打满,Redis 报超时,MQ 堆积开始暴涨,最后整个集群雪崩。事后复盘的时候我们发现,问题并不是出在单点性能上,而是出在整个链路上没有任何一道“守卫”——没有限流,没有熔断,分布式锁用的姿势也不对,消息队列只管发不管消费。一顿操作下来,其实我们连 3000 QPS 的真实水平都没达到,系统就倒了。
这篇文章我想把这几年在高并发和微服务实战里踩过的坑、总结出来的套路,系统性地梳理一遍。包含分布式锁的正确姿势、消息队列削峰填谷的完整链路、限流熔断的落地避坑,还有一套可以直接抄作业的微服务高并发骨架。内容偏实战,不堆概念,适合正在做微服务改造、或者被线上高并发问题折磨过的后端工程师阅读。如果你是刚接触微服务的新人,也可以从头看,我会把关键原理用直白的话讲清楚。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高并发系统首先要回答的三个问题
2.1 流量进来之后,你的系统到底在怕什么
做高并发设计之前,先搞清楚系统面对流量暴增时到底在怕什么。我总结下来就三类:资源不够、数据不对、链路不稳。
资源不够,说的是 CPU、内存、连接池、带宽这些硬指标撑不住了。很多人以为高并发就是加机器,其实不对。加机器只能解决一部分计算压力,数据库连接池、文件存储、第三方接口这些瓶颈不是靠加机器就能绕过去的。数据不对,说的是并发场景下的数据竞争问题——库存超卖、账户扣错、订单状态错乱,这些本质上都是并发写操作没有做好互斥。链路不稳,说的是一个下游服务抖动,导致上游所有服务跟着一起等,等变成超时,超时变成重试,重试变成更大的流量,最后整个系统像多米诺骨牌一样倒掉。
这三个问题对应的解决手段,恰好就是文章标题里的分布式锁、消息队列、限流熔断。我经常跟团队说一句话:高并发治理不是做加法,而是做减法和防守。减法是把不必要的同步调用改成异步,防守是在每个入口、每个依赖边界上把流量管住。
2.2 从用户点击到下单成功,链路里每一环都可能是堵点
举个具体的例子。一个典型的用户下单流程是这样的:用户点击下单,请求先打到 Nginx,再进到网关,网关做路由和限流,然后把请求转发到订单服务,订单服务要校验库存,要扣减库存,要创建订单,要发消息给下游服务。这个流程里,每一个环节都有可能出现堵点。
Nginx 层是第一个堵点,连接数、带宽、worker 进程数都有限制。网关层容易堵在鉴权和转发上,如果过滤器里做了大量的同步 IO 操作,网关会先挂掉。订单服务最容易堵在数据库上,尤其是扣库存这一条 SQL,热点行更新会导致锁等待。消息队列看似是缓冲,但如果消费者消费速度跟不上生产速度,队列堆积越来越多,下游的服务会被延迟消息冲垮。
所以做高并发设计,思维上要先从“单点优化”切换成“链路治理”。你不光要考虑每个服务自己能扛多少 QPS,还要考虑服务和服务之间是怎么协作的,依赖关系是什么,故障是怎么传播的。这也是为什么现在微服务架构里特别强调流量治理这个概念——把限流、熔断、降级、隔离这些能力做进框架里,而不是靠开发人员在业务代码里自己写。
2.3 高并发治理的四板斧:分流、缓冲、互斥、兜底
我把高并发治理的常用手段总结成四板斧:分流、缓冲、互斥、兜底。
分流指的是把流量分散到多个节点、多个链路上去,比如负载均衡、读写分离、缓存挡在数据库前面。缓冲指的是用消息队列把突发的流量存下来,让下游按照自己的节奏去消费,削峰填谷就是这个意思。互斥指的是在分布式环境下保证同一份数据只能被一个线程修改,Redis 分布式锁就是干这个用的。兜底指的是当系统实在扛不住的时候,要有限流熔断降级这样的保护机制,让系统有尊严地失败,而不是整体崩溃。
这四个能力组合在一起,就是一个完整的高并发系统骨架。这篇文章后面每个部分,其实都是在讲这四个能力在实践当中怎么落地。我建议你在做架构设计的时候,也可以拿这四板斧去检查自己的系统,看看每个环节到底缺了哪一个。
3. 分布式锁:为什么说 synchronized 在微服务里不顶用了
3.1 单机锁和分布式锁的本质区别
很多新手会对分布式锁有一个误解,觉得它跟 synchronized 差不多,只是换了个实现方式。这个理解是错的。
synchronized 是 Java 虚拟机层面的锁,它的作用范围是单个 JVM 进程。你在单机部署的时候,多个线程访问同一个资源,synchronized 可以保证互斥。但是微服务部署之后,同一个服务通常会启动多个实例,每个实例是独立的 JVM 进程,线程锁就管不到其他实例上的线程了。
我画过一张图来解释这个事:假设一个库存服务部署了 3 个实例,用户请求被负载均衡分发到不同的实例上。如果每个实例各自用 synchronized 锁住扣减库存的方法,三个请求同时到达三个实例,三个实例可以同时执行扣减逻辑,库存就有可能会扣成负数。分布式锁要解决的是跨进程的互斥问题,它需要一个所有实例都能访问到的公共组件,Redis、ZooKeeper、数据库都可以充当这个角色,但实现难度和可靠性各不相同。
3.2 Redis SETNX 不是不能用,是坑太多
说到分布式锁,很多人第一时间想到 Redis 的 SETNX 命令。这个思路本身没错,但真正落地的时候,坑非常多。
最早期的写法是 SETNX lock_key value,如果返回 1,说明拿到了锁。拿到锁之后执行业务,最后 DEL 删除锁。这个写法有两个致命问题:第一,如果拿到锁的线程在执行业务的过程中挂了,锁永远不会被释放,其他线程永远拿不到锁;第二,如果 A 线程的锁超时过期了,B 线程拿到了新锁,A 线程执行完业务直接 DEL,把 B 的锁给删了,导致多个线程同时进入临界区。
后来官方推荐了原子命令 SET lock_key value NX PX 30000,解决了“设置锁和超时时间不是原子操作”的问题。但第二个问题仍然存在,就需要在删除锁之前判断 value 是不是自己设置的。比较靠谱的做法是使用 Lua 脚本,先比对 value 再删除,保证判断和删除两个操作是原子的。
code复制-- 释放锁的 Lua 脚本
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
这个脚本现在几乎成了分布式锁的标准删除方式。但我要提醒你,自己写 Redis 分布式锁,除了删除锁要小心,还面临一个更麻烦的问题:锁超时时间到底设置多长才合适。设置太短,业务还没执行完锁就过期了,出现并发问题;设置太长,万一进程真挂了,其他线程要等待很久才能拿到锁。这个矛盾在复杂的业务场景里很难平衡。
3.3 Redisson 看门狗机制:锁超时问题的实用解法
既然自己设置超时时间很纠结,业界的主流做法是用 Redisson 的分布式锁,它内置了一个叫“看门狗”(Watchdog)的机制来动态续期。
Redisson 加锁成功之后,默认会启动一个后台定时任务,每隔一段时间(默认是锁过期时间的 1/3,也就是 10 秒检查一次,锁默认 30 秒过期)检查当前线程是否还持有锁,如果还持有,就把过期时间重置回 30 秒。这样业务逻辑不管执行多久,只要线程一直活着,锁就不会被提前释放。如果线程挂了,看门狗自然也就不再续期,锁会在最长 30 秒后自动过期。
看门狗解决了我前面说的“锁时间设多长”的问题,但 Redisson 也不是银弹。它的看门狗机制只在未指定 leaseTime 的时候生效,如果你在代码里手动传了锁过期时间,看门狗就不会启动。另外,Redisson 默认的锁还是非公平锁,高竞争场景下可能会有线程饿死问题,不过多数业务场景其实不需要公平锁。
下面这段代码是我们项目里实际在用的 Redisson 分布式锁写法:
java复制@Autowired
private RedissonClient redissonClient;
public boolean deductStock(Long productId, int count) {
String lockKey = "lock:stock:" + productId;
RLock lock = redissonClient.getLock(lockKey);
boolean locked = false;
try {
// 尝试加锁,最多等待 3 秒,锁自动过期时间 30 秒
locked = lock.tryLock(3, 30, TimeUnit.SECONDS);
if (!locked) {
// 拿不到锁,直接返回失败或者走快速失败
return false;
}
// 业务逻辑:检查库存,扣减库存
return doDeductStock(productId, count);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
if (locked) {
lock.unlock();
}
}
}
3.4 锁粒度与热点 Key:高并发下分布式锁最容易忽略的问题
分布式锁解决了跨进程互斥,但如果你把锁的粒度设计得太粗,照样会带来性能问题。最典型的例子是把全店库存用一把锁保护,那所有商品的下单请求都会串行化,并发能力瞬间回到单机时代。
正确的做法是把锁的粒度尽量细化。比如锁库存的时候用 lock:stock:{productId},而不是 lock:stock:all;锁用户账户的时候用 lock:account:{userId},而不是用一个全局锁。锁粒度越细,并发冲突的概率越低,系统吞吐量就越高。
但这里有一个非常矛盾的点:热点商品(爆款)恰恰是并发冲突最集中的场景。一把细粒度的锁挂在同一个商品 ID 上,QPS 一高,Redis 里的这个 Key 就变成了热点 Key,所有请求都打到 Redis 的同一个分片上。这时候你可能会遇到 Redis 单线程处理能力被单个 Key 拖住的情况。
我们当时遇到过类似问题,一个爆款商品秒杀的时候,Redis 里那个锁 Key 的 QPS 直接十万级,Redis 单分片 CPU 被打满。后面我们用了几个办法来优化:一是把商品 ID 打散成多个段,比如把 1000个库存 拆成 10个段,每个段对应一个锁 Key,请求进来先随机或者按用户哈希路由到一个段上,这样就把热点锁从 1 个变成了 10 个。二是对于一些读多写少的场景,优先用 Lua 脚本做原子扣减,而不是锁 + 查询的两次网络交互。三是做好兜底,如果 Redis 锁获取失败次数太多,可以考虑本地锁加分布式锁的二级降级方案,但要注意降级后的一致性风险。
3.5 分布式锁面试题背后的真实考点
我面试后端工程师的时候,分布式锁是必问题目。但我不喜欢问“怎么用 Redis 实现分布式锁”这种答案背得很熟的题,我更愿意问:你的锁超时时间设多少,依据是什么?如果锁过期了业务还没执行完怎么办?Redis 主从切换的时候锁丢了怎么办?
这几个问题背后的真实考点是:你有没有真正理解分布式锁的适用范围和局限性。比如 Redis 主从切换导致锁丢失的问题,Redis 的分布式锁在极端情况下确实不是绝对可靠的,官方为此还提出了 RedLock 算法,但 RedLock 本身在业界也很有争议。多数业务场景,我更倾向于接受“Redis 分布式锁在绝大多数情况下够用”,但要在设计上保证业务有幂等兜底,这样即使极端情况下出现并发问题,也不会造成资金损失或数据不可恢复。没有绝对可靠的分布式锁,只有配套了补偿机制的业务闭环。
4. 消息队列:削峰填谷不是把消息发出去就完事
4.1 秒杀系统里消息队列到底扮演什么角色
消息队列在微服务里被提到最多的价值就是削峰填谷。我拿秒杀系统举例:如果流量是 8000 QPS,直接打到订单服务,订单服务必然扛不住。但在用户点击“立即购买”到真正创建订单之间,插入一个 MQ,情况就完全不一样了。
用户请求进来之后,服务端先做基本的参数校验、库存预扣、风控校验,这些操作是轻量的,可以抗住高流量。校验通过之后,把“创建订单”这个任务封装成一条消息发到 MQ,然后立刻返回用户“排队中”的提示。真正去数据库创建订单的消费者,按照自己能够承载的速度(比如每秒处理 500 个)去拉取消息消费。这样瞬时流量就成了平滑的流量,这就是“削峰填谷”的含义。
很多团队把消息队列当做一个简单的异步解耦工具来用,我觉得低估了它的价值。消息队列的真正价值是,它让系统的生产端和消费端不再绑死在同一个节奏上。生产端快了,消息会在队列里积压;生产端慢了,队列会空闲。两个服务之间的状态互相不影响,这个特性在流量峰值时就是系统能不能活下去的关键。
4.2 重复消费问题:面试必问,线上必炸
消息队列的另一个核心机制是至少一次(At Least Once)投递。这意味着每一条消息都有可能被消费者重复收到。这不是某一个 MQ 的 Bug,而是分布式环境下“网络超时重试”带来的必然结果。
我举一个实际案例:一个消费者从 Kafka 拉取消息,处理完消息之后,在提交 offset 的时候网络超时了。Kafka 认为这条消息没被消费成功,会重新投递给消费者。于是同一条消息被处理了两遍。如果这个逻辑是“给用户加积分”或者“发送短信通知”,重复消费的后果就是用户多了积分、收了两条短信。
解决重复消费问题的唯一可靠手段是消费幂等。什么是幂等?就是同一个操作执行一次和执行一万次,最终的结果是一样的。实现幂等的常见方案有几种:第一种,利用数据库唯一约束,比如订单号唯一索引,重复插入直接报错;第二种,在业务表里增加一个消息去重表,消费前先查询消息 ID 是否已经处理过,用数据库的唯一索引来保证同一时刻只有一个线程能插入成功;第三种,利用 Redis SETNX 做分布式去重,消息处理之前先设置一个 key,设置成功才继续处理。
我个人的建议是,核心链路优先用数据库唯一约束做幂等兜底,Redis 去重只能做前置过滤。原因是 Redis 存在数据丢失的可能,一旦去重 key 丢失,重复消息还是会打到业务上,而数据库唯一约束是最终防线,谁也绕不过去。
4.3 顺序消息与事务消息的真实取舍
顺序消息是面试高频考点,但我要说一个反常识的事实:大多数业务根本不需要全局顺序消息。
所谓顺序消息,是指消息按照生产的顺序被消费者消费。Kafka 只能在分区(Partition)内保证顺序,RocketMQ 只能在队列(Queue)内保证顺序。要保证顺序,就得把需要有序的消息发送到同一个分区或队列里,这样会严重限制并发度。比如订单的“创建、支付、确认”这几个消息,如果放到同一个队列里串行消费,这个订单维度的并发就没了。
关于分布式事务消息,RocketMQ 提供了事务消息机制保证本地事务和消息发送的原子性。我见过不少团队在初期使用事务消息,后面项目复杂了之后反而把它替换掉了。原因是事务消息的流程很重,需要反查接口(check),一旦实现不对,消息永远发不出去,问题比不发还难查。如果你追求轻量,也可以使用“本地消息表”方案:本地事务里插入业务数据和消息记录,另起一个任务轮询消息表,把未发送的消息发出去。这个方案实现简单,逻辑直观,适合绝大多数项目。什么时候需要引入 RocketMQ 事务消息?我个人的判断是:当你的团队已经对 MQ 运行机制足够熟悉,并且确实存在跨服务事务强一致需求时再考虑。
4.4 消息堆积:别等告警了才开始算消费容量
线上消息队列最常见的事故就是堆积。堆积的直接表现是消费者处理不过来,消息越积越多,延迟越来越大,最终下游看到的都是过期的旧数据。
处理堆积问题,第一原则是:不要一上来就去扩展消费者实例,先搞清楚消费瓶颈在哪。我曾经遇到一次堆积事故,团队第一反应是把消费者实例从 3 个扩到 10 个,结果发现堆积不但没缓解,反而有些消息处理得更慢了。排查下来发现瓶颈在数据库,10 个消费者同时去更新一张表,行锁冲突比 3 个消费者的时候更严重,整体吞吐量反而下降了。
消费端的容量规划应该从上到下逐层分析。假设消息平均处理耗时是 100ms,单个消费者线程的吞吐量就是 1 / 0.1 = 10 TPS。如果要支撑 1000 TPS 的峰值消费速率,至少需要 1000 / 10 = 100 个线程。这 100 个线程分布到多少个实例上,还要看下游数据库、Redis 能抗住多大的并发。消息队列是缓冲,不是让下游无限承受压力的理由。 消费端设计的时候就要考虑下游的容量上限,必要时在消费者里也做一次限流,保证下游安全。
5. 限流熔断:把故障隔离在发生之前
5.1 限流算法不是越多越好,关键是选对场景
限流算法有很多种:固定窗口、滑动窗口、漏桶、令牌桶。面试的时候被问到区别,很多人能背出来。但我想从实战角度去讲一下选型逻辑。
固定窗口计数器是最简单的,思路是:一分钟内最多允许 1000 个请求,每来一个请求计数加一,超过 1000 就拒绝。它的缺陷非常明显,在窗口切换的时候会出现双倍流量。假设最后一秒有 200 个请求,窗口重置后第一秒又能进 200 个请求,实际这 2 秒内就处理了 400 个请求,超过了速率限制。
滑动窗口解决了临界问题,它把一分钟切成多个小的子窗口,比如切成 6 个 10 秒钟窗口,每次滑动时统计最近 60 秒的值,实现起来稍微复杂一点,但更平滑。
漏桶算法的特点是恒定速率。不管上游请求多猛,漏桶每秒最多漏出固定数量的请求,超过桶容量的请求直接丢弃。这个特性很适合保护下游弱依赖系统,比如某个第三方接口限流每秒 200 次,你用漏桶就能保证不会超。
令牌桶算法是很多框架默认选用的算法。它以固定的速率往桶里放令牌,桶里最多存一定数量的令牌,每来一个请求就从桶里拿走一个令牌,没有令牌就拒绝。令牌桶允许一定的突发流量,因为桶里可以积攒令牌,适合绝大多数互联网业务场景。
我在实际项目里的做法是,网关层用令牌桶做整体流量限流,业务层对特定接口用滑动窗口做精细化限流,对下游弱依赖单独做漏桶。没有哪一种算法是万能的,关键看你要保护的是什么。
5.2 网关限流和业务层限流怎么分工
限流不是只做一层就够了的。我见过很多项目只在 Nginx 做了限流,业务接口没有限流,结果绕过网关的流量照样可以把服务打挂。限流应该是分层的。
第一层在 Nginx/LVS,用 limit_req 和 limit_conn 模块限制每个 IP 的连接数和请求速率。这一层主要防的是恶意攻击和无脑刷接口的机器人。第二层在 API 网关(Spring Cloud Gateway、Zuul、Kong 等),按照服务、路由、API Key 做限流。这一层是做业务分级限流的地方,比如普通用户接口限流 100 QPS,管理员接口限流 20 QPS。第三层在业务服务内,针对具体接口做更精细的限流,比如秒杀接口限制 500 QPS,普通查询接口限制 2000 QPS。
举个例子,在 Spring Cloud Gateway 里结合 Redis 做限流,核心代码大概长这样:
yaml复制spring:
redis:
host: localhost
port: 6379
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
这里 replenishRate 是每秒允许填充的令牌数,burstCapacity 是桶的最大容量。权重比是 100 QPS 持续流量加 200 的突发容量。这个方案是 Spring Cloud 内置的,原理就是令牌桶,但底层用了 Redis 做计数器存储,可以支撑分布式环境下多个网关实例共享同一个限流状态。
5.3 熔断不是关闭服务,而是有策略地失败
熔断和限流很容易混淆。我用一个比喻解释:限流是小区门口的保安,控制每小时最多放多少人进入;熔断是楼里的电梯检修员,发现电梯出了问题,直接拉闸停运,不让更多人上楼,避免出现安全事故。
熔断机制的三个状态:关闭、打开、半开。正常时熔断器是关闭状态,请求正常通过;当错误率达到阈值(比如 50%)或者慢调用比例过高时,熔断器打开,后续请求直接快速失败,不再调用下游服务;经过一段冷却时间后,熔断器进入半开状态,放少量试探请求过去,如果成功,说明下游恢复了,熔断器关闭,如果失败,继续打开。
这个机制的核心价值是快速失败。没有熔断的时候,下游服务挂了,上游服务所有线程都在等待超时,线程池被占满,内存飙升,最后上游也挂了。有了熔断,下游挂了之后,上游在几毫秒内就能收到失败响应,资源被释放出来处理其他请求。
在实际项目中,熔断一定要配合降级逻辑。降级是指熔断触发之后,给用户返回一个合理的兜底结果。比如下单服务熔断了,用户看到的不应该是 500 报错,而应该是“当前购买人数较多,请稍后再试”。再比如某个商品推荐接口熔断了,可以降级返回默认推荐列表或者空列表,让主流程继续走。
5.4 Sentinel 还是 Resilience4j:我把选择依据说清楚
微服务里做限流熔断,主流选择是 Sentinel、Hystrix、Resilience4j。Hystrix 已经停止开发了,这里不做推荐。重点对比 Sentinel 和 Resilience4j。
Sentinel 是阿里开源的,特点是功能丰富、接入简单、控制台可视化。它提供了实时监控、流控规则、熔断降级规则、系统自适应保护等能力,并且能针对热点参数做精确限流。在 Spring Cloud Alibaba 生态里,Sentinel 几乎是零成本接入。RuoYi-Cloud(若依微服务)这种国内常见的脚手架,也默认整合了 Sentinel。
Resilience4j 是 Hystrix 的继任者,设计上更轻量,是一个纯粹的库,没有控制台,需要自己搭建监控体系。它的优势是代码侵入性更低,通过装饰器模式包装业务逻辑,非常适合已经有完善监控体系的团队,或者不想引入重框架的项目。因为它是纯 Java 库,不依赖 Spring Cloud Alibaba 组件,避免了部分组件版本兼容问题。
选型建议很简单:如果项目基于 Spring Cloud Alibaba 体系,直接无脑用 Sentinel;如果项目是原生 Spring Cloud 或者其他微服务框架,优先考虑 Resilience4j。 不要因为 Sentinel 功能多就选它,也不要因为 Resilience4j 轻量就选它,要看技术栈是不是匹配。我在项目里还见到有人同时引入 Hystrix + Sentinel,这是完全没有必要的,两套熔断逻辑叠加在一起,排查问题的时候会非常痛苦。
6. 一套可直接落地的微服务高并发骨架
6.1 架构分层与核心组件配置
前面讲了那么多原理和工具,最终要落到架构上。我以 Spring Cloud Alibaba 技术栈为例,给出一套我在项目中实际使用的微服务高并发骨架。
接入层是 Nginx,负责静态资源缓存和 IP 级限流。服务网关是 Spring Cloud Gateway,负责统一鉴权、灰度路由、接口级限流,限流采用 Sentinel 的网关流控规则。业务服务按领域拆分:用户服务、商品服务、订单服务、库存服务、支付服务。服务间同步调用使用 OpenFeign;异步通知和削峰使用 RabbitMQ 或者 RocketMQ,秒杀类场景用 RocketMQ 更合适,因为它在事务消息和顺序消息上的支持更好。缓存统一走 Redis,分布式锁使用 Redisson。分布式事务采用 Seata 的 AT 模式,但只在核心链路开启。
服务注册与配置中心使用 Nacos。监控体系使用 Prometheus + Grafana + SkyWalking,告警接入钉钉或者企业微信机器人。
这个骨架的关键点在于:每个服务都要加上 Sentinel 依赖,并且在启动类上加上 @EnableSentinel 注解。配置上,我建议在 Nacos 里统一管理 Sentinel 规则,这样规则变更不需要重启服务。网关层单独配置一份流控规则,业务层配置另一份。
yaml复制# bootstrap.yml 中 Sentinel 接入 Nacos 的配置
sentinel:
transport:
dashboard: localhost:8080
port: 8719
datasource:
flow:
nacos:
server-addr: ${NACOS_ADDR}
data-id: ${spring.application.name}-sentinel-flow
group-id: DEFAULT_GROUP
data-type: json
rule-type: flow
6.2 从若依微服务项目谈谈脚手架改造经验
国内很多团队做微服务的时候会基于若依微服务版本(RuoYi-Cloud)来起步。这个脚手架的好处是把 Spring Cloud Alibaba 整套组件都整合好了,消息队列用的是 RabbitMQ,分布式事务用的 Seata,监控用的 SkyWalking。但我接手过几个若依项目,发现一个问题:脚手架把组件都接上了,但很多团队根本不会配流量治理的规则。
若依微服务默认整合了 Sentinel,但很多从若依出发的项目,Sentinel 控制台的规则几乎是空的。也就是说,微服务该拆的都拆了,该接的组件都接了,但高并发治理能力还是一片空白。这就像一个城市把高速公路修好了,但收费站没有设卡,什么车都能上,一旦车多就全堵死。
如果你正在基于若依做改造,我建议最先补齐三块:第一,在网关层加上全局限流规则,先保证网关不会被打挂;第二,在核心服务(比如订单、库存)里加上限流和熔断规则;第三,把 Sentinel 规则持久化到 Nacos,防止规则在应用重启后丢失。
这里分享一个我踩过的坑:若依客户端和服务端有多个模块,Sentinel 规则持久化的时候,一定要确认规则是持久化到了 Nacos,而不是只存在 Sentinel 控制台里。控制台默认规则是存在内存中的,重启控制台或者客户端应用,规则就丢了。我们要把规则配置到 Nacos 的配置中心里,然后在客户端里配置数据源从 Nacos 拉取规则。这个配置容易遗漏,但是非常关键。
6.3 压测与故障演练:上线前必须做的高并发体检
架构设计得再好,没有经过压测和故障演练,你永远不知道系统在真实流量下会怎么样。我见过太多项目上线前只用 Postman 测了几遍接口就敢直接搞活动,结果线上第一批流量就把服务打挂了。
压测的目标不是跑一个“压测报告说 QPS 能到多少”的数字,而是要找到系统的容量水位线。你至少要回答这几个问题:单个服务的瓶颈在哪里?数据库连接池在多少并发下开始告警?Redis 的 CPU 到多少的时候开始不稳定?消息队列堆积多久开始影响下游?这些数据在不存在压测数据的时候是猜不出来的。
我认为线上压测是必不可少的,但可以分级。最稳妥的方式是在生产环境单独搭建一套压测集群,与线上数据隔离。如果没有这个条件,可以在线上低峰期做短时压测,但必须做好应急预案。故障演练也很重要,我建议在一个独立环境里做以下演练:停掉一个服务实例,观察流量会不会自动负载均衡到其他节点;停掉 Redis,观察服务有没有降级策略;让一个下游服务响应变慢,观察熔断器能否在预期时间内打开。
这些演练做一遍,你对系统稳定性的理解会比看十篇架构文章都深。因为我们讨论的高并发问题,大多不是理论问题,而是具体环境下的具体问题。
6.4 常见面试追问与项目亮点该怎么说
顺便聊聊面试。看热搜词里也有很多分布式锁和消息队列的面试题,很多人项目做了不少,但面试的时候讲不出亮点。我总结一个比较实用的表达套路:问题 -> 方案 -> 取舍 -> 数据验证。
比如面试官问分布式锁,你不要只说“我用 Redisson 实现了分布式锁”,而要这样讲:项目里有秒杀场景,库存扣减存在超卖风险,单机锁无法覆盖多实例,所以引入了 Redis 分布式锁;选择 Redisson 是因为它的看门狗机制能自动续期,避免了锁超时和误删问题;同时我们在业务层做了幂等去重,保证了即使极端情况下锁失效,也不会出现重复扣款。最后可以说,经过压测,库存接口在 3000 QPS 下不再出现超卖,接口响应时间 P99 从 800ms 降到了 120ms。
这样面试官听到的不只是一个单词,而是一个完整的工程决策过程。很多时候,面试官看中的并不是你会不会用某个工具,而是你有没有判断力,知道什么场景用什么方案,以及方案的代价和风险是什么。
7. 最后说几句项目里的体会
分布式锁、消息队列、限流熔断这些东西,看起来是三个独立的技术点,但在实际系统里它们是环环相扣的。限流负责在最外层挡住过量流量,消息队列在中间削峰填谷,分布式锁在数据层面保证并发安全,而熔断降级则保证任何一个环节出故障时,系统不会整体崩溃。你只做其中一项,系统还是不稳,只有把它们配合起来,才算真正建起了防护体系。
我在实际项目中最后悔的一件事,是没有在项目早期就建立起完善的压测和监控机制。很多东西等到线上炸了才去被动排查,代价太大了。如果你正处在一个新项目起步阶段,一定要早一点把 Sentinel 规则、监控告警、压测报告这些基础设施想清楚,后面能省非常多的事。
还有一个很实在的建议:不要迷信任何一个框架或组件。Redis 分布式锁不是百分之百可靠,消息队列也会丢消息,限流算法也有误杀率。工程的本质就是在这些不完美之间做权衡,并用额外的机制去弥补它们的弱点。能把这一点想通,你的系统设计能力会上一个台阶。
