系統流量突增十倍,該怎么辦?
前言
最近看到一道面試題:假如線上系統流量突然增加了10倍,你該怎么辦?
感覺挺有意思的。
其實我在之前的工作中,也經常遇到流量突增的情況,特別是在中午和晚上的用餐高峰期,流量會突增幾倍。
今天這篇文章就跟大家好好聊一下這個問題,希望對你會有所幫助。
1.先快速解決問題
1.1 緊急擴容
如果發現系統真的扛不住了,第一時間應該是擴容。
現在云計算這么方便,擴容就是點幾下鼠標的事。
圖片
為什么要先擴容?
因為這是最快見效的方法。
你可能需要5分鐘分析代碼,但擴容只需要1分鐘。
先保住系統,再慢慢優化。
1.2 快速定位問題
當監控告警響起時,千萬別慌!首先要快速定位瓶頸點。
我有個"5分鐘排查法":
# 第1分鐘:看整體負載
top -c # 按CPU排序,看哪個進程最耗資源
# 第2分鐘:看網絡連接
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
# 第3分鐘:看JVM狀態
jstat -gcutil <pid> 1000 # 看內存回收情況
# 第4分鐘:看接口QPS
tail -f access.log | awk '{print $7}' | sort | uniq -c | sort -nr | head -10
# 第5分鐘:看錯誤日志
tail -n 100 error.log | grep -E "(ERROR|Exception)"2.分層防御
2.1 網關層
它是流量入口的第一道防線。
網關就像小區的門衛,先把不必要的訪客擋在外面。
Spring Cloud Gateway限流配置示例:
@Bean
public RedisRateLimiter redisRateLimiter() {
// 每秒允許1000個請求,最大允許2000個
return new RedisRateLimiter(1000, 2000);
}
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("order_route", r -> r.path("/api/orders/**")
.filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
.uri("lb://order-service"))
.build();
}這個配置的作用:當訂單接口的請求量超過每秒1000次時,多余的請求會被直接拒絕,保護后臺服務不被沖垮。
2.2 服務層
我們需要保護核心業務。
服務層就像公司的各個部門,需要保護核心部門正常運轉。
熔斷降級示例:
@Service
publicclass OrderService {
@Autowired
private ProductClient productClient;
@CircuitBreaker(name = "productService", fallbackMethod = "getProductFallback")
public Product getProduct(Long id) {
// 調用商品服務
return productClient.getProduct(id);
}
// 降級方法:當商品服務不可用時執行
private Product getProductFallback(Long id, Throwable t) {
log.warn("商品服務不可用,返回緩存數據,商品ID: {}", id);
// 返回緩存中的默認商品信息
returnnew Product(id, "默認商品", 0.0);
}
}熔斷器的工作原理:

當商品服務的失敗率超過50%時,熔斷器會打開,后續請求直接走降級邏輯,避免雪崩效應。
2.3 緩存層
通過緩存減少數據庫壓力。
緩存就像你的筆記本,先把常用的東西記下來,不用每次都去翻大詞典。
多級緩存架構:
圖片
代碼實現:
@Service
publicclass ProductService {
// 本地緩存
private Cache<Long, Product> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
public Product getProduct(Long id) {
// 1. 先查本地緩存
Product product = localCache.getIfPresent(id);
if (product != null) {
return product;
}
// 2. 查Redis
product = redisTemplate.opsForValue().get("product:" + id);
if (product != null) {
localCache.put(id, product);
return product;
}
// 3. 查數據庫
product = productRepository.findById(id);
if (product != null) {
redisTemplate.opsForValue().set("product:" + id, product, 30, TimeUnit.MINUTES);
localCache.put(id, product);
}
return product;
}
}這樣設計后,90%的請求在本地緩存就返回了,9%的請求走到Redis,只有1%的請求會到數據庫。
2.4 數據庫層
它是最后的堡壘。
數據庫就像銀行的保險庫,訪問要特別小心。
讀寫分離:把讀操作和寫操作分開到不同的數據庫
# application.yml 配置
spring:
datasource:
write:
url:jdbc:mysql://write-db:3306/app
username:user
password:pass
read:
url:jdbc:mysql://read-db:3306/app
username:user
password:pass代碼中使用:
@Service
publicclass OrderService {
// 寫操作用寫庫
@Transactional
@WriteDataSource
public void createOrder(Order order) {
orderRepository.save(order);
}
// 讀操作用讀庫
@ReadDataSource
public Order getOrder(Long id) {
return orderRepository.findById(id);
}
}如果有需求,可以做分庫分表。
// 基于ShardingSphere的分庫分表配置
spring:
shardingsphere:
datasource:
names:ds0,ds1
ds0:...
ds1:...
rules:
sharding:
tables:
orders:
actualDataNodes:ds$->{0..1}.orders_$->{0..15}
databaseStrategy:
standard:
shardingColumn:user_id
shardingAlgorithmName:database_inline
tableStrategy:
standard:
shardingColumn:order_id
shardingAlgorithmName:table_inline可以用批量處理提升吞吐量。
批量寫入數據庫示例:
@Slf4j
@Service
publicclass BatchInsertService {
private List<Order> batchList = new ArrayList<>();
privatefinalint BATCH_SIZE = 1000;
@Scheduled(fixedDelay = 1000) // 每秒批量寫入一次
public void batchInsert() {
if (batchList.isEmpty()) {
return;
}
List<Order> currentBatch;
synchronized (batchList) {
currentBatch = new ArrayList<>(batchList);
batchList.clear();
}
try {
jdbcTemplate.batchUpdate(
"INSERT INTO orders (...) VALUES (?, ?, ...)",
currentBatch,
100,
(ps, order) -> {
ps.setLong(1, order.getId());
ps.setString(2, order.getNo());
// ...其他字段
});
} catch (Exception e) {
log.error("批量插入失敗", e);
}
}
}3. 異步化
讓請求排隊處理。
同步處理就像只有一個收銀臺的超市,異步處理就像讓顧客把需求寫在紙上,我們慢慢處理。
消息隊列削峰示例:
圖片
代碼實現:
@Component
@RocketMQMessageListener(topic = "order_topic", consumerGroup = "order_group")
public class OrderConsumer implements RocketMQListener<OrderMessage> {
@Override
public void onMessage(OrderMessage message) {
// 這里可以慢慢處理,不用著急
orderService.processOrder(message);
}
}這樣即使瞬間來了10萬個訂單,也不會把數據庫沖垮,而是慢慢處理。
4.容量評估與彈性伸縮
4.1 性能壓測與容量規劃
使用JMH進行壓力測試代碼如下:
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Thread)
publicclass OrderServiceBenchmark {
private OrderService orderService;
@Setup
public void setup() {
// 初始化測試環境
}
@Benchmark
public void testCreateOrder() {
Order order = new Order();
// 設置訂單參數
orderService.createOrder(order);
}
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder()
.include(OrderServiceBenchmark.class.getSimpleName())
.forks(1)
.warmupIterations(5)
.measurementIterations(10)
.build();
new Runner(opt).run();
}
}4.2 基于指標的彈性伸縮
我們需要建立一套基于指標的彈性伸縮的機制:
當監控系統發現異常時,在K8S中能夠自動擴容Prod實例,同時自動更新負載均衡。
5.實戰演練
我們需要有全鏈路壓測方案,每隔一段時間做一次實戰演練。
5.1 影子庫表方案
為壓測流量提供隔離的數據庫環境,防止壓測數據污染正式數據。
基于MyBatis插件的影子庫表路由:
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class ShadowDatabaseInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
if (isPressureTestRequest()) {
// 切換到影子庫表
String shadowTableName = "shadow_" + getOriginalTableName(ms);
MappedStatement shadowMs = createShadowMappedStatement(ms, shadowTableName);
invocation.getArgs()[0] = shadowMs;
}
return invocation.proceed();
}
private boolean isPressureTestRequest() {
// 通過ThreadLocal或請求頭判斷是否為壓測流量
return PressureTestContext.isPressureTest();
}
}5.2 壓測流量染色
流量染色,顧名思義,就是給壓測流量“染上顏色”,打上獨特的標記,以便在整個復雜的分布式系統中能夠清晰地識別和追蹤它。
下面的例子中通過header中的X-Pressure-Test參數,判斷是否需要加上染色。
// 全局壓測上下文
publicclass PressureTestContext {
privatestaticfinal ThreadLocal<Boolean> PRESSURE_TEST_FLAG = new ThreadLocal<>();
public static void markPressureTest() {
PRESSURE_TEST_FLAG.set(true);
}
public static boolean isPressureTest() {
return Boolean.TRUE.equals(PRESSURE_TEST_FLAG.get());
}
public static void clear() {
PRESSURE_TEST_FLAG.remove();
}
}
// 網關過濾器進行流量染色
@Component
publicclass PressureTestFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String pressureTestHeader = exchange.getRequest().getHeaders().getFirst("X-Pressure-Test");
if ("true".equals(pressureTestHeader)) {
PressureTestContext.markPressureTest();
}
return chain.filter(exchange).then(Mono.fromRunnable(PressureTestContext::clear));
}
}總結
流量暴增時的應對策略如下:
- 預防優于救治:建立完善的監控預警體系,提前發現容量瓶頸。
- 立即行動:先擴容保住系統,再分析問題。
- 分層防御:從網關到數據庫,每層都要有相應的防護措施。
- 彈性設計:系統要具備水平擴展能力,能夠快速應對流量變化。
- 異步解耦:通過消息隊列等手段,將同步調用轉為異步處理。
- 容錯降級:保證核心業務的可用性,非核心功能可適當降級。
- 定期演練:通過全鏈路壓測驗證系統容量和應急預案。
記住這個處理順序:先保命(擴容),再治病(優化),最后養生(架構升級)。






























