ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

微服务网关限流熔断:Redis与MongoDB的选型与分工实践

微服务网关限流熔断:Redis与MongoDB的选型与分工实践 先说结论在微服务网关做限流熔断MongoDB和Redis不是一回事也不该放在同一个天平上比。Redis才是实时拦截链路里的主力MongoDB更适合做限流的“后台”统计、审计、持久化、以及给规则引擎当配置库。这篇我会从网关限流熔断的真实场景出发把两个存储的定位、代码落地、部署要点和坑全部拆开讲清楚。这个题目来自我最近在做的网关治理项目算是对过去踩坑的一次系统复盘。如果你正在做微服务网关的限流、熔断、降级方案或者准备面试时被问到“Redis和MongoDB怎么选”这篇可以直接帮你建立一套判断框架。1. 先拆场景限流熔断到底需要存储做什么很多人一上来就纠结数据库其实搞反了。限流熔断这套东西核心不是“选什么存数据”而是“你在哪个环节、以什么频率、拿数据来干什么”。搞清楚这件事选型自然就清晰了。1.1 由热词反推的真实需求结合最近的搜索热词比如“sentinel限流和熔断降级”“微服务网关”“神禹网关”“微服务整合knife4j nacos”来看大家真正关心的是这条链路请求进来 → 网关 → 限流判断 → 放行 / 触发熔断 → 拿到结果 → 处理业务这条链路有两个非常清晰的数据诉求实时判断当前请求能不能放行需要在一个极短的时间窗口内完成判断。这个数据的读写频率跟QPS成正比毫秒级响应是底线。事后追溯谁被限流了、限流了几次、熔断是从什么时间开始的、什么时间恢复的。这类数据是低频写入、长期保存而且要支持聚合查询。这两类诉求指向的存储模型完全不同。实时判断要的是“快、原子、低延迟”事后追溯要的是“稳、结构化、好查询”。1.2 实时链路上的三个硬指标在网关这一层做限流存储方案必须满足三个硬指标缺一个都会出事。原子性计数器加一和判断阈值必须是一个原子操作。如果是应用层先读再写并发一高就会出现“流量穿墙”限流直接失效。Redis的INCR、EVAL配合Lua脚本天然满足MongoDB要处理分布式计数就得加事务或者自己实现CAS逻辑成本完全不对等。低延迟网关每拦截一个请求就要做一次限流判断这个判断本身不能成为新的瓶颈。Redis是纯内存操作P99延迟在毫秒以内。MongoDB虽然有内存Cache但写路径有WiredTiger引擎和磁盘持久化介入延迟比Redis高一个量级放在主链路上会很吃亏。高可用网关挂了可以重启限流器挂了那就是全站流量裸奔。Redis可以用主从哨兵或者集群模式保证可用性MongoDB虽然有副本集但为了一个计数器去维护一整套副本集重了。1.3 统计链路上的三个柔性指标事后统计的诉求就完全不一样了。这里不需要实时甚至可以接受几分钟的延迟但数据不能丢、要能查、要有结构。结构化存储限流日志里通常要记录时间、用户ID、接口路径、被限流次数、网关实例ID、命中规则ID。这种数据天然适合文档模型MongoDB的文档结构可以直接对应一条日志记录字段随意扩展不需要提前建表。聚合查询能力统计报表要按分钟、按接口、按用户维度聚合。MongoDB的Aggregation Pipeline做按时间分组的统计非常顺手而且不需要额外的分析引擎。数据过期清理审计日志不需要永久保存MongoDB的TTL索引可以自动清理过期数据省去定时任务脚本。把这两类需求拆开答案已经很明显了Redis负责“拦截”MongoDB负责“沉淀”。下面我把对比细节拉深一点。2. 从并发模型和数据结构看本质差异限流熔断场景下的选型不妨先回到数据存储的底层原理看看两个引擎各自擅长什么。2.1 Redis单线程事件循环带来的原子操作红利Redis之所以能在限流场景当主力核心不是“快”而是“原子”。Redis是单线程事件循环模型所有命令在服务端严格串行执行。也就是说多个客户端同时发送INCR命令Redis也是逐个执行的天然没有并发写冲突。这个特性对限流来说太重要了。计数器场景最常见的实现就是固定窗口INCR rate:limiter:user:1001:202406011200 EXPIRE rate:limiter:user:1001:202406011200 120这个组合看似简单但如果不做原子封装高并发下先INCR后EXPIRE之间会穿插其他请求导致key没设置过期时间内存被撑爆。正确的做法是写进Lua脚本让Redis整个执行中间不容插队local key KEYS[1] local limit tonumber(ARGV[1]) local now tonumber(ARGV[2]) local window tonumber(ARGV[3]) -- 固定窗口计数 local current redis.call(INCR, key) if current 1 then redis.call(PEXPIRE, key, window) end -- current limit 则拒绝 if current limit then return 0 end return 1这就是为什么“分布式限流十有八九是基于Redis”的原因不是Redis最聪明而是它的并发模型让“判断计数过期”成了一个不可分割的原子单元应用层不需要加锁也不会有并发穿透。2.2 MongoDB文档模型和索引查询的灵活性MongoDB的核心竞争力是文档模型。每个限流事件可以存成一份JSON风格的文档字段天然可扩展。比如今天只记录用户ID明天想加上用户所在城市、设备类型、请求来源不需要改表结构直接往文档里加字段就行。对于快速迭代的产品这个优势很实在。另外一个容易被忽略的点是MongoDB的TTL索引。限流审计日志通常是高频写入、低频读取、定时过期。TTL索引可以在后台自动删除过期文档不需要开发人员维护定时清理任务。当你一天产生几千万条限流日志时这个能力能省很多事。但MongoDB在实时链路也有明显短板它的读写操作要经过驱动层、网络层、存储引擎层每一步都有额外开销。虽然WiredTiger有缓存但在高QPS下写入要确认刷盘策略writeConcern延迟很难压到网关层能接受的范围。所以MongoDB更适合做“写慢一点没关系、数据不能丢、后面要查”的事情。2.3 如果用MongoDB硬扛实时拦截会踩什么坑不是做不到而是不划算。我见过有人用MongoDB做网关的限流计数器把限流规则和用户计数都存在同一个collection里结果请求量一上来数据库连接池先被打满。MongoDB的原子性只能保证单文档级别的操作要做“先查再比较再更新”这种逻辑要么用findAndModify要么上事务。findAndModify虽然能保证单个文档的原子更新但文档之间的“计数器规则判断”无法原子完成需要二次判断逻辑复杂。MongoDB事务多文档可以搞定原子性但事务的性能开销对网关层来说是“杀鸡用牛刀”。更麻烦的是连接数一旦飙升MongoDB本身的连接管理会成为新的瓶颈网关一重启连接池就要被反复重建故障恢复慢。核心结论MongoDB能做但它解决的问题是“存和查”不是“快速判断”。把实时的活儿交给Redis把沉淀的活儿交给MongoDB各自干各自擅长的事。3. 实战选型不是二选一而是各归其位聊完原理聊聊我在项目里的实际落地方案。我参与的网关治理项目中最终方案是Redis做主链路实时限流与熔断状态存储MongoDB做限流审计日志与用户维度指标沉淀。这个方案跑了大半年效果很稳定下面具体说。3.1 Redis在网关链路里到底承担什么网关层面的限流我一般分成三层全局限流、接口维度限流、用户维度限流。全局限流整个服务集群每秒最多处理多少请求按Gateway节点维度计数。接口维度限流某个接口的QPS阈值比如/api/order/create限300QPS。用户维度限流某个用户每秒最多调用多少次防止单个用户刷爆服务。Redis对这三层都非常友好基本套路就是设计好key把窗口、阈值、计数都塞进去fixwindow:接口:路径前缀:时间窗口起始时间戳比如固定窗口1秒内的接口维度限流String key rate:api:/api/order/create: (System.currentTimeMillis() / 1000);因为key带上了当前秒级时间戳每个窗口自动失效EXPIRE基本可以省略逻辑更干净。3.2 熔断状态用Redis还是本地缓存熔断比限流更复杂一点因为熔断是有“状态机”的关闭 → 打开 → 半开 → 关闭。这三个状态的切换需要跨多个服务实例保持一致否则就会出现“一台机器熔断了另一台还在放流量”的问题。这时候Redis就派上用场了。熔断状态可以存成三个Hash字段breaker:服务名:接口名 - { state: OPEN, openTime: 1717200000000, halfOpenTime: 1717200030000 }网关每个实例在放行请求前都先检查Redis里的熔断状态state CLOSE正常放行state OPEN看当前时间是否超过openTime 熔断时长超过则尝试半开否则直接拒绝半开状态下放行少量探测请求如果成功率恢复到阈值以上把状态改回CLOSE这里有一个细节状态变更操作必须用Lua保证原子性避免多个网关实例同时把OPEN改成CLOSE。熔断状态的存储单独用一个小型Redis Cluster就够了不建议和业务缓存混用因为熔断元数据如果被业务key的淘汰策略挤出内存后果很严重。3.3 MongoDB沉淀数据的两种典型落法限流审计日志和用户指标我建议分两个collection来存collection一rate_limit_log{ request_id: a1b2c3d4, gateway_id: gw-01, api: /api/order/create, userId: 10086, client_ip: 10.20.30.40, limit_key: user:10086, action: reject, rule_id: rule_order_qps, timestamp: ISODate(2025-06-01T12:00:00.123Z) }TTL索引设置为7天过期db.rate_limit_log.createIndex({ timestamp: 1 }, { expireAfterSeconds: 604800 })collection二user_rate_metric做用户维度的半小时聚合存最近30天的数据用于业务侧分析哪些用户被限流最频繁有没有疑似爬虫{ userId: 10086, date: 2025-06-01, hour: 12, total_requests: 50000, rejected_count: 1200, avg_latency_ms: 342, updated_at: ISODate(2025-06-01T12:30:00.000Z) }用复合索引支撑高频查询db.user_rate_metric.createIndex({ userId: 1, date: 1, hour: -1 })很多人用Spring Data MongoDB查询时会把findAll当SQL的select *用后面要过滤再内存筛选这是错误的。MongoDB的普通查询要用Query加Criteria比如查指定用户在指定小时内的数据Query query new Query(); query.addCriteria(Criteria.where(userId).is(10086L)); query.addCriteria(Criteria.where(date).is(2025-06-01)); query.addCriteria(Criteria.where(hour).is(12)); ListUserRateMetric list mongoTemplate.find(query, UserRateMetric.class);而聚合分析直接上AggregationAggregation agg Aggregation.newAggregation( Aggregation.match(Criteria.where(timestamp) .gte(start).lt(end)), Aggregation.group(api) .count().as(rejectCount) .sum(rejected_count).as(totalRejected), Aggregation.sort(Sort.by(Direction.DESC, totalRejected)), Aggregation.limit(20) );这套方案在数据量上去之后依旧查询很快关键是把索引建好聚合第一级最好能走match过滤掉大部分数据不要全表扫描。3.4 为什么选型不是“Redis更好”而是“分工合理”要是只看“谁更快”当然是Redis但项目的复杂度不在“快”而在“可靠”。限流日志如果只放在Redis里Redis重启就全丢了事后没法排查线上事故。而如果把实时判断逻辑全交给MongoDB网关性能天花板又会被拉低。两边都是单一依赖都经不起生产环境的拷打。正确的姿势是分段处理网关实时判断走Redis失败快速返回网关把限流事件异步发给消息队列Kafka/RabbitMQ或直接批量写MongoDB离线统计、分析、报警走MongoDB异步写MongoDB的注意点是别同步写否则实时链路延迟会被数据库抖动拖垮。可以用缓冲队列或Spring的Async批量刷入保证网关主流程零等待。4. 核心代码与部署落地方案纯理论聊多了容易飘这一节把代码和部署直接贴出来照着改就能用。4.1 Redis客户端初始化的几个关键配置用Spring Boot RedisTemplate时序列化器一定要配置正确。很多人踩过这个坑默认的JdkSerializationRedisSerializer会往Redis里写一堆\xac\xed\x00\x05t前缀导致客户端比如Redis Desktop Manager阅读或排查看到的是乱码而且在跨服务反序列化时容易出现版本兼容问题。更适合网关限流场景的是StringRedisTemplate或者自定义GenericJackson2JsonRedisSerializerBean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); template.afterPropertiesSet(); return template; }Lua脚本用DefaultRedisScript封装加载一次反复执行Bean public DefaultRedisScriptLong rateLimitScript() { DefaultRedisScriptLong script new DefaultRedisScript(); script.setLocation(new ClassPathResource(rate_limit.lua)); script.setResultType(Long.class); return script; }调用Long result redisTemplate.execute( rateLimitScript, Collections.singletonList(key), String.valueOf(limit), String.valueOf(System.currentTimeMillis()), String.valueOf(windowMs) ); if (result ! null result 0L) { // 触发限流 throw new RateLimitException(request blocked); }4.2 带Sentinel的网关限流熔断实践热词里“sentinel限流和熔断降级”出现频率很高这里单独说下和网关的结合。阿里开源的Sentinel在做网关流控时官方提供了sentinel-spring-cloud-gateway-adapter可以无缝整合Spring Cloud Gateway。它支持按RouteId、按API分组、按参数Header参数、Query参数、Cookie进行流控并且内置了Dashboard控制台。网关里引入依赖dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-sentinel/artifactId /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-alibaba-sentinel-gateway/artifactId /dependency规则加载用GatewayFlowRuleSetGatewayFlowRule rules new HashSet(); rules.add(new GatewayFlowRule(order-service) .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_ROUTE_ID) .setCount(500) .setIntervalSec(1)); GatewayRuleManager.loadRules(rules);Sentinel的规则如果配置在内存里重启就丢了生产环境建议把规则持久化到Nacos动态刷新不需要重启网关。而且Sentinel本身是支持结合Redis做参数限流的在熔断降级方面也可以把熔断状态写入Redis保证多实例状态一致。不过Sentinel的规则推送和存储官方默认是推到Dashboard内存生产环境要自己扩展。这里有一个我在项目里验证过的简化方案把限流规则存到MongoDB网关启动时读取并加载到Sentinel内存配置变更通过Nacos广播Sentinel监听配置变化后刷新规则。这个方案既享受了Sentinel的链路埋点、滑动窗口计算又把规则的持久化交给了MongoDB比纯内存强很多。4.3 Docker部署Redis主从与MongoDB副本集热词里很多人搜“docker安装redis主从”和“mongodb安装”这种部署坑不少我把关键步骤整理成清单。Redis主从用docker-compose最方便version: 3.8 services: redis-master: image: redis:7.0 container_name: redis-master command: [redis-server, --requirepass, yourpass, --appendonly, yes] ports: - 6379:6379 volumes: - ./redis-master-data:/data redis-slave1: image: redis:7.0 container_name: redis-slave1 command: [redis-server, --slaveof, redis-master, 6379, --masterauth, yourpass, --requirepass, yourpass, --appendonly, yes] depends_on: - redis-master ports: - 6380:6379 volumes: - ./redis-slave1-data:/data redis-slave2: image: redis:7.0 container_name: redis-slave2 command: [redis-server, --slaveof, redis-master, 6379, --masterauth, yourpass, --requirepass, yourpass, --appendonly, yes] depends_on: - redis-master ports: - 6381:6379 volumes: - ./redis-slave2-data:/data生产环境建议至少一主两从再加3个Sentinel做自动故障转移。注意masterauth必须配置否则主从切换后从节点无法认证新主。MongoDB副本集用Docker部署时坑更多。最常见的问题是mongodb安装失败和The installer has encountered an unexpected error这类Windows安装报错。这里先给结论Windows上装MongoDB遇到安装程序卡死或报错多数是路径权限或者VC运行库缺失建议直接用Docker或tar包别用msi安装器。镜像方式version: 3.8 services: mongo1: image: mongo:7.0 container_name: mongo1 command: mongod --replSet rs0 --bind_ip_all --port 27017 ports: - 27017:27017 volumes: - ./mongo1-data:/data/db mongo2: image: mongo:7.0 container_name: mongo2 command: mongod --replSet rs0 --bind_ip_all --port 27017 ports: - 27018:27017 volumes: - ./mongo2-data:/data/db mongo3: image: mongo:7.0 container_name: mongo3 command: mongod --replSet rs0 --bind_ip_all --port 27017 ports: - 27019:27017 volumes: - ./mongo3-data:/data/db启动后初始化副本集rs.initiate({ _id: rs0, members: [ { _id: 0, host: mongo1:27017 }, { _id: 1, host: mongo2:27017 }, { _id: 2, host: mongo3:27017 } ] })副本集对限流审计日志的价值是数据不丢、自动故障切换。单机MongoDB也可能在一次断电后丢失部分日志副本集至少多给了一层保险。4.4 网关限流熔断的完整调用链把Redis、Sentinel、MongoDB串起来完整链路长这样请求进入Spring Cloud Gateway的FilterSentinel Gateway适配器根据路由ID、参数判断是否需要限流Redis固定窗口或滑动窗口Lua脚本做精确计数触发限流后返回自定义错误响应同时异步记录限流日志到MongoDB熔断器定期检查Redis保存的熔断状态执行状态机切换离线统计任务按小时或天从MongoDB聚合数据生成报表这套链路的精髓是Redis是快路径上的闸门MongoDB是慢路径上的账本。闸门要快账本要全二者各司其职系统才能扛住流量波动又能在事后回溯。5. 高频踩坑与排查实录最后把我在实际部署和运维中遇到的高频问题整理成一个速查表每个都是真实踩过的坑。问题症状原因解决方案限流不生效流量穿透实际QPS超过阈值后还在放行应用层先读后写并发下计数丢失必须用Lua脚本或INCR等原子命令Redis连接池耗尽网关响应变慢请求堆积限流逻辑中网络IO耗时太长连接池参数调优增加maxTotal避免同步跨网络调用Lua脚本在集群模式下报错ERR eval not allowed cluster集群模式下Lua脚本的多个key必须处于同一个slot把key的hash tag设计成{rate:user}:1001固定窗口限流临界突发窗口边界处瞬间流量翻倍固定窗口的天然缺陷换成滑动窗口ZSET或令牌桶算法MongoDB主从切换后写入报错not master异常副本集主节点发生变化客户端还在写旧主配置retryWritestrue使用MongoDB连接串的副本集地址限流日志把MongoDB写入打满数据库CPU飙升每个请求都同步写日志写入QPS过高改为异步批量写入适当采样率熔断状态在Redis被误删熔断器不起作用用了EXPIRE给熔断key设置了太短的过期时间熔断key设置永不过期或用persist状态变更由代码控制新增字段后旧数据查询报错部分文档缺字段统计为null文档schema灵活导致数据不一致聚合时用ifNull或迁移脚本补齐字段Redis key没有设置TTL内存增长快速固定窗口或计数器的key忘记设置过期用Lua脚本实现“首次INCR时设置EXPIRE”排障时的排查思路我建议从两个维度入手先看数据是否准确再看性能是否达标。如果限流数据不准确优先看Redis的操作是否原子脚本有没有把INCR和EXPIRE包在同一个redis.call链路里。如果日志查询慢优先检查MongoDB的索引使用情况用explain(executionStats)确认全表扫描还是走索引。如果网关RT升高优先排查Redis连接池和网络延迟用redis-cli --latency先看基础延迟。其实很多问题不是存储本身的bug而是使用姿势不对。比如很多人直接在网关里用MongoDB的Repository接口做数据查询结果在findAll之后再用Stream过滤数据量一大内存直接溢出。正确的做法是用Query条件、分页和索引把过滤放在数据库端完成。还有一个容易忽略的细节监控指标。限流熔断功能上线后要监控Redis的sentinel命令数、MongoDB的写QPS、以及限流触发次数的趋势。限流触发次数异常升高往往意味着别的服务出问题了这个信号比业务报警更早能帮你提前发现上游故障。一句话总结这次选型的心得在实际生产环境里优秀的架构不是“用一个全能的组件解决所有问题”而是“让每个组件做自己最擅长的事”。Redis擅长原子计数和低延迟判断就让它站在流量入口当“门卫”MongoDB擅长灵活存储和聚合统计就让它待在后台当“账房先生”。两者搭配比迷信任何单一存储都可靠得多。如果非要说一个最简单的选型标准实时拦截选Redis事后分析选MongoDB。剩下细节都在这篇文章里了。
返回列表