场景题-面试真题
面试真题
如果项目运行中接口突然变慢,怎么去查找问题所在并解决?从网络,中间件,数据库等方面思考问题
第一步:现象确认与定界(最关键的第一步)
不要一上来就开始看代码,先明确故障范围:
- 是所有接口变慢,还是只有这一个接口变慢?
- 所有变慢:通常是全局性问题(网络、数据库宕机、服务器CPU打满、Redis挂了)。
- 单个变慢:通常是特定SQL慢、特定外部API调用超时、特定代码逻辑死循环或锁等待。
- 是偶尔变慢,还是持续变慢?
- 偶尔变慢:可能是JVM垃圾回收(Full GC)停顿、网络偶发抖动、定时任务抢占资源。
- 持续变慢:可能是底层依赖出问题(如数据库索引失效造成全表扫描)、线程池被打满。
第二步:分层排查(从外向内)
1. 网络与网关层(Network & Gateway)
排查思路:请求是不是根本没到应用,或者堵在了路上?
- 查看 Nginx / 接入层日志:查看 request_time(整个请求耗时)和 upstream_response_time(后端应用响应耗时)。
- 如果 request_time 很大,但 upstream 正常:说明客户端到网关的网络差,或者网关自身扛不住了。
- 如果两者都很大:说明确实是后端应用处理慢。
- 网络带宽与连接:
- 服务器带宽是否被打满(被 DDoS 攻击或遭遇突发大流量)?
- 使用 netstat 查看 TIME_WAIT 或 CLOSE_WAIT 状态的连接数是否异常过高,导致端口耗尽。
- 跨机房/跨云调用:如果涉及多地部署,排查专线是否延迟增加。
2. 应用服务器层(Application / JVM)
排查思路:代码是不是卡住了?服务器资源是不是耗尽了?
- CPU 指标:通过 top 命令查看 CPU 是否 100%。
- 如果是,使用 top -Hp
找到占用最高的线程,再通过 jstack(或阿里开源利器 Arthas 的 thread 命令)将线程转为16进制,查看具体是哪行代码在狂转(可能是死循环、死锁、正则回溯)。
- 如果是,使用 top -Hp
- 内存与 GC(针对 Java):
- 查看监控面板或使用 jstat -gcutil。如果发现 Full GC 非常频繁且耗时很长(STW,Stop The World),应用就会表现为极度卡顿。
- 原因:可能出现了内存泄漏,或者接口突然查出了几十万条数据把内存撑爆了(OOM前兆)。
- 线程池与连接池:
- Tomcat 线程池是否被打满?
- 日志中是否有获取数据库连接超时(Connection Timeout)的报错?
3. 数据库层(Database - 最常见的原因)
排查思路:90% 的接口突然变慢,都是因为 SQL 变慢或数据库锁。
- 查看慢查询日志(Slow Query Log):
- 有没有该接口相关的 SQL 突然变成了慢 SQL?
- 使用 EXPLAIN 分析 SQL 执行计划:
- 索引失效:数据量变大后,MySQL 优化器可能选错了索引,或者隐式类型转换(比如字符串字段传了数字)导致索引失效,变成了全表扫描。
- 锁等待(Lock Wait / Deadlock):
- 是否有其他长事务(比如定时跑批任务)锁住了当前接口需要更新的表或行?
- 使用 show processlist 查看是否有大量处于 Waiting for table metadata lock 或死锁状态的连接。
- 数据库负载:
- DB 服务器的 CPU、磁盘 I/O 是否飙升?连接数是否打满?
4. 中间件与其他依赖层(Middleware & Third-party)
排查思路:Redis、MQ 或调用的第三方 API 拖慢了当前接口。
- Redis(缓存):
- 大Key / 热Key:Redis 是单线程模型,如果接口突然去获取一个几MB的 BigKey,会阻塞整个 Redis,导致所有访问 Redis 的接口瞬间变慢(查看 Redis 的 SLOWLOG)。
- 缓存击穿/雪崩:某个热点缓存刚好过期,导致大量请求瞬间打到数据库上,导致接口变慢。
- 消息队列(MQ):
- 生产消息时网络是否超时?或者 MQ 集群 Broker 磁盘报警导致写入降级?
- RPC / 第三方 HTTP 接口调用:
- 该接口是否调用了微信支付、外部短信接口或内部其他微服务?对方如果变慢,你的接口也会挂起等待。检查日志中是否有 Read Timeout。
一、 数据库层面的解决(最常见,收益最大)
数据库问题通常占接口变慢原因的 70% 以上,解决的核心是减少 DB 的负担。
- 针对“慢 SQL / 索引失效”:
- 添加/优化索引:根据 EXPLAIN 的结果,为查询条件添加合适的联合索引,遵循“最左前缀法则”。
- 改写 SQL:避免在 WHERE 子句中对字段进行函数计算或隐式类型转换(会导致全表扫描);避免使用 SELECT *,只查需要的字段以利用“覆盖索引”。
- 深分页优化:如果是因为 LIMIT 1000000, 10 导致的慢查询,改为基于上一页最大 ID 进行查询(WHERE id > last_id LIMIT 10)。
- 针对“数据库锁等待 / 死锁”:
- 缩短事务粒度:也就是常说的“大事务拆小”。不要把发邮件、调外部接口、复杂计算放在 @Transactional 注解的方法里,只把纯 DB 更新操作放在事务中。
- 按相同顺序访问表:如果是死锁,检查不同业务代码中更新多张表的顺序,保持严格一致,破坏死锁产生的条件。
- 针对“数据库并发被打满 / CPU 飙高”:
- 引入缓存:如果是读多写少的接口,果断在前面挡一层 Redis 缓存,直接拦截掉 90% 的 DB 请求。
- 读写分离 / 分库分表:如果单表数据量已达千万级,且硬件升无可升,需要通过 ShardingSphere 等中间件进行数据拆分。
二、 应用层与代码层面的解决(JVM & 并发)
- 针对“CPU 100% / 代码死循环”:
- 修复 Bug:修改业务逻辑,打破死循环。
- 优化算法/正则:如果是因为复杂的正则表达式引发了“回溯陷阱”导致 CPU 飙满,重写正则或换用简单的字符串匹配。
- 大数据量计算外包:如果接口里有极其复杂的统计计算,改为离线跑批(定时任务算好存入结果表),接口直接读结果。
- 针对“频繁 Full GC / 内存泄漏 (OOM 边缘)”:
- 控制单次查询的数据量:最常见的 OOM 原因是“一次性从数据库查出几十万条数据放入 List 中”。强制要求分页查询,或使用 MyBatis 的 Cursor(游标)流式读取。
- 修复内存泄漏:检查 ThreadLocal 是否在 finally 中执行了 remove()(正如前面提到的分页插件问题);检查是否有不断膨胀的静态集合。
- JVM 调优:适当调大堆内存(-Xmx、-Xms),或者更换更高效的垃圾回收器(如 Java 8 换成 G1,Java 11+ 换成 ZGC)。
- 针对“线程池 / 连接池耗尽”:
- 调参:合理设置 Tomcat 线程池大小(max-threads)和数据库连接池大小(如 HikariCP 的 maximumPoolSize)。注意:连接池不是越大越好,通常依据公式 核心数 * 2 + 硬盘数量。
- 隔离线程池:如果是某个非核心业务(如导出报表)耗尽了线程,导致核心业务(如支付)变慢,应该为不同的业务分配不同的线程池(舱壁隔离模式)。
三、 中间件与外部依赖层面的解决
- 针对“Redis 大 Key / 热 Key 阻塞”:
- 拆分大 Key(BigKey):如果一个 Key 里存了 10MB 的 JSON 或十万个 Hash 字段,Redis 单线程会被卡死。需要将其拆分为多个小 Key,或者只缓存关键信息。
- 应对热 Key(HotKey):如果是类似“微博热搜”的超高频访问 Key,可以在应用服务器本地加一层内存缓存(如 Guava / Caffeine Cache),形成 “本地缓存 + Redis” 的二级缓存架构,减轻 Redis 压力。
- 针对“下游 RPC / 第三方接口变慢”:
- 设置合理的超时时间(Timeout):永远不要无限期等待外部接口!设置 1~3 秒的超时,超时后抛出异常或返回默认值。
- 引入熔断降级(Circuit Breaking):使用 Sentinel 或 Resilience4j。当下游接口错误率升高或响应时间变长时,自动“熔断”,不再调用它,直接返回兜底数据,保护当前应用不被拖死。
- 改为异步解耦(MQ):如果调外部接口只是为了发个通知、增加个积分,坚决不要同步调用。把消息丢到 RabbitMQ 或 Kafka 里,让后台消费者慢慢去执行,接口立刻给前端返回“操作成功”。
四、 网络与架构层面的解决(防御性防护)
- 针对“突发异常流量 / 恶意刷单”:
- 网关限流(Rate Limiting):在 Nginx 或 API 网关层配置令牌桶限流。比如限制单个 IP 每秒最多访问 5 次,超出直接返回 HTTP 429 (Too Many Requests)。
- 接入 WAF / CDN:把静态资源剥离出去放到 CDN,同时利用云厂商的 WAF(Web 应用防火墙)清洗恶意流量。
要是mq占据大量CPU该怎么排查?MySQL占据大量CPU该怎么排查?
问题一:如果 MQ 占据大量 CPU,该怎么排查?
核心思路: MQ 的 CPU 飙高,90% 的原因是**“不合理的生产/消费模式”或“磁盘 I/O 瓶颈”**导致的。
🌟 面试满分话术(三步排查法):
“当监控告警线上 MQ Broker 的 CPU 持续飙高时,我会遵循一套‘先看状态,再追根源,后做优化’的排查 SOP。
第一步:快速定界,看 Broker 状态。
我会立刻登录 MQ 的管理后台(如 RocketMQ-Console 或 RabbitMQ-Management):
- 看 Topic/Queue 积压:是不是某个 Topic 突然积压了几百万条消息?这说明消费能力严重不足,Broker 在忙于消息的存储和管理。
- 看生产者/消费者连接数:连接数是否异常暴增?大量的建连、心跳和断连会消耗大量 CPU。
- 看消息收发 TPS:是不是有某个业务的 Producer 疯了,以平时 100 倍的速率在疯狂生产消息?
第二步:深入分析,追查 CPU 消耗源。
如果管理后台看不出明显问题,我会登录到 MQ 服务器上进行深度排查:
- 定位到具体线程:我会用 top -Hp
命令,找到占用 CPU 最高的那个线程的 tid。 - 打印线程堆栈:接着用 jstack
dump 出整个 JVM 的线程堆栈,然后将刚才的 tid 转换为 16 进制,在堆栈文件里搜索,就能精确地看到这个最耗 CPU 的线程到底在执行哪一行 Java 代码。
- 如果是 RocketMQ,大概率会看到是 Netty 的 I/O 线程在疯狂处理网络请求,或者是 CommitLog / ConsumeQueue 相关的刷盘线程在忙碌。
- 如果是 RabbitMQ,可能看到的是与 queue 内部数据结构操作相关的线程。
第三步:结合现象与堆栈,定位根因并解决。
综合前两步的信息,常见的根因有:
- Case 1:消费者假死。消费者代码有 Bug(如死循环、长事务),导致不消费也不 ACK,消息大量积压。解决方案: 紧急修复并重启消费者应用。
- Case 2:不合理的使用方式。比如 Producer 发送了大量无业务意义的小消息(比如心跳包),或者消费者使用了推(Push)模式但处理极慢,导致 Broker 不断地徒劳推送。解决方案: 优化代码,批量发送,或改为拉(Pull)模式。
- Case 3:磁盘 I/O 瓶颈。如果堆栈显示刷盘线程 CPU 很高,说明磁盘慢是根因。CPU 在忙于等待 I/O 和进行内存页交换。解决方案: 联系运维更换更高性能的 SSD 硬盘,或者开启异步刷盘(牺牲少量可靠性换取性能)。”
问题二:如果 MySQL 占据大量 CPU,该怎么排查?
核心思路: MySQL 的 CPU 飙高,99% 的原因是**“烂 SQL(慢查询)”或“并发锁等待”**导致的。
🌟 面试满分话术(四步排查法):
“MySQL CPU 100% 是线上最常见的数据库故障,我的排查路径非常清晰,总结为**‘一查、二看、三分析、四解决’**。
第一步:查进程列表(show processlist)。
这是我的第一反应。我会立刻连接到数据库实例,执行 show full processlist;。
- 重点关注 State 列:是不是有大量的连接处于 sending data, copying to tmp table, waiting for table metadata lock 等危险状态。
- 重点关注 Time 列:是不是有执行了几百上千秒的“老赖”SQL?
- 如果情况紧急,我会先和业务方确认,果断 kill 掉那个执行时间最长、状态最可疑的查询会话,先让 CPU 降下来,保住数据库不挂。
第二步:看慢查询日志(Slow Query Log)。
processlist 只能看到当前正在跑的。对于已经跑完但很慢的,我会去查看慢查 询日志。
- 定位元凶 SQL:找到在 CPU 飙高时间点前后,执行次数最频繁、扫描行数(Rows_examined)最多的那几条 SQL。通常元凶就在其中。
第三步:分析执行计划(EXPLAIN)。
找到元凶 SQL 后,我会在其前面加上 EXPLAIN,来分析它的执行计划,看看到底“烂”在哪里。
- type 列是不是 ALL? 如果是,说明在搞全表扫描。
- key 列是不是 NULL? 如果是,说明没用上索引。
- Extra 列是不是有 Using filesort 或 Using temporary? 这说明在做额外的排序或使用临时表,性能极差。
第四步:定位根因并解决。
结合 EXPLAIN 的结果,99% 的问题都出在索引上:
- Case 1:索引未创建。WHERE 条件后的字段压根没建索引。解决方案: ADD INDEX。
- Case 2:索引失效。比如在索引字段上用了函数(WHERE a+1=10),或者字符串类型传了数字导致隐式类型转换。解决方案: 改写 SQL,保证索引字段“干净”。
- Case 3:烂 SQL 逻辑。比如深分页(LIMIT 1000000, 10)或者大量 JOIN 没有优化。解决方案: 改为基于上一页 ID 的滚动分页,或者拆分 JOIN。
- Case 4:并发锁争用。如果 processlist 里大量线程在 Lock 状态,说明有长事务锁住了热点行。解决方案: 优化业务逻辑,缩短事务持有时间。”
如何评估接口性能
第一步:明确性能指标(KPIs)
“首先,我会和产品、运维同学一起明确这个接口需要达到的性能目标,主要关注四个核心指标:”
- 吞吐量(Throughput):这是最重要的指标,指的是单位时间内系统能成功处理的请求数,常用 **QPS(每秒查询数)**或 **TPS(每秒事务数)**来衡量。比如,我们要求这个接口在生产环境至少能抗住 500 QPS。
- 响应时间(Response Time):指从发送请求到接收到完整响应所花费的时间。我们会特别关注 平均响应时间 和 P95/P99 响应时间(即 95% 或 99% 的请求都在多少毫秒内完成)。例如,要求 P99 响应时间必须在 200ms 以内。
- 并发用户数(Concurrent Users):系统能同时处理多少个用户的请求。
- 错误率(Error Rate):在压力下,请求失败的比例,通常要求低于 0.1%。
第二步:设计测试方案与准备环境
“明确目标后,我会设计详细的测试方案:”
- 选择工具:对于常规的 HTTP 接口,Jmeter 是一个非常成熟且功能强大的选择。如果需要极高的并发量或者进行代码级的定制,我也会考虑使用 Gatling 或 K6。
- 准备测试环境:测试环境的硬件配置(CPU、内存、网络)需要和生产环境保持一致或按比例缩放,以保证测试结果的参考价值。同时,需要隔离测试环境,避免影响线上业务。
- 准备测试数据:准备足够多、足够真实的测试数据。比如,测试一个用户查询接口,需要提前造好大量的用户数据,避免缓存命中率过高导致结果失真。
第三步:执行压力测试
“我会使用 Jmeter 执行不同场景的测试:”
- 基准测试:在低压力下(比如 10 个并发用户)运行,获取接口在理想状态下的性能基线。
- 负载测试:逐步增加并发用户数,观察各项性能指标的变化,直到找到系统的性能拐点(比如 QPS 开始下降,响应时间急剧升高)。
- 压力/极限测试(Stress Test):继续加大压力,直到系统出现大量错误或崩溃,目的是找到系统的最大容量和瓶颈。
- 稳定性测试(Soak Test):在某个较高的压力水平下(比如 80% 的最大容量),长时间运行(如 8 小时),观察系统是否存在内存泄漏、连接池耗尽等问题。
第四步:分析瓶颈与调优
“测试完成后,最关键的是分析报告和定位瓶颈:”
- 分析 Jmeter 报告:查看聚合报告中的吞吐量、响应时间和错误率是否达标。
- 结合后端监控:在压测过程中,我会同时监控后端的 APM 系统(如 SkyWalking, Pinpoint)、服务器的 CPU/内存/磁盘I/O/网络带宽、数据库的慢查询日志和连接数、JVM 的 GC 状态等。
- 定位瓶颈:通过综合分析,定位性能瓶颈是在代码逻辑、数据库查询、中间件(如 Redis/MQ)还是网络层面,然后进行针对性调优。
支付场景的全流程,高并发支付怎么设计
第一部分:支付全流程(正向时序与核心角色)
一个典型的第三方支付(如微信/支付宝)全流程,主要涉及三个角色:用户端(App/H5)、商户后端(我们的支付系统)、第三方支付网关。
核心流转步骤如下:
- 创单与下单(前端 -> 后端): 用户点击“去支付”,前端将业务订单号(如外卖单号)传给我们的支付后端。
- 生成支付单与预下单(后端内部):
- 后端校验业务单状态(是否已关单)。
- 在核心的 payment_order (支付单表) 中生成一笔唯一的“待支付”流水单。
- 请求三方支付网关(后端 -> 三方):
- 后端带着这个支付单号、金额、商户秘钥等,调用微信/支付宝的“统一下单 API”。
- 三方网关返回一个 prepay_id (预支付交易会话标识) 或一段可以拉起收银台的参数/URL。
- 拉起收银台(后端 -> 前端 -> 三方):
- 后端将参数组装签名后返回给前端。前端唤起微信/支付宝收银台,用户输入密码完成扣款。
- 异步回调通知(三方 -> 后端 - 最关键的一步):
- 用户在三方扣款成功后,三方网关会根据我们提前配置好的 notify_url,向我们的后端发送异步的 HTTP POST 回调请求。
- 处理回调与业务状态流转(后端内部):
- 后端收到回调,必须先验签(验证确实是微信发来的,不是黑客伪造的)。
- 验证金额和状态无误后,将 payment_order 表的状态改为“支付成功”。
- 最后,通知上游业务系统(如外卖订单变为“已支付”,通知商家接单),并向三方网关返回 SUCCESS 应答。
第二部分:高并发支付怎么设计?(性能与解耦)
在双十一、秒杀等场景下,瞬间涌入海量的创单和回调请求,如果架构设计不当,数据库会被瞬间打死。
针对高并发,我的核心设计原则是:“动静分离、极简核心、异步削峰解耦”。
1. 极轻的交易核心(数据库层减负)
- 痛点: 如果在创单或处理回调时,还要去同步更新外卖订单状态、发优惠券、加积分,那么数据库事务会非常大,行锁竞争极高,并发量根本上不去。
- 设计: 支付核心库(如 payment_order 表)只做最纯粹的资金流水记录。
- 表结构极其简单(只存支付单号、业务单号、金额、状态)。
- 为了抗住极高并发的 Insert/Update,这层数据库必须进行分库分表(Sharding),通常按 user_id 或内部 payment_id 的哈希值路由。
2. 异步削峰与业务解耦(回调处理层的救星)
- 痛点: 微信/支付宝的并发回调量可能极其恐怖。如果我们在回调接口里同步去查库、调外卖服务,接口一旦变慢,微信会认为我们挂了,从而触发疯狂的指数级重试,形成雪崩。
- 设计:引入消息队列(MQ,如 RocketMQ / Kafka)。
- 当收到三方回调并完成最基本的验签后,我们仅仅更新支付单表的状态(极快),然后立刻给微信返回 SUCCESS。
- 同时,将“支付成功”的事件打包成一条消息,发送到高可用的 MQ 中。
- 外卖系统、积分系统、发票系统作为 MQ 的消费者,各自以自己能承受的速度去消费这条消息,完成后续复杂的业务逻辑(发货、加积分)。这实现了完美的流量削峰和架构解耦。
3. 缓存前置与预创单(优化链路)
- 在用户进入收银台页面前,我们就异步提前在后端生成了待支付单并放入 Redis(设置较短的过期时间,如 15 分钟)。当用户真正点击支付时,直接去调三方接口,减少了一次关键路径上的数据库写操作。
第三部分:支付系统的“保命符”——一致性与防坑机制
除了快,支付更要命的是安全和准确。以下这几条红线是在设计时必须向面试官强调的(这也是最能展现资深经验的地方):
1. 绝对的防重与幂等性(第一生命线)
- 场景: 用户手抖点了两次支付;或者网络抖动,微信重发了 5 次回调。
- 设计:
- 前端防抖: 点击支付后按钮置灰。
- 创单幂等: 后端根据上游传来的业务订单号(biz_order_id)做唯一性校验(建唯一索引或用 Redis 分布式锁 SETNX),保证一个业务单绝不可能生成两笔成功的支付单。
- 回调幂等(重中之重): 收到微信回调时,必须先查 payment_order 表。如果状态已经是“支付成功”,必须直接返回 SUCCESS 终止处理,绝不能再次发送 MQ 消息去让下游重复发货。
2. 主动轮询对账机制(防掉单的最后防线)
- 场景(掉单): 用户明明在微信里付了钱,但由于我们机房断网或者服务器 OOM 重启,微信发来的回调通知全丢了。用户干等,订单一直显示“待支付”。
- 设计: 绝不能死等回调!必须有一个后台的定时补偿任务(Job)。
- 它会定期扫描 payment_order 表中创建时间超过 3 分钟但状态仍为“待支付”的记录。
- 拿着这些单号,主动去调用微信/支付宝的“交易查询 API”。
- 如果查到确实支付成功了,我们的系统会走正常的支付成功流转逻辑,从而完成补单,保证了数据的最终一致性。
要实现一个微信红包,(10个红包,总额20元),有哪些问题,你会怎么解决
核心问题 1:红包拆分算法(如何保证总额绝对是 20 元,且分配随机?)
你提到的“预设一个随机数组”是非常正确的方向,这也是业界主流的做法(预分配策略)。
- 痛点: 如果在用户抢红包的那一刻(实时计算)去随机生成金额,不仅会消耗大量的 CPU 资源导致接口变慢,而且很难控制最后一个红包的金额正好把剩余的钱凑齐(可能会出现小数点精度丢失或者最后一个红包金额为负数的极端 Bug)。
- 解决方案(二倍均值法 + 预分配):
- 算法选择: 我会采用经典的“二倍均值法”来生成随机金额。
- 公式:每次抢到的金额 = 随机区间 (0.01, (剩余总金额 / 剩余红包数) * 2)。
- 这个算法能保证每个红包金额的数学期望大致相等,比较公平,且绝对不会超出总额。
- 预先生成与入库(发红包时):
- 当发红包的人点击“发送”并扣除 20 元后。
- 后端在执行实际业务前,立刻通过二倍均值法,预先生成一个包含 10 个金额的数组(如 [1.5, 3.2, 0.8… 2.1]),确保这 10 个数字加起来绝对等于 20.00 元。
- 将这 10 个确定的金额序列化后,推送到 Redis 的一个 List 或 Set 结构中(这也就是你说的预设数组)。
- 算法选择: 我会采用经典的“二倍均值法”来生成随机金额。
核心问题 2:高并发抢红包的性能瓶颈与防超发(如何保证 10 个人抢完,第 11 个人立刻被拦截?)
这是秒杀场景的标配问题。
痛点: 100 个人同时点击一个只剩 1 个红包的记录,如果直接去查 MySQL 的 remain_count > 0 然后 UPDATE remain_count = remain_count - 1,数据库的行级锁(悲观锁)会被瞬间打满,系统直接崩溃,且极易超发(扣成负数)。
解决方案(Redis 内存操作 + Lua 脚本原子性):
拦截 90% 的无效流量: 既然我们预先在 Redis 里生成了一个包含 10 个金额的 List(比如叫 redpacket:pool:12345)。
抢的过程就是出队(Pop): 当用户点击抢红包时,请求到达后端。我们不需要去查数据库,而是直接调用 Redis 的 LPOP 命令从 List 中弹出一个金额。
原子操作保证防超发:
Redis 的 LPOP 是单线程原子操作,绝对不会出现两个线程弹出一个金额的情况。
如果 LPOP 成功返回了一个金额(如 3.2),说明用户抢到了这 3.2 元。
如果 List 已经被弹空了,LPOP 返回 nil。此时说明红包已经被前 10 个人抢光了,直接给用户返回“手慢了,红包派发完毕”。
优势: 这种方案把高并发的查库存和扣库存操作,变成了一次纯内存的
1
O(1)O(1)出队操作,性能极高,且物理上杜绝了超发的可能(因为池子里就 10 个数字)。
核心问题 3:幂等性与防重抢(一个人不能抢两次)
- 痛点: 用户 A 抢到了红包,但因为网络卡顿,他疯狂连点了 5 次。或者系统超时重试,导致他抢了两个名额。
- 解决方案(Redis Set 防重):
- 在执行 LPOP 之前或之后(最好使用 Lua 脚本保证原子性),必须利用 Redis 的 Set 结构记录已经抢到红包的用户。
- 比如维护一个 redpacket:users:12345 的 Set。
- 用户请求来时,先执行 SISMEMBER redpacket:users:12345 {userId} 检查他是否在里面。如果在,直接返回“您已经抢过该红包了”。如果不在,再去执行 LPOP 抢钱,并用 SADD 将他加入 Set。
核心问题 4:资金的绝对安全与异步落盘(抢到了,但入账失败怎么办?)
这是支付架构师最关心的问题。Redis 只是内存缓冲,最终用户的余额必须落入 MySQL。
- 痛点: 如果用户在 Redis 里成功 LPOP 到了 3.2 元,但此时由于后端服务宕机,或者往 MySQL 插入红包流水记录时数据库挂了,这 3.2 元就凭空消失了,用户没拿到钱,发红包的人的钱也扣了。
- 解决方案(MQ 异步解耦 + 最终一致性补偿):
- 异步化核心链路:
- 当 Redis LPOP 成功并且防重校验通过后,不要在抢红包的 API 里同步去操作 MySQL。
- 立即给前端返回“恭喜你,抢到 3.2 元”(用户体验极佳)。
- 同时,将这条抢红包的记录(包含红包 ID、用户 ID、金额 3.2 元)作为一条消息,发送到高可用的 消息队列(如 RocketMQ/Kafka) 中。
- 消费者落盘:
- 后台有专门的消费者监听这个队列,以数据库能承受的速率,平滑地将这些记录 INSERT 到 MySQL 的红包明细表,并 UPDATE 用户的账户余额。
- 极端兜底:
- 如果在发 MQ 的瞬间应用挂了怎么办?这就需要依赖本地日志记录,或者更严格的基于本地消息表的最大努力通知事务,确保 Redis 里的成功记录最终一定能反映在数据库中。
- 异步化核心链路:
核心问题 5:拆包与冷热数据分离(红包过期退回)
- 痛点: 红包 24 小时未领完需要退回给发件人。如果用定时任务每分钟去扫庞大的数据库找过期红包,性能很差。
- 解决方案:
- 发送红包时,利用 MQ 的延迟消息队列(或 Redis 的 ZSet 发送一个延迟 24 小时的任务)。
- 24 小时后,消费者收到延迟消息,去 Redis 检查 redpacket:pool:12345 的剩余长度。
- 如果有剩余,计算剩余总金额,执行退款操作给原发件人,并销毁 Redis 中的相关 Key,完成冷数据的清理。
异步领卷是保证的弱一致性,怎么保证强一致性(分布式事务)
在“Redis 预扣库存 + MQ 异步下单”这个场景下,如果业务要求强一致性(Strong Consistency),那意味着在给用户返回“成功”的那一刻,所有系统的数据必须是一致的。
这是分布式事务的鼻祖,通过**协调者(Coordinator)**来保证原子性。
框架:使用 Seata AT/XA 模式 或基于 JTA 的事务管理器。
流程:
- 准备阶段 (Prepare):
- 事务协调者通知 Redis 和 MQ“准备提交”。
- Redis 锁定库存(但不是真的扣),MQ 准备好消息(但不投递)。
- 提交阶段 (Commit):
- 如果都准备好了,协调者通知大家一起“提交”。
- 准备阶段 (Prepare):
为什么高并发不用:
- 性能极差:2PC 是同步阻塞的,在整个过程中会长时间占用资源锁。
- 依赖支持:需要 Redis 和 MQ 都支持 XA 协议,这在开源组件中支持度不一。
做法:把 Redis 扣减、MQ 发送、MySQL 写入绑在一个全局大事务里。
后果:性能崩塌。
- 为了等所有人确认,用户点一下要等几秒。
- Redis 的高性能完全被拖累了。
- 结论:高并发秒杀场景绝对不用强一致事务。
我们采用的是基于 MQ 的最终一致性方案。允许 Redis 库存和 MySQL 数据在几秒钟内短暂不一致,但通过 MQ 的重试机制和死信队列,保证数据最终一定会落库。对于用户体验,我们通过前端展示‘处理中’来掩盖这个延迟。”