一次高并发压垮系统的排查与重生(上)

在一次常规的权益包过期批量退款中,我们的履约系统遭遇了预料之外的流量洪峰。短时间内涌入的1万多笔退款请求,让数据库CPU持续满载十分钟,部分请求失败。本文将完整回顾我们如何定位问题、实施紧急修复,并进行系统化加固的全过程。

1. 复盘"案发现场"

1.1. 业务背景

商城售卖了一批权益包的单,对于消费者未使用的在过期当天需要自动退款。这就好比你买了一张月底过期的"咖啡月卡"(权益包),但一直没去喝。公司很厚道,承诺:过期当天,自动退款!问题来了:如果10万个用户同时有这样的权益包,系统会在过期日瞬间发起10万笔退款。这就是我们面临的业务现实:定时、批量、高并发的"脉冲式"流量冲击。

1.2. 灾难时刻

时间线还原:

T+0s:商城侧开始推送过期权益包的退款申请。

T+10s:1万笔退款请求已抵达履约系统接口,平均QPS高达1000。

T+30s:数据库服务器CPU飙升至100%,并持续"僵死"。

T+5分钟:部分商城请求超时。

关键发现:

通过配置的监控告警,运维迅速协助锁定了一条"死亡慢SQL"。

sql 复制代码
-- 示例sql
SELECT * FROM order
WHERE user_id = ? 
  AND status = 'pending' 
  AND package_type = ? 
ORDER BY create_time DESC;

它的致命问题:

  • 缺少关键索引,导致全表扫描。
  • 在当时生产环境300w+单量的订单表中,单次查询就消耗数秒。
  • 当1000QPS并发执行时,大量慢sql让CPU使用率飙到100%,部分web请求出现超时。

2. DBA的"黄金5分钟"

2.1. 第一原则:先恢复,再优化

面对线上事故,我们的应急响应流程:

  1. 快速定位:通过监控告警和数据库慢查询日志,快速定位到具体SQL。
  2. 紧急处理:DBA立即执行索引添加。
sql 复制代码
-- 紧急添加的索引示例
ALTER TABLE refund_order 
ADD INDEX idx_user_status_type (user_id, status, package_type);

增加索引后,CPU虽从100%下降,但仍处于60%+的高位,说明系统只是"退烧",并未"痊愈"。

3. 为什么我们的系统如此脆弱

3.1. 业务链路

在了解系统脆弱性之前先看看以下简化版的权益包退款业务链路,大致逻辑是先将商城报文存入中间表a,再通过定时任务消除平台差异后存入中间表b并通过MQ解耦生成退单,再由退单生成退款单。

3.2. 架构层面的"三道防线"全部失守

第一道防线(接入层):只有一扇"总门"

  • 原有方案:全局网关限流5000 QPS。

  • 问题:这个限流是针对所有接口的,而退款接口只是其中之一。当退款流量暴涨时,其他接口的额度也被占用,且无法阻止退款接口自身的内部风暴。

第二道防线(业务层):自己制造的"二次风暴"

  • 原有流程:中间表a转换生成中间表b的Task,一次性查询出5000条待处理记录,然后分批并发处理。

  • 放大效应:外部1000QPS的请求,进入系统后,又被这个Task放大成内部并发数据处理请求,进一步冲击数据库。

第三道防线(数据层/MQ消费):有枪无"准星"

  • 原有方案

RocketMQ消费者:单次拉取1000条消息。

框架层:全局共享消费线程池。

  • 致命缺失:没有接口层面的限流,一旦消息堆积,所有线程都会疯狂消费同一个"退款"Topic,再次打满数据库。

4. 构建全链路"防波堤"

4.1. 接入层

在流量网关层设置接口限流(基于Resilience4j),防止系统过载,当服务调用失败率超过阈值时,自动熔断,防止故障扩散。

4.2. 业务层

调整中间表a转换生成中间表b的Task单次查询的单量,比如1min执行一次,单次能处理600单,则结合服务处理能力设置单次查询单量为600,避免一次捞取大量数据。

4.3. 数据层

4.3.1. 慢sql优化

通过调用链分析耗时发现慢sql是主要的耗时大头,因此通过增加/调整索引、数据归档、数据缓存、循环查代码逻辑改造等手段解决百万级大表查询慢的问题。

4.3.2. MQ消费限流

中间件层面和MQ消费者框架代码层面都只有全局限流设置,因此,利用项目中的Sentinel组件对MQ消费逻辑进行接口级限流。示例代码如下:

java 复制代码
@Slf4j
@Service
@RocketMQMessageListener(consumerGroup = "refund-consumer-group", topic = "REFUND_TOPIC")
public class RefundMessageConsumer implements RocketMQListener<MessageExt> {

    // 定义资源名,用于Sentinel限流
    private static final String RESOURCE_NAME = "processRefundMessage";
    
    @Override
    @SentinelResource(
        value = RESOURCE_NAME,
        blockHandler = "handleFlowLimit", // 流控降级方法
        blockHandlerClass = {RefundMessageConsumerBlockHandler.class}
    )
    public void onMessage(MessageExt message) {
        // 核心退款业务逻辑
        refundService.processRefund(parseMessage(message));
    }
}

// 流控降级处理器
public class RefundMessageConsumerBlockHandler {
    public static void handleFlowLimit(MessageExt message, BlockException ex) {
        log.warn("触发消费限流,消息将延迟处理: {}", message.getMsgId());
        // 可选:将消息重新投递或记录日志,稍后重试
        throw new RuntimeException("系统繁忙,请稍后重试");
    }
}

限流规则配置(通过Sentinel Dashboard动态设置):

  • QPS限流 :将processRefundMessage资源的QPS阈值设置为与数据库处理能力匹配的值(如200)。

  • 线程数限流:限制同时处理退款消息的线程数,防止线程池被打满。


下篇预告

优化方案纸上谈兵?不!下一篇带你直击 压测战场:如何用"真实数据"验证系统扛得住 **,**看如何通过全链路压测,让优化效果"看得见、摸得着",并分享我们从这次事故中提炼出的高并发设计黄金法则。

相关推荐
C雨后彩虹2 小时前
字符串拼接
java·数据结构·算法·华为·面试
小北方城市网2 小时前
第7课:Vue 3应用性能优化与进阶实战——让你的应用更快、更流畅
前端·javascript·vue.js·ai·性能优化·正则表达式·json
C雨后彩虹2 小时前
ConcurrentHashMap入门:高并发场景的 HashMap替代方案
java·数据结构·哈希算法·集合·hashmap
weixin_425023002 小时前
Spring boot 2.7.18使用knife4j
java·spring boot·后端
产幻少年2 小时前
面试题八股
java
wanghowie2 小时前
01.08 Java基础篇|设计模式深度解析
java·开发语言·设计模式
Data_agent2 小时前
京东商品价格历史信息API使用指南
java·大数据·前端·数据库·python
Knight_AL3 小时前
Java 17 新特性深度解析:记录类、密封类、模式匹配与增强的 switch 表达式对比 Java 8
java·开发语言
最贪吃的虎3 小时前
Spring Boot 自动装配(Auto-Configuration)深度实现原理全解析
java·运维·spring boot·后端·mysql