一、问题背景
当大量请求同时写入数据库时,数据库可能出现以下问题:
- 连接数耗尽:大量请求占用数据库连接,新请求无法获取连接。
- CPU 和磁盘 I/O 过高:数据库处理不过来,SQL 执行时间变长。
- 锁竞争严重:多个事务同时修改相同的数据,互相等待。
- 请求超时:数据库响应变慢,应用请求不断超时。
- 重试导致流量放大:请求超时后不断重试,进一步加重数据库压力。
- 服务雪崩:数据库变慢导致应用线程堆积,最终影响整个系统。
核心问题:请求进入数据库的速度超过了数据库的处理能力。
因此,不能只依赖数据库自身配置,还需要在应用和数据库之间建立流量控制机制
复习:SQL 慢查询怎么排查
二、整体解决方案
推荐的整体链路:
用户请求 → 网关限流 → 应用服务 → 消息队列 → 消费者 → 数据库
同时配合 Redis 缓存、数据库连接池、SQL 优化、监控告警等措施。
1. 网关:入口限流
作用:在请求进入业务服务之前,限制流量。
常见措施:
- 按用户、接口或业务维度设置请求速率限制。
- 限制突发流量,超过阈值时拒绝请求或返回友好提示。
- 对非核心业务进行降级,优先保障核心业务。
例如:某个接口正常情况下每秒处理 2,000 个请求,突然收到 10,000 个请求,可以通过限流机制控制进入后端的流量。
注意:具体限流阈值需要根据压测结果和业务要求确定。
2. 消息队列:削峰填谷
常见中间件:Kafka、RabbitMQ。
作用:将突发的写入请求暂存在消息队列中,让数据库按照自身的处理能力逐步消费。
原本的同步写入:
用户请求 → 应用服务 → 数据库
改造后的异步写入:
用户请求 → 应用服务 → 消息队列 → 消费者 → 数据库
例如:
数据库稳定处理能力为每秒 2,000 条写入,突然来了每秒 10,000 条写入请求。
此时可以将请求写入消息队列,由消费者控制写入速度,而不是让所有请求同时冲击数据库。
需要注意:
- 消息队列不是无限容量的,必须监控积压量和消息延迟。
- 消费者不能无限增加并发,否则仍然会压垮数据库。
- 应设置队列容量或积压告警,并制定过载处理方案。
- 需要考虑消息确认、失败重试、死信处理和重复消费。
- 只有适合异步处理的业务才能这样改造;必须立即返回最终写入结果的场景,需要重新设计接口语义。
3. 消费者:控制数据库写入速度
作用:决定消息队列中的数据以多快的速度进入数据库。
主要措施:
- 限制消费者数量和并发线程数。
- 限制每个消费者同时执行的数据库任务数量。
- 根据数据库 CPU、连接数、响应时间动态调整消费速度。
- 合理批量写入,减少频繁执行 SQL 和提交事务的开销。
- 避免无限重试,失败消息采用退避重试,必要时进入死信队列。
例如:
数据库经过压测,发现每秒稳定处理 2,000 条写入。可以先将消费者目标处理速率设为每秒 1,500 条,再根据实际监控逐步调整。
这只是示例,不能直接将该数值用于所有生产环境。
重点:消息队列负责缓冲,消费者负责控速,数据库负责最终落盘。
4. Redis:缓存与分布式限流
Redis 主要有两种用途。
第一,缓存热点查询结果,减少数据库的读取压力。
例如:商品详情、配置数据等被大量重复读取,可以优先从 Redis 获取。
第二,使用 Redis 计数器或令牌桶等机制实现分布式限流,避免多个应用实例各自限流后,总流量仍然超过数据库承载能力。
注意:
- Redis 缓存主要减少读压力,不能直接解决所有写入压力。
- 对库存、余额等关键数据,不能为了减轻数据库压力而随意使用缓存代替数据库事务。
- 分布式限流需要处理 Redis 故障、限流规则和关键业务的可用性。
5. Sentinel:限流、熔断与降级
Sentinel 可用于 Java 应用的流量控制。
主要作用:
- 限流:请求过多时限制进入业务方法的流量。
- 熔断:依赖服务持续异常时,暂时停止部分调用,避免故障扩散。
- 降级:必要时关闭非核心功能,优先保障核心业务。
例如:数据库持续超时,可以限制非核心查询接口的请求,避免业务线程持续等待数据库。
注意:Sentinel 不是数据库本身的保护机制,也不能代替消息队列的异步缓冲。
6. ShardingSphere:读写分离与分库分表
当数据库长期达到处理能力或存储容量上限,经过 SQL 优化、缓存和流量控制后仍然无法满足业务需求,可以考虑扩展数据库架构。
- 读写分离:将部分查询请求分配给只读副本,降低主库读取压力。
- 分库分表:按照用户 ID、订单 ID 等合适的分片键,将数据分布到多个数据库或数据表中。
- 水平扩展:增加数据库节点,提高整体处理能力。
注意:
- 读写分离主要缓解读压力,不能直接解决主库写入过载。
- 分库分表会增加事务、跨分片查询、数据迁移和运维复杂度。
- 不能把分库分表当作流量过大时的第一反应。
三、数据库自身的保护措施
中间件之外,还需要从数据库自身进行优化。
1. 限制连接数
设置合理的数据库最大连接数,并通过应用连接池控制每个实例的连接数量。
注意:多个应用实例的连接池总量不能超过数据库可承受范围。
连接数限制可以避免连接资源被耗尽,但不等于严格限制所有 SQL 的执行并发。
2. 优化 SQL 和索引
- 为高频查询和更新条件建立合适的索引。
- 避免不必要的全表扫描。
- 避免一次查询或更新过多无关数据。
- 分析慢 SQL,减少不必要的数据库访问。
- 关注索引维护开销,避免建立过多无用索引。
3. 控制事务
- 缩短事务执行时间。
- 避免在事务中执行耗时的外部网络请求。
- 减少长事务和不必要的锁竞争。
- 避免大事务一次修改过多数据,必要时分批处理。
4. 合理批量写入
对于大量数据写入,可以采用批量插入或批量更新,减少频繁执行 SQL 的开销。
但要控制批次大小和事务范围,避免单批操作过大,造成锁等待、日志压力或回滚成本过高。
5. 监控与告警
重点关注:
- 数据库 CPU 和内存使用情况。
- 磁盘 I/O 和存储空间。
- 活跃连接数和连接池等待数。
- 慢 SQL、锁等待和死锁。
- 事务执行时间。
- 消息队列积压量、消费延迟和失败率。
- 应用请求超时率与重试次数。
当指标达到阈值时,应及时限流、降低消费速度或暂停非核心任务,并排查真正的瓶颈。
四、不同问题如何选择方案?
| 具体问题 | 优先考虑的方案 |
|---|---|
| 突发写入流量过大 | 消息队列削峰填谷、消费者控速 |
| 请求入口流量过大 | 网关限流、Sentinel |
| 热点查询过多 | Redis 缓存、查询优化 |
| 数据库连接耗尽 | 连接池限制、连接数监控 |
| 慢 SQL、锁竞争严重 | 索引优化、SQL 优化、缩短事务 |
| 数据库长期达到性能上限 | 压测、容量评估、必要时扩容 |
| 单库存储或写入能力不足 | 评估分库分表 |
| 数据库超时后重试风暴 | 超时控制、退避重试、重试上限、熔断 |
五、必须考虑的可靠性问题
引入消息队列后,并不是所有问题都自动解决,还需要考虑以下事项。
1. 消息重复消费
消费者可能已经完成数据库写入,但确认消息之前发生故障,导致消息再次被消费。
解决方案:
- 为业务请求设置唯一业务 ID 或幂等键。
- 在数据库中建立唯一约束,或使用可靠的幂等记录。
- 使用事务或适当的状态流转,避免重复执行关键业务。
2. 消息丢失
需要合理配置消息持久化、生产者确认、消费者确认和重试机制。
具体保障取决于消息队列的配置与故障场景,不能仅凭使用了消息队列就认为绝对不会丢消息。
3. 消息积压
如果消息进入队列的速度长期高于消费速度,积压会越来越大。
解决方案:
- 监控积压量和消息延迟。
- 排查慢 SQL、数据库瓶颈和消费失败。
- 在数据库有余量时适当提高消费者并发。
- 必要时限制入口流量,暂停非核心业务。
- 评估队列容量、数据保留时间和过载处理策略。
4. 数据一致性
如果接口采用异步写入,需要明确接口返回的含义:是消息已被可靠接收,还是数据已真正写入数据库。
如果业务要求最终一致性,还要设计失败补偿、状态查询和异常恢复机制。
六、面试口播答案
如果数据库写入流量过大,我会从入口流量控制、中间件削峰和数据库自身保护三个层面处理。
首先,在网关或应用层设置限流,避免请求无限进入系统。
其次,对于突发的大量写入,我会引入 Kafka 或 RabbitMQ 消息队列,将同步写入改成异步处理。由消费者控制并发和消费速度,让数据库按照自身的承载能力逐步处理,实现削峰填谷。
然后,在数据库层面限制连接数,优化 SQL、索引和事务,必要时采用批量写入。同时使用 Redis 缓存热点查询,减少不必要的数据库访问。
如果数据库持续过载,还可以使用 Sentinel 对非核心业务进行限流和降级;如果经过优化后,单库容量或处理能力仍然不足,再评估读写分离、分库分表或扩容。
最后,我会监控数据库负载、慢 SQL、消息积压和请求超时,并通过幂等、失败重试和异常恢复保证数据可靠性。
七、记忆框架
记住这六步:
- 入口限流:不要让流量无限进入。
- 队列削峰:把突发写入暂存起来。
- 消费控速:控制进入数据库的速度。
- 数据库优化:连接、SQL、索引、事务。
- 缓存与扩容:减少不必要的访问,必要时提升容量。
- 可靠性保障:幂等、重试、积压监控、数据一致性。
最终要表达的核心是:先控制流量,再提高处理效率,最后根据实际瓶颈扩容,而不是一上来就分库分表。
相关文章
- 数据库流量过大:网关入口限流的四种工程方案:入口限流的具体工程实现与选型对比。
- 方案一:Spring Cloud Gateway 入口限流:网关令牌桶限流的完整实施步骤。
- 方案三:Redis + Lua 分布式限流:多实例共享限流额度的实现方式。