1.spring cloud gateway 使用 redis-ratelimiter 来做限流,要引入spring-boot-starter-data-redis-reactive。
Spring Cloud Gateway 是 WebFlux 响应式框架,限流必须用
spring-boot-starter-data-redis-reactive,不能用普通的 spring-boot-starter-data-redis,否则会报错。
Gateway 的RedisRateLimiter底层需要ReactiveRedisTemplate+ReactiveRedisConnectionFactory(响应式 Bean),只有spring-boot-starter-data-redis-reactive才会自动装配这两个 Bean; 如果你只引入普通spring-boot-starter-data-redis(Lettuce/Jedis 阻塞式):
✅项目可以正常启动,不会启动报错
❌一访问带 RequestRateLimiter 的路由,直接抛出 Bean 找不到异常
1. 最常见报错(标准日志)
No qualifying bean of type 'org.springframework.data.redis.core.ReactiveRedisTemplate<java.lang.String, java.lang.String>' available或者:
No qualifying bean of type 'org.springframework.data.redis.connection.ReactiveRedisConnectionFactory' available含义:RedisRateLimiter 尝试注入响应式 Redis 模板 Bean,但是容器里没有,因为普通 starter 只创建阻塞式 RedisTemplate,不创建 ReactiveRedisTemplate。
追问
阻塞式 RedisTemplate(
spring-data-redis里的)
RedisTemplate是同步阻塞的 Redis 客户端 API,底层:
- Jedis:本身就是阻塞 IO
- Lettuce:虽然底层是 Netty 非阻塞,但
RedisTemplate封装的方法是同步阻塞调用一句话:调用
redisTemplate.opsForValue().get("key")这行代码,当前线程会卡住,等待 Redis 返回结果,期间什么都不干,拿到结果才继续往下执行。1. 对比:RedisTemplate(阻塞) vs ReactiveRedisTemplate(响应式)
特性 RedisTemplate(阻塞) ReactiveRedisTemplate(响应式) 依赖 starter spring-boot-starter-data-redisspring-boot-starter-data-redis-reactive返回值 String、Object普通对象Mono<T>/Flux<T>线程行为 调用时阻塞当前线程,等待 IO 不阻塞线程,注册回调,IO 就绪后再执行 适用场景 Spring MVC(Servlet 线程池) WebFlux(事件循环 EventLoop 线程,Spring Cloud Gateway) 2. 阻塞的直观代码例子
// 阻塞式 RedisTemplate String value = redisTemplate.opsForValue().get("demo:key"); // 👉 执行get()的时候,当前线程停在这里! // 网络慢、Redis卡顿,线程就一直卡在这行等待返回Servlet 体系下,这没问题:Servlet 线程池有很多线程,卡住一个只是消耗一个线程,线程池可以扩容。
但是 Spring Cloud Gateway 是 WebFlux,只有少量 EventLoop 事件线程!Gateway 默认就几个 EventLoop 线程(比如等于 CPU 核心数),EventLoop 线程绝对不能被阻塞。 如果在 EventLoop 里调用阻塞 RedisTemplate,一旦 Redis 慢:
- EventLoop 线程卡死,没法处理其他请求
- 请求排队堆积,网关整体卡死,雪崩
这就是为什么 Gateway 限流组件强制要求
ReactiveRedisTemplate。3. 补充:Lettuce 的特殊点(很多面试会问)
Lettuce 底层通信是 Netty 非阻塞,但是:
RedisTemplate包装的是同步 API,内部会调用.block()等待结果,变成阻塞调用ReactiveRedisTemplate直接用 Lettuce 的响应式 API,不 block误区纠正:不是 Lettuce = 响应式,Jedis = 阻塞。 Lettuce 两套 API 都有,看你用的是
RedisTemplate还是ReactiveRedisTemplate。4. 回到你刚才的 Gateway 限流场景
Gateway 的
RedisRateLimiter是 WebFlux 过滤器,运行在 EventLoop 线程,它内部代码直接注入ReactiveRedisTemplate。
- 只引入普通 redis starter:容器没有
ReactiveRedisTemplateBean → 请求进来直接报 Bean 找不到- 如果你强行手动创建
ReactiveRedisTemplate但用阻塞 API,属于高危操作,高并发会 eventLoop 阻塞。面试精简版
阻塞式 RedisTemplate,是 SpringDataRedis 提供的同步 Redis 操作模板。调用方法时当前线程会阻塞等待 Redis IO 返回。适用于 Servlet MVC。Gateway 基于 WebFlux 事件循环,不能使用阻塞 API,必须用 ReactiveRedisTemplate,不能用普通 RedisTemplate。
2.
Gateway 网关服务 gateway‑server
❗重要:不要引入 spring‑boot‑starter‑web!gateway 使用 webflux。
Gateway 依赖 webflux,不能共存 web。
3.
Spring Cloud 版本和 Boot 版本不匹配,网关启动异常。
4.
Feign 调用服务名写错,大小写敏感。
5.
Nacos:服务注册成功,但是 Feign 调用报错找不到实例:检查 namespace、group 两边必须一致。
ex:Namespace 写错:服务注册在 dev 命名空间,消费方在 public,服务列表看不见。
6.
Nacos-配置中心:
- ❗SpringBoot3,必须引入 spring‑cloud‑starter‑bootstrap,否则 bootstrap.yml 不加载,读取不到 nacos 配置。
- 忘记加
@RefreshScope:修改 nacos 配置,程序不刷新。
7.
Seata Server 有两种存储模式
- file:内存存储,重启事务数据丢失,只用于 demo 测试。
- db:数据库存储(生产必须),事务信息持久化 mysql。
8.
Seata:
有发起方加 @GlobalTransactional(rollbackFor = Exception.class);远程被调用微服务只需要普通 @Transactional。
Feign 调用,XID 全局事务 ID 会自动透传 Header,Seata 拦截 Feign 请求自动传递 xid,不需要手动处理。
rollbackFor = Exception.class:一定要写;默认 RuntimeException 才回滚,普通 Exception 不会回滚。
9.
启动微服务后,访问一次接口,Sentinel 控制台就能看到该服务。
Sentinel 是懒加载:默认第一次访问资源,才会注册到控制台;配置
eager:true启动就注册。
10.
- blockHandler 和 fallback 分不清:blockHandler 只管 sentinel 拦截;fallback 管业务异常。
- 懒加载:不配置 eager:true,服务启动控制台看不到服务,必须访问一次接口。
11.
网关流控普通流控区别?
网关支持基于http 请求属性(IP、header、query 参数)流控;普通 Sentinel 是方法维度,没有原始 http 请求信息。
12.
为什么网关有 Sentinel,还需要服务内 Sentinel?
场景 1:RPC 调用,不走网关
服务之间 Feign/Dubbo 互相调用,根本不经过 Gateway。
例:A 服务调用 B 服务的接口,这个流量不会经过网关,Sentinel Gateway 感知不到。 如果 B 服务只有网关 Sentinel,没有服务内 Sentinel,一旦 A 疯狂调用 B,直接雪崩。
场景 2:网关是粗粒度,需要接口级防护
网关只能按路由限流:/order/** 全部算一类。 但业务上:下单接口压力大,查询订单压力小。网关没法区分。 服务内 Sentinel 可以单独对 /order/create 做独立限流,查询接口不受影响。
场景 3:多层故障隔离
- 第一层(网关):挡住外部超大流量,防止洪水直接打进集群;
- 第二层(服务内):即使网关放行流量,单个接口异常、RPC 超时,服务自己熔断,故障锁在当前服务,不扩散。
网关挂了、绕过网关的请求,服务自身还有一层保护。
场景 4:热点参数限流
热点参数限流是普通 Sentinel 的能力,Sentinel Gateway 不支持。 比如秒杀:限制同一个用户 ID 高频下单,这种需要解析请求参数做热点限流,放在服务内部更合适。
面试一句话回答
Sentinel Gateway 是网关入口防护,只处理经过网关的 HTTP 流量,做路由级的限流熔断;但是微服务之间的 RPC 调用不经过网关,网关监控不到。 所以一般是双层防护:网关挡外部大流量,服务内部 Sentinel 负责接口、RPC 调用、热点参数的细粒度防护,实现多层故障隔离,防止雪崩。
13.
LoadBalancer没有内置权重策略。 如果需要权重负载均衡,使用 Nacos Discovery 自带权重能力;或者自己自定义 LoadBalancer 策略。
Nacos 控制台 → 服务管理 → 实例列表,可以修改实例权重。
权重范围:0‑100;权重 = 0,不会接收流量。
场景:灰度发布。新启动实例权重设置很小 (10),老实例权重 90;少量流量打到新版本,验证没问题再把权重拉满。
开启权重负载均衡:
spring: cloud: nacos: discovery: # 开启nacos权重负载均衡,底层替换LoadBalancer实例选择逻辑 weight: true
开启 weight:true 之后,会覆盖掉 LoadBalancer 自带的 round_robin/random 策略;优先使用 Nacos 权重算法。
14.
LoadBalancer 重试机制
调用服务实例失败,自动换一个实例重试。
⚠️注意:重试一定要注意幂等性!
非幂等接口(新增订单)不要开启重试,会产生重复数据。
15.
重试如何保证接口幂等?
方案 1:全局唯一业务幂等 Token(最常用,适合新增类接口:创建订单、生成单据)
流程:
上游调用方,在业务发起之前,生成全局唯一幂等 Token(UUID / 雪花 ID)。
把 Token 放在请求头(Idempotency‑Token)或者请求体中,每次重试,复用同一个 Token,不要生成新 Token。
✅重点:LoadBalancer 重试,不会重新生成请求,复用原始请求对象,Token 自然不变,这点很关键。
下游服务收到请求:
先拿 Token 去 Redis / 数据库幂等表查询是否已经处理过。
如果不存在:执行业务逻辑,执行成功后,把 Token 存入 Redis(设置过期时间,比如 24h)。
如果 Token 已经存在,直接返回上一次成功的处理结果,不再执行业务逻辑。
Redis 伪代码:
// 使用setnx保证原子性 Boolean ok = redisTemplate.opsForValue().setIfAbsent(token,"processed",24, TimeUnit.HOURS); if(!ok){ // 已经处理过,直接返回历史结果 return 历史成功响应; } // 执行业务逻辑 createOrder() // 业务执行完成,key已经写入,后续重试全部直接返回注意:
必须保证 检查 + 写入 Token 是原子操作,不能先查询再 set(并发下会穿透),Redis 用SETNX;数据库用唯一索引。
Token 过期时间,大于业务最大重试窗口。
方案 2:数据库唯一约束(唯一索引)
适合数据库落库场景,给业务唯一标识建立数据库唯一索引。 例如:订单外部业务号、单据号。
第一次请求:插入成功。
重试请求再次插入相同唯一值,数据库抛出唯一索引冲突异常。
下游捕获唯一冲突异常,把它转化成正常成功响应返回给上游。
优点:简单,不需要 Redis; 缺点:只针对新增插入;不适合更新操作;高并发下数据库压力大。
方案 3:基于业务状态机(更新、扣减类业务,库存、订单状态流转)
适合状态流转业务,比如:订单状态:待支付→已支付→已发货;库存扣减。
更新语句带上前置状态条件,状态机约束,只允许状态流转一次。
示例:库存扣减
-- 只有库存>=扣减数量才执行扣减;重复重试,条件不满足,update返回0行 update stock set count = count‑#{num} where id=#{id} and count >= #{num};订单状态更新:
-- 只能从【待支付】修改为【已支付】,重复重试,条件不命中,更新行数0 update `order` set status='PAID' where id=#{orderId} and status='WAIT_PAY';
执行完 SQL 判断 update 返回影响行数:
返回 > 0:执行业务成功;
返回 0:代表状态已经变更过,是重试请求,直接返回成功,不再执行业务。
这个方案非常适配分布式事务、Seata、库存扣减场景。
16.loadbalancer
- 客户端负载均衡:消费端本地选实例,直连目标服务,不是 Nginx 这种服务端 LB。
- 内置策略:轮询 round_robin、随机 random。
- 权重负载均衡靠 Nacos‑Discovery 扩展,LoadBalancer 本身没有权重策略。
- @LoadBalanced给 RestTemplate 增加拦截器,解析服务名替换真实 ip 端口。
17.
服务器部署
1、开发 / 测试环境(最常见:单台机器混部全部组件)
一台 Linux 服务器,把下面全部装在同一台机器,不同端口区分:
Nacos(端口 8848)
Seata TC(端口 8091)
Sentinel Dashboard(8080)
Prometheus(9090)
Grafana(3000)
✅优点:省机器,部署简单,本地测试、联调非常合适。❌缺点:资源争抢,一个组件 CPU / 内存打满会影响其他组件;不能用于正式生产。
2、中小规模生产(2 台机器,推荐)
很多中小公司生产就是这么玩的
机器 A:Nacos + Seata TC
机器 B:Prometheus + Grafana + Sentinel Dashboard
理由:
Nacos + Seata TC 属于微服务核心控制面,放一组机器;
监控类组件(Prometheus/Grafana/Sentinel 控制台)属于观测面,放另一组;
Sentinel Dashboard 压力很小,占用资源极低,随便和监控放一起没问题。
注意:生产 Nacos、Seata TC 建议集群部署(多实例),不是单实例。比如 Nacos 至少 3 节点集群,Seata TC 至少 2 节点集群。
3、大规模生产(4 组独立机器,隔离)
高可用、大流量、企业规范严格场景,分开独立机器 / 独立 K8s 节点组:
Nacos 集群(注册 + 配置中心)
Seata TC 集群
Sentinel Dashboard(单实例即可,压力很小)
监控集群:Prometheus + Grafana
好处:资源隔离,互不影响,故障域隔离。
重点:Sentinel Dashboard 本身压力很小,绝大多数场景不需要单独一台服务器,不需要单独集群。
不需要必须部署在 4 台服务器。
组件是独立进程,但进程可以混部。
测试环境:
全部组件部署在同一台服务器,依靠不同端口隔离。
中小生产:
一般分成两组,一组放 Nacos 和 Seata TC(微服务控制面);
另一组放 Prometheus、Grafana、Sentinel Dashboard(监控观测组件)。
大规模生产,为了故障隔离,会分开部署到多台机器。
注意:Sentinel Dashboard 只是控制台,资源消耗很小,不需要独占一台服务器。
❗️❗️❗️
Sentinel、Seata 的客户端(RM/TM、sentinel client)是嵌在业务服务进程内
18.
2. Nacos 配置不生效,@RefreshScope 失效
现象:Nacos 修改配置,程序没有拿到新值。 根因:
忘记加@RefreshScope;
Bean 被@Conditional提前初始化;
bootstrap 配置没有加载;SpringBoot3 需要引入spring‑cloud‑starter‑bootstrap。
3. 权重灰度不生效
现象:Nacos 控制台修改实例权重,流量没有按权重分配。 根因:没有开启 spring.cloud.nacos.discovery.weight=true;LoadBalancer 默认轮询不会读取权重。
19.
Q1:介绍下你项目用的微服务技术栈,整体架构是怎样的?
答: 我们采用 Spring Cloud 微服务体系。注册 & 配置中心:Nacos,服务注册发现、配置动态管理,支持 namespace/group 做环境隔离,支持权重实现灰度发布。
网关:Spring Cloud Gateway,基于 WebFlux 响应式,作为统一入口,负责路由转发、鉴权、限流、跨域。
远程调用:OpenFeign 做声明式 HTTP 调用,负载均衡使用 Spring‑Cloud‑LoadBalancer,替代旧的 Ribbon。
容错防护:Sentinel 做流量防护,限流、熔断降级、热点参数限流,规则持久化到 Nacos,防止服务雪崩。
分布式事务:Seata AT 模式,业务无侵入解决微服务之间数据一致性问题。
可观测体系三支柱
Metrics 指标:Micrometer 采集指标,Prometheus Pull 模式拉取 /actuator/prometheus,Grafana 做大盘可视化,Alertmanager 实现告警推送。
Tracing 链路追踪:SpringBoot3 废弃 Sleuth,使用 Micrometer‑Tracing,底层 Brave 实现,Span 上报 Zipkin,全链路 TraceId 透传定位慢调用。
Logging 日志:日志打印 traceId、spanId,Loki 做日志聚合,三者依靠 traceId 关联排查问题。
请求流程:前端请求打到 Gateway 网关 → Gateway 路由转发,LoadBalancer 从 Nacos 选取实例 → Feign 调用业务微服务;服务之间 Seata XID、Tracing traceId 自动通过 Http Header 透传。
20.
看门狗锁 API 上相关参数(影响看门狗是否开启)
| 参数 | 说明 | 看门狗状态 |
|---|---|---|
lock.lock() | 无参加锁 | ✅ 开启看门狗自动续期 |
lock.tryLock(long waitTime, TimeUnit unit) | 只填等待时间,不填 leaseTime | ✅ 开启看门狗 |
lock.lock(long leaseTime, TimeUnit unit) | 指定 leaseTime | ❌ 看门狗禁用,leaseTime 到期锁释放 |
lock.tryLock(long waitTime, long leaseTime, TimeUnit unit) | 指定 leaseTime | ❌ 看门狗禁用 |
核心对比表
| 特性 | lock() | tryLock(waitTime, unit) |
|---|---|---|
| 阻塞行为 | 无限阻塞拿不到锁就一直等,永不超时 | 有限阻塞最多等待 waitTime,超时直接返回 false |
| 返回值 | void,成功拿到锁才返回;拿不到一直卡着 | boolean:true = 拿到;false = 超时没拿到 |
| 中断响应 | 等待锁时不响应中断线程 interrupt,不会抛异常;拿到锁之后才会标记中断状态 | 等待锁期间可以响应中断被 interrupt 直接抛出InterruptedException |
| 使用场景 | 必须拿到锁才能继续,不允许失败 | 不希望无限卡死,有兜底降级逻辑(防止死锁、服务雪崩) |
平时生产的隔离级别是什么,RC 还是 RR?
线上生产用的是RC(Read Committed,读已提交)。
项目中防重放是什么
Ex:
5 分钟时间戳防重放是:请求中携带发送时间requestTime,服务端收到请求后,比较它和服务器当前时间。
如果相差太久,就认为这可能是一份被截获后重新发送的旧请求,直接拒绝。
// 防重放校验
long diffMinutes = Math.abs(System.currentTimeMillis() - time) / (1000 * 60);
if (diffMinutes > REQUEST_EXPIRE_MINUTES) {
log.info("防重放检查不通过");
return Response.fail(HttpStatus.UNAUTHORIZED, "请求已过期");
}
log.info("完成防重放检查");
什么是“重放攻击”
假设业务方在 12:00 发送了一个合法请求:
{ "businessLineCode": "LINE_A", "receiptTypeCode": "INVOICE", "fileId": "F001", "requestTime": 1789224000000 }攻击者即使无法解密或修改它,也可能截获整个 RSA 密文,然后在 13:00 原样再发送一次:
第一次:合法业务方发送 第二次:攻击者原样复制并重新发送