高并发系统设计:核心思路与实践 ⭐⭐⭐
🎯 面试题:如何设计一个高并发系统?
高并发系统设计是面试最高频的系统设计题,没有之一。无论是阿里、字节、腾讯还是美团,但凡面高级工程师及以上岗位,几乎必问高并发相关问题。本质考察的是:对系统全链路的理解深度、问题拆解能力、以及在流量洪峰面前的架构决策能力。
一、高并发核心指标
1.1 关键性能指标详解
高并发系统的核心指标体系:
┌─────────────────────────────────────────────────────────────┐
│ 核心指标体系 │
├─────────────────────────────────────────────────────────────┤
│ QPS (Queries Per Second) │
│ └─ 每秒请求数,衡量系统吞吐能力的基准指标 │
│ │
│ TPS (Transactions Per Second) │
│ └─ 每秒事务数,一个事务可能包含多个请求 │
│ │
│ 并发数 (Concurrent Users) │
│ └─ 同时在线的用户数,与 QPS 非线性相关 │
│ │
│ 响应时间 (Response Time) │
│ └─ p50/p95/p99/p999 分布 │
│ │
│ 错误率 (Error Rate) │
│ └─ 5xx/4xx 占比、超时率 │
│ │
│ 资源利用率 │
│ └─ CPU/内存/磁盘 IO/网络 IO │
└─────────────────────────────────────────────────────────────┘
QPS vs TPS 的区别:
// QPS:每秒钟请求数
// 一个页面请求可能包含 10 个接口
// 10000 QPS 的页面 = 100000 QPS 的后端接口
// TPS:每秒钟事务数
// 一个事务 = 一次完整的业务操作
// 例如:一次下单 = 1 TPS(内部可能调用 5 个接口)
1.2 响应时间的分位数
分位数的含义:
100 个请求的响应时间从小到大排序:
p50 = 第 50 个请求的响应时间(中位数)
p95 = 第 95 个请求的响应时间
p99 = 第 99 个请求的响应时间
p999 = 第 999 个请求的响应时间
为什么关注 p99/p999?
- p50 只能反映一半用户的体验
- p99 反映的是"那 1% 的用户可能正在骂你"
- 电商大促时,p999 才是生命线
典型目标:
- p50 < 200ms(优秀)
- p95 < 500ms(良好)
- p99 < 1s(及格)
- p999 < 2s(高并发场景可接受)
1.3 并发数的计算
// 并发数估算公式
// 并发数 = QPS × 平均响应时间(秒)
// 示例:
// QPS = 10000,平均响应时间 = 100ms
// 并发数 = 10000 × 0.1 = 1000 个并发请求
// 线程数估算
// 最佳线程数 = CPU 核心数 × 期望 CPU 利用率 × (1 + 等待时间/处理时间)
// IO 密集型:最佳线程数 = CPU 核心数 × 2 ~ 3
// CPU 密集型:最佳线程数 = CPU 核心数 + 1
二、分层架构设计
2.1 高并发系统分层架构
用户请求的完整链路:
┌─────────────────────────────────────────────────────────────────────┐
│ 用户层 │
│ (浏览器 / App / 小程序) │
└─────────────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ CDN 层 │
│ └─ 静态资源缓存(JS/CSS/图片/视频) │
│ └─ 就近接入(全国 CDN 节点) │
│ └─ 边缘计算(简单逻辑下沉) │
└─────────────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 接入层 │
│ └─ DNS 解析(智能调度) │
│ └─ 负载均衡(Nginx / LVS / SLB) │
│ └─ SSL 卸载 │
│ └─ 请求限流(令牌桶 / 漏桶) │
└─────────────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 网关层 │
│ └─ 统一鉴权(Token 验证) │
│ └─ 路由转发(微服务网关) │
│ └─ 请求染色(TraceId 透传) │
│ └─ 熔断降级(Sentinel / Hystrix) │
│ └─ 风控拦截(黑名单 / 频率限制) │
└─────────────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 应用层 │
│ └─ 业务逻辑处理 │
│ └─ 服务编排(Spring Cloud / Dubbo) │
│ └─ 线程池并行调用 │
│ └─ 本地缓存(Caffeine / Guava Cache) │
└─────────────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 缓存层 │
│ └─ 多级缓存(本地 → Redis → CDN) │
│ └─ 热点数据探测 │
│ └─ 缓存预热 │
│ └─ 缓存淘汰策略 │
└─────────────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 数据层 │
│ └─ 读写分离(主从复制) │
│ └─ 分库分表(水平/垂直分片) │
│ └─ 连接池优化(HikariCP / Druid) │
│ └─ SQL 优化(索引 / 执行计划) │
└─────────────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 基础设施层 │
│ └─ 容器化部署(Docker / K8s) │
│ └─ 服务网格(Istio) │
│ └─ 日志 / 监控 / 告警 │
│ └─ 弹性伸缩(Auto Scaling) │
└─────────────────────────────────────────────────────────────────────┘
2.2 每层防护的目标
分层防护的核心思想:层层过滤,把流量挡在靠近用户的那一层
层级 拦截目标 防护手段
─────────────────────────────────────────────────────────
CDN 层 静态资源请求 浏览器缓存、CDN 缓存
接入层 非法请求、CC 攻击 IP 限流、验证码
网关层 恶意调用、业务攻击 鉴权、风控、熔断
应用层 业务层流量 线程池隔离、接口限流
缓存层 数据库访问 多级缓存、热点探测
数据层 持久化写入 读写分离、分库分表
目标:每层过滤掉 80% 的流量,数据库只承受 1% 的压力
三、流量防护体系
3.1 限流算法
令牌桶算法
令牌桶的核心思想:以固定速率向桶中添加令牌,请求来时取令牌,桶满则丢弃
┌────────────────────────────────────────┐
│ 令牌桶示意图 │
│ │
│ ┌──────────────────────┐ │
│ │ 令牌生成器 │ │
│ │ (每秒 N 个令牌) │ │
│ └─────────┬────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ 桶 │ │
│ │ 令牌 │ │
│ │ 令牌令牌 令牌 │ │
│ │ 令牌令牌 │ │
│ │ 桶容量 = M │ │
│ └─────────┬────────────┘ │
│ │ │
│ ▼ 取令牌 │
│ ┌──────────────────────┐ │
│ │ 请求通过 │ │
│ └──────────────────────┘ │
└────────────────────────────────────────┘
特点:
- 允许一定程度的突发流量(桶内有令牌)
- 令牌桶满时新令牌被丢弃,不影响已存在的令牌
- 适合限流场景:允许瞬时高峰,但限制总量
// Java 实现令牌桶(Guava RateLimiter)
public class TokenBucketRateLimiter {
// 每秒产生 100 个令牌,桶容量 200
private final RateLimiter rateLimiter = RateLimiter.create(100, 200);
public boolean tryAcquire() {
// 尝试获取一个令牌,非阻塞立即返回
return rateLimiter.tryAcquire();
}
public boolean tryAcquireWithTimeout(long timeout, TimeUnit unit) {
// 等待最多 timeout 时间获取令牌
return rateLimiter.tryAcquire(timeout, unit);
}
// 在 Controller 中使用
@GetMapping("/api/resource")
public ResponseEntity<?> getResource() {
if (!rateLimiter.tryAcquire()) {
return ResponseEntity.status(429)
.body("请求过于频繁,请稍后再试");
}
// 业务逻辑
return ResponseEntity.ok(resourceService.get());
}
}
漏桶算法
漏桶的核心思想:请求像水一样进入漏桶,以固定速率漏出,超出容量则拒绝
┌────────────────────────────────────────┐
│ 漏桶示意图 │
│ │
│ ┌──────────────────────┐ │
│ │ 入水 │ │
│ │ 请求请求请求 │ │
│ └─────────┬────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ 桶 │ │
│ │ 请求请求 │ │
│ │ 桶容量 = M │ │
│ └─────────┬────────────┘ │
│ │ │
│ ▼ 固定速率漏出 │
│ ┌──────────────────────┐ │
│ │ 请求通过 │ │
│ └──────────────────────┘ │
└────────────────────────────────────────┘
特点:
- 无论请求多少,流出速率恒定
- 严格平整流量,不允许突发
- 适合下游保护场景:下游处理能力固定,不允许瞬时冲击
令牌桶 vs 漏桶
对比维度 令牌桶 漏桶
─────────────────────────────────────────────────────────
允许突发 是(桶内有令牌) 否(严格均匀)
实现复杂度 中等 简单
适用场景 限流(保护自己) 削峰(保护下游)
算法特性 配额消费 队列排队
实际选择:
- 保护自己的接口:令牌桶(允许用户快速响应)
- 保护下游 MQ/数据库:漏桶(严格控制消费速率)
3.2 Sentinel 限流实战
// Sentinel 限流配置
@Configuration
public class SentinelConfig {
// 自定义流控规则
@PostConstruct
public void initFlowRules() {
List<FlowRule> rules = new ArrayList<>();
// 秒杀接口:每秒 100 次
FlowRule seckillRule = new FlowRule("/api/seckill/do");
seckillRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
seckillRule.setCount(100);
seckillRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
seckillRule.setMaxQueueingTimeMs(500); // 排队等待时间
rules.add(seckillRule);
// 普通接口:每秒 1000 次
FlowRule normalRule = new FlowRule("/api/normal");
normalRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
normalRule.setCount(1000);
rules.add(normalRule);
FlowRuleManager.loadRules(rules);
}
}
// 在 Service 中使用
@Service
public class OrderService {
@SentinelResource(value = "createOrder", blockHandler = "handleBlock")
public Result<Order> createOrder(OrderRequest request) {
// 正常业务逻辑
return Result.success(orderMapper.insert(request));
}
// 限流降级处理
public Result<Order> handleBlock(BlockException e) {
return Result.error("系统繁忙,请稍后再试");
}
}
3.3 熔断降级
熔断器的工作原理:
┌────────────────────────────────────────┐
│ 熔断器状态机 │
│ │
│ ┌─────────┐ 失败率过高 ┌──────────┐
│ │ Closed │ ──────────────→│ Open │
│ │ 正常 │ │ 熔断中 │
│ │ 请求 │ │ 直接 │
│ └─────────┘ │ 拒绝 │
│ │ └────┬─────┘
│ │ │
│ │ 探测成功 │
│ │ │
│ ▼ 半开状态│
│ ┌─────────┐ ◀────────────────┐ │
│ │ Half │ ────────────────│─────┘
│ │ Open │ 探测请求通过 │
│ └─────────┘ │
└────────────────────────────────────────┘
熔断策略:
- 慢调用比例:响应时间 > 阈值 的请求占比
- 异常比例:异常请求占总请求的比例
- 异常数:单位时间内异常请求数
// Sentinel 熔断配置
@PostConstruct
public void initDegradeRules() {
List<DegradeRule> rules = new ArrayList<>();
// 慢调用熔断:响应时间 > 1s 的请求占比 50% 时熔断 10 秒
DegradeRule slowRule = new DegradeRule("createOrder");
slowRule.setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType());
slowRule.setCount(0.5); // 50% 失败率
slowRule.setSlowRatioThreshold(1000); // 响应时间 > 1s 视为慢调用
slowRule.setMinRequestAmount(10); // 最小请求数
slowRule.setStatIntervalMs(10000); // 统计时间窗口 10 秒
slowRule.setTimeWindow(10); // 熔断持续时间 10 秒
rules.add(slowRule);
DegradeRuleManager.loadRules(rules);
}
3.4 隔离策略
隔离的核心思想:不把鸡蛋放在一个篮子里
┌─────────────────────────────────────────────────────────────┐
│ 隔离策略分类 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 线程池隔离 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 线程池1 │ │ 线程池2 │ │ 线程池3 │ │
│ │ 5 线程 │ │ 10 线程 │ │ 20 线程 │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 订单服务 支付服务 库存服务 │
│ │
│ 信号量隔离 │
│ ┌─────────┐ ┌─────────┐ │
│ │ 信号量1 │ │ 信号量2 │ ← 不创建线程,用计数器限流 │
│ │ 100 │ │ 200 │ │
│ └─────────┘ └─────────┘ │
│ │
│ 进程隔离 │
│ ┌─────────┐ ┌─────────┐ │
│ │ 进程1 │ │ 进程2 │ ← 不同服务部署在不同机器 │
│ │ 秒杀服务 │ │ 普通服务│ │
│ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
// 线程池隔离实战
@Configuration
public class ThreadPoolConfig {
// 订单服务线程池
@Bean("orderExecutor")
public Executor orderExecutor() {
return new ThreadPoolExecutor(
10, // 核心线程
50, // 最大线程
60, // 空闲存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), // 队列容量
new ThreadFactoryBuilder().setNamePrefix("order-").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用方执行
);
}
// 消息通知线程池
@Bean("notifyExecutor")
public Executor notifyExecutor() {
return new ThreadPoolExecutor(
5, 20, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(5000),
new ThreadFactoryBuilder().setNamePrefix("notify-").build(),
new ThreadPoolExecutor.DiscardPolicy()
);
}
}
// 使用
@Service
public class OrderService {
@Autowired
@Qualifier("orderExecutor")
private Executor executor;
public void createOrderAsync(Order order) {
executor.execute(() -> {
orderMapper.insert(order);
// 后续逻辑
});
}
}
四、缓存为王
4.1 多级缓存架构
高并发系统的缓存层级:
┌─────────────────────────────────────────────────────────────────┐
│ 多级缓存架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ L1: 浏览器缓存 │
│ - 静态资源:JS/CSS/图片 │
│ - 策略:Cache-Control / Etag │
│ - 命中率:通常 60-80% │
│ │
│ L2: CDN 缓存 │
│ - 边缘节点缓存 │
│ - 静态页面/接口响应 │
│ - 命中率:通常 40-60% │
│ │
│ L3: Nginx 缓存 │
│ - OpenResty / Lua 缓存 │
│ - 热点数据 │
│ - 命中率:通常 30-50% │
│ │
│ L4: 本地缓存 │
│ - Caffeine / Guava Cache │
│ - JVM 进程内缓存 │
│ - 命中率:通常 20-40% │
│ │
│ L5: 分布式缓存 │
│ - Redis / Memcached │
│ - 跨进程共享 │
│ - 命中率:通常 10-30%(视业务而定) │
│ │
│ L6: 数据库 │
│ - 最终数据来源 │
│ - 只读场景尽量绕过 │
│ │
└─────────────────────────────────────────────────────────────────┘
关键认知:
- 缓存不是银弹,命中率才是核心
- 层级越多,缓存成本越高,一致性越难保证
- 根据业务特性选择合适的缓存层级组合
4.2 缓存策略详解
// 多级缓存实现
@Service
public class ProductCacheService {
// L1: 本地缓存(Caffeine)
private final Cache<String, ProductVO> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private ProductMapper productMapper;
public ProductVO getProduct(Long productId) {
String cacheKey = "product:" + productId;
// L1: 查本地缓存
ProductVO result = localCache.getIfPresent(cacheKey);
if (result != null) {
return result;
}
// L2: 查 Redis
String redisValue = redisTemplate.opsForValue().get(cacheKey);
if (redisValue != null) {
result = JSON.parseObject(redisValue, ProductVO.class);
localCache.put(cacheKey, result); // 回填本地缓存
return result;
}
// L3: 查数据库
result = productMapper.selectById(productId);
if (result != null) {
// 回填 Redis
redisTemplate.opsForValue().set(cacheKey,
JSON.toJSONString(result),
10, TimeUnit.MINUTES);
// 回填本地缓存
localCache.put(cacheKey, result);
}
return result;
}
// 缓存更新:双删策略
public void updateProduct(ProductVO product) {
String cacheKey = "product:" + product.getId();
// 1. 先删除缓存
localCache.invalidate(cacheKey);
redisTemplate.delete(cacheKey);
// 2. 更新数据库
productMapper.updateById(product);
// 3. 延迟删除(应对并发:其他线程可能刚读到旧数据)
new Thread(() -> {
try {
Thread.sleep(500);
redisTemplate.delete(cacheKey);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
4.3 缓存命中率分析
缓存命中率的计算与优化:
┌─────────────────────────────────────────────────────────────┐
│ 缓存命中分析 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 缓存命中率 = 命中次数 / 总请求次数 │
│ │
│ 典型场景的命中率: │
│ ┌─────────────────┬────────────┬────────────┐ │
│ │ 业务场景 │ 缓存命中率 │ 说明 │ │
│ ├─────────────────┼────────────┼────────────┤ │
│ │ 热点商品详情 │ 95%+ │ 1% 的商 │ │
│ │ │ │ 品贡献 99% │ │
│ │ │ │ 的访问 │ │
│ │ 用户画像 │ 80-90% │ 用户行为 │ │
│ │ │ │ 相对稳定 │ │
│ │ 列表页 │ 30-50% │ 分页参数 │ │
│ │ │ │ 多样 │ │
│ │ 实时数据 │ 10-20% │ 数据变化 │ │
│ │ │ │ 频繁 │ │
│ └─────────────────┴────────────┴────────────┘ │
│ │
│ 命中率低的常见原因: │
│ 1. 缓存 key 设计不合理(太多参数组合) │
│ 2. 缓存过期时间太短 │
│ 3. 缓存容量太小导致频繁淘汰 │
│ 4. 热点数据不均匀(存在热点 key) │
│ 5. 缓存穿透(大量不存在的数据请求) │
│ │
└─────────────────────────────────────────────────────────────┘
4.4 缓存预热
// 缓存预热实现
@Service
public class CachePreheatService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private ProductMapper productMapper;
/**
* 定时预热:每天凌晨 3 点预热热点商品
*/
@Scheduled(cron = "0 0 3 * * ?")
public void preheatHotProducts() {
log.info("开始缓存预热...");
// 1. 获取热点商品列表(根据历史访问数据)
List<Long> hotProductIds = getHotProductIds();
// 2. 批量查询并写入缓存
for (Long productId : hotProductIds) {
try {
ProductVO product = productMapper.selectById(productId);
if (product != null) {
String cacheKey = "product:" + productId;
redisTemplate.opsForValue().set(
cacheKey,
JSON.toJSONString(product),
1, TimeUnit.HOURS
);
}
} catch (Exception e) {
log.error("缓存预热失败: productId={}", productId, e);
}
}
log.info("缓存预热完成,共预热 {} 个商品", hotProductIds.size());
}
/**
* 主动预热:秒杀活动开始前 5 分钟预热
*/
@PostConstruct
public void preheatSeckillProducts() {
// 从配置中心读取秒杀商品 ID 列表
List<Long> seckillProductIds = seckillProductConfig.getProductIds();
for (Long productId : seckillProductIds) {
// 预热库存
ProductStock stock = stockMapper.selectByProductId(productId);
String stockKey = "seckill:stock:" + productId;
redisTemplate.opsForValue().set(stockKey, String.valueOf(stock.getStock()));
}
}
private List<Long> getHotProductIds() {
// 从 Redis 获取最近 7 天访问 Top 1000 的商品
Set<String> topProducts = redisTemplate.opsForZSet()
.reverseRange("product:access:rank", 0, 999);
return topProducts.stream()
.map(Long::parseLong)
.collect(Collectors.toList());
}
}
4.5 缓存问题与应对
缓存三大经典问题:
┌─────────────────────────────────────────────────────────────┐
│ 1. 缓存穿透 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 请求 → 缓存(无) → 数据库(无) → 返回空 │ │
│ │ │ │
│ │ 问题:恶意请求查询不存在的数据,打穿数据库 │ │
│ │ │ │
│ │ 解决方案: │ │
│ │ - 布隆过滤器(推荐) │ │
│ │ - 缓存空值(短 TTL) │ │
│ │ - 参数校验 + 风控 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 2. 缓存击穿 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 热点 key 过期瞬间 → 并发请求全部打向数据库 │ │
│ │ │ │
│ │ 解决方案: │ │
│ │ - 互斥锁(分布式锁) │ │
│ │ - 逻辑过期(不过期,用后台异步更新) │ │
│ │ - 永不过期(热点数据) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 3. 缓存雪崩 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 大量缓存同时过期 → 请求全部打向数据库 │ │
│ │ │ │
│ │ 解决方案: │ │
│ │ - 随机过期时间( TTL + random) │ │
│ │ - 持久化(Redis 集群 + 哨兵) │ │
│ │ - 多级缓存 + 熔断降级 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
// 缓存击穿:互斥锁方案
public ProductVO getProductWithLock(Long productId) {
String cacheKey = "product:" + productId;
// 1. 先查缓存
String value = redisTemplate.opsForValue().get(cacheKey);
if (value != null) {
return JSON.parseObject(value, ProductVO.class);
}
// 2. 获取分布式锁
String lockKey = "lock:product:" + productId;
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(acquired)) {
try {
// 双重检查
value = redisTemplate.opsForValue().get(cacheKey);
if (value != null) {
return JSON.parseObject(value, ProductVO.class);
}
// 查数据库
ProductVO product = productMapper.selectById(productId);
if (product != null) {
redisTemplate.opsForValue().set(cacheKey,
JSON.toJSONString(product),
30, TimeUnit.MINUTES);
} else {
// 缓存空值,防止穿透
redisTemplate.opsForValue().set(cacheKey,
"NULL", 60, TimeUnit.SECONDS);
}
return product;
} finally {
redisTemplate.delete(lockKey);
}
} else {
// 没拿到锁,短暂等待后重试
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getProductWithLock(productId);
}
}
五、异步化与MQ
5.1 异步化的价值
同步 vs 异步:
同步调用:
用户请求 → 服务A → 服务B → 服务C → 返回
耗时 = T(A) + T(B) + T(C)
异步调用:
用户请求 → 服务A → 写 MQ → 立即返回
耗时 = T(A) + T(写MQ)
适用场景:
- 非核心链路:消息通知、日志记录、数据统计
- 耗时操作:批量处理、第三方调用
- 削峰填谷:秒杀下单、优惠券发放
核心价值:
1. 提升吞吐量:不受下游耗时影响
2. 削峰填谷:MQ 缓冲洪峰,平滑处理
3. 解耦微服务:上下游不直接依赖
4. 最终一致性:允许短暂延迟
5.2 MQ 削峰实战
// 秒杀场景:MQ 削峰
@Service
public class SeckillService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private RocketMQTemplate mqTemplate;
/**
* 秒杀接口:Redis 预减库存 + MQ 异步下单
*/
public Result<String> seckill(SeckillRequest request) {
Long skuId = request.getSkuId();
Long userId = request.getUserId();
// 1. Lua 脚本原子扣减库存
String stockKey = "seckill:stock:" + skuId;
Long remainStock = redisTemplate.execute(
new DefaultRedisScript<>(
"local stock = redis.call('GET', KEYS[1]) " +
"if stock == false or tonumber(stock) < 1 then return -1 end " +
"redis.call('DECR', KEYS[1]) " +
"return redis.call('GET', KEYS[1])",
Long.class
),
List.of(stockKey)
);
if (remainStock == null || remainStock < 0) {
return Result.error("已售罄");
}
// 2. 发送 MQ 消息(异步下单)
SeckillMessage message = new SeckillMessage(skuId, userId);
mqTemplate.asyncSend("seckill-order-topic", message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("秒杀消息发送成功: {}", sendResult.getMsgId());
}
@Override
public void onException(Throwable e) {
// 消息发送失败,回补库存
log.error("消息发送失败,回补库存: skuId={}", skuId, e);
redisTemplate.opsForValue().increment(stockKey);
}
});
// 3. 立即返回
return Result.success("抢购成功,请前往订单页面确认");
}
}
5.3 线程池并行化
// CompletableFuture 并行调用多个服务
@Service
public class OrderDetailService {
@Autowired
private UserService userService;
@Autowired
private ProductService productService;
@Autowired
private AddressService addressService;
public OrderDetailVO getOrderDetail(Long orderId) {
long startTime = System.currentTimeMillis();
// 1. 串行调用(总耗时 = A + B + C + D)
// Order order = orderMapper.selectById(orderId);
// User user = userService.getUser(order.getUserId());
// Product product = productService.getProduct(order.getProductId());
// Address address = addressService.getAddress(order.getAddressId());
// 2. CompletableFuture 并行调用(总耗时 ≈ max(A, B, C))
CompletableFuture<Order> orderFuture = CompletableFuture.supplyAsync(
() -> orderMapper.selectById(orderId)
);
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(
() -> userService.getUserByOrderId(orderId)
);
CompletableFuture<Product> productFuture = CompletableFuture.supplyAsync(
() -> productService.getProductByOrderId(orderId)
);
CompletableFuture<Address> addressFuture = CompletableFuture.supplyAsync(
() -> addressService.getAddressByOrderId(orderId)
);
// 3. 等待所有结果
CompletableFuture<Void> allOf = CompletableFuture.allOf(
orderFuture, userFuture, productFuture, addressFuture
);
try {
allOf.join(); // 阻塞等待
} catch (Exception e) {
// 处理异常
throw new RuntimeException("获取订单详情失败", e);
}
// 4. 组装结果
OrderDetailVO detail = new OrderDetailVO();
detail.setOrder(orderFuture.get());
detail.setUser(userFuture.get());
detail.setProduct(productFuture.get());
detail.setAddress(addressFuture.get());
log.info("订单详情查询耗时: {}ms", System.currentTimeMillis() - startTime);
return detail;
}
}
5.4 MQ 消息可靠性
MQ 消息可靠性的核心原则:
┌─────────────────────────────────────────────────────────────┐
│ 消息可靠性的三个环节 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 生产可靠(不丢消息) │
│ - 同步发送 + 确认回调 │
│ - 事务消息(RocketMQ) │
│ - 消息持久化 │
│ │
│ 2. 存储可靠(不丢消息) │
│ - 主从复制 │
│ - 刷盘策略(同步刷盘) │
│ - 集群部署 │
│ │
│ 3. 消费可靠(不丢消息) │
│ - 手动 ACK │
│ - 消息幂等 │
│ - 失败重试 │
│ │
└─────────────────────────────────────────────────────────────┘
// 消费者:手动 ACK + 幂等处理
@Service
@Slf4j
public class OrderConsumer {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@RocketMQMessageListener(
topic = "seckill-order-topic",
consumerGroup = "seckill-consumer-group",
consumeThreadMax = 10
)
public void consume(SeckillMessage message, Acknowledgment ack) {
String dedupKey = "dedup:order:" + message.getSkuId() + ":" + message.getUserId();
try {
// 1. 幂等检查
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(dedupKey, "1", 1, TimeUnit.HOURS);
if (!Boolean.TRUE.equals(success)) {
log.info("消息已处理,跳过: {}", message);
ack.acknowledge();
return;
}
// 2. 业务处理
createOrder(message);
// 3. 手动 ACK
ack.acknowledge();
log.info("订单创建成功: {}", message);
} catch (Exception e) {
log.error("订单创建失败,不 ACK 会重试: {}", message, e);
throw e; // 抛出异常,MQ 会自动重试
}
}
}
六、数据库优化
6.1 读写分离
读写分离架构:
┌─────────────────────────────────────────────────────────────┐
│ 读写分离架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ 应用服务 │ │
│ └──────┬───────┘ │
│ │ │
│ ┌────────────┴────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 写库(主) │ │ 读库(从) │ │
│ │ Master │ ◄───────│ Slave │ │
│ │ QPS: 1000 │ 同步 │ QPS: 5000 │ │
│ └──────┬───────┘ └──────────────┘ │
│ │ │
│ │ binlog 异步复制 │
│ └─────────────────────────────────► │
│ │
│ 读写策略: │
│ - 写操作:强制走主库 │
│ - 读操作:强制走从库(或根据一致性要求选择) │
│ - 延迟问题:金融类业务强制读主库 │
│ │
└─────────────────────────────────────────────────────────────┘
// Spring Boot 读写分离配置
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource(
@Value("${spring.datasource.master.url}") String masterUrl,
@Value("${spring.datasource.slave.url}") String slaveUrl) {
Map<Object, Object> targetDataSources = new HashMap<>();
// 主库
HikariDataSource masterDs = new HikariDataSource();
masterDs.setJdbcUrl(masterUrl);
targetDataSources.put("master", masterDs);
// 从库
HikariDataSource slaveDs = new HikariDataSource();
slaveDs.setJdbcUrl(slaveUrl);
targetDataSources.put("slave", slaveDs);
// 路由数据源
RoutingDataSource routingDs = new RoutingDataSource();
routingDs.setTargetDataSources(targetDataSources);
routingDs.setDefaultTargetDataSource(masterDs);
return routingDs;
}
}
// 切换数据源
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 读操作走从库,写操作走主库
boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
return isReadOnly ? "slave" : "master";
}
}
// 使用:在 Service 中控制读写
@Service
public class UserService {
@Transactional(readOnly = true) // 走从库
public User getUserById(Long id) {
return userMapper.selectById(id);
}
@Transactional(readOnly = false) // 走主库
public void createUser(User user) {
userMapper.insert(user);
}
}
6.2 分库分表
分库分表策略:
┌─────────────────────────────────────────────────────────────┐
│ 分库分表策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 垂直分库:按业务模块拆分 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 用户库 │ │ 订单库 │ │ 商品库 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 垂直分表:按字段冷热拆分 │
│ ┌─────────────┬─────────────┐ │
│ │ user_basic │ user_detail │ │
│ │ (id,name) │ (addr,desc) │ ← 冷数据 │
│ └─────────────┴─────────────┘ │
│ │
│ 水平分片:按数据量拆分 │
│ ┌──────────┬──────────┬──────────┐ │
│ │ order_0 │ order_1 │ order_2 │ │
│ │ userId%3 │ userId%3 │ userId%3 │ │
│ │ = 0 │ = 1 │ = 2 │ │
│ └──────────┴──────────┴──────────┘ │
│ │
│ 分片键选择原则: │
│ 1. 尽量选择查询最频繁的字段 │
│ 2. 避免跨分片查询 │
│ 3. 均匀分布(避免热点) │
│ │
└─────────────────────────────────────────────────────────────┘
// ShardingSphere 分片配置
@Configuration
public class ShardingSphereConfig {
@Bean
public DataSource dataSource() throws SQLException {
ShardingSphereDataSourceFactory.createDataSource(
createDataSourceMap(),
createRuleConfiguration(),
createProps()
);
}
private ShardingRuleConfiguration createRuleConfiguration() {
ShardingRuleConfiguration config = new ShardingRuleConfiguration();
// 订单表分片:按 user_id 分片到 4 个库,每库 4 张表
TableRuleConfiguration orderTable = new TableRuleConfiguration("t_order", "ds$->{0..3}.t_order_$->{0..3}");
orderTable.setDatabaseShardingStrategy(
new StandardShardingStrategyConfiguration("user_id", "databaseShardingAlgorithm")
);
orderTable.setTableShardingStrategy(
new StandardShardingStrategyConfiguration("user_id", "tableShardingAlgorithm")
);
config.getTableRuleConfigs().add(orderTable);
return config;
}
private Map<String, DataSource> createDataSourceMap() {
// 配置 4 个数据源
Map<String, DataSource> map = new HashMap<>();
for (int i = 0; i < 4; i++) {
map.put("ds" + i, createDataSource(i));
}
return map;
}
}
6.3 连接池优化
// HikariCP 连接池优化配置
@Configuration
public class HikariCPConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
// 核心配置
config.setJdbcUrl("jdbc:mysql://localhost:3306/db");
config.setUsername("root");
config.setPassword("password");
config.setDriverClassName("com.mysql.cj.jdbc.Driver");
// 连接池大小
config.setMaximumPoolSize(20); // 最大连接数
config.setMinimumIdle(5); // 最小空闲连接
// 空闲连接存活时间
config.setIdleTimeout(300000); // 5 分钟
// 连接最大存活时间
config.setMaxLifetime(1800000); // 30 分钟
// 连接超时
config.setConnectionTimeout(10000); // 10 秒
// 慢查询日志
config.setLeakDetectionThreshold(60000); // 60 秒未归还视为泄漏
// PreparedStatement 缓存
config.setPreparedStatementCacheSize(250);
config.addDataSourceProperty("cachePrepStmts", "true");
return new HikariDataSource(config);
}
}
// 监控连接池指标
@Bean
public MeterBinder HikariCPMetrics(HikariDataSource dataSource) {
return registry -> {
HikariConfigMXBean bean = dataSource.getHikariConfigMXBean();
Gauge.builder("hikari.connections.active", bean, HikariConfigMXBean::getActiveConnections)
.register(registry);
Gauge.builder("hikari.connections.idle", bean, HikariConfigMXBean::getIdleConnections)
.register(registry);
Gauge.builder("hikari.connections.waiting", bean, HikariConfigMXBean::getThreadsAwaitingConnection)
.register(registry);
};
}
七、水平扩展
7.1 无状态设计
无状态服务设计原则:
┌─────────────────────────────────────────────────────────────┐
│ 无状态 vs 有状态 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 有状态服务: │
│ - 用户的 Session 存储在服务器内存 │
│ - 每次请求必须路由到同一台服务器 │
│ - 扩缩容麻烦,需要 Session 复制 │
│ │
│ 无状态服务: │
│ - Session 存储在 Redis / JWT Token │
│ - 请求可以路由到任意服务器 │
│ - 轻松扩缩容 │
│ │
│ 无状态化改造要点: │
│ 1. 移除本地缓存(用分布式缓存) │
│ 2. 移除本地 Session(用 Redis) │
│ 3. 文件存储外置(用 OSS / NAS) │
│ 4. 定时任务不走集群(用 XXL-Job) │
│ │
└─────────────────────────────────────────────────────────────┘
// 无状态服务改造示例
// ❌ 有状态:本地缓存用户信息
@Service
public class OldUserService {
private Map<Long, User> localCache = new ConcurrentHashMap<>(); // 有状态
public User getUser(Long userId) {
return localCache.computeIfAbsent(userId, id -> userMapper.selectById(id));
}
}
// ✅ 无状态:使用 Redis 分布式缓存
@Service
public class NewUserService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public User getUser(Long userId) {
String key = "user:" + userId;
String value = redisTemplate.opsForValue().get(key);
if (value != null) {
return JSON.parseObject(value, User.class);
}
User user = userMapper.selectById(userId);
redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
return user;
}
}
7.2 服务发现
服务发现架构:
┌─────────────────────────────────────────────────────────────┐
│ 服务发现架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 服务A v1 │ │ 服务A v2 │ │ 服务A v3 │ │
│ │ 192.168.1.1 │ │ 192.168.1.2 │ │ 192.168.1.3 │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └────────────────┬┴─────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 注册中心 │ │
│ │ (Nacos/Zookeeper│ │
│ │ Consul/Eureka)│ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 消费者 │ │
│ │ 服务发现 + 负载 │ │
│ └─────────────────┘ │
│ │
│ 注册中心核心功能: │
│ - 服务注册:心跳检测、自动剔除 │
│ - 服务发现:动态感知服务变更 │
│ - 负载均衡:多种策略(轮询/随机/权重) │
│ │
└─────────────────────────────────────────────────────────────┘
7.3 自动扩缩容
# Kubernetes HPA 自动扩缩容配置
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-service-hpa
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
minReplicas: 3 # 最小 3 个 Pod
maxReplicas: 50 # 最大 50 个 Pod
metrics:
# CPU 使用率 > 70% 时扩容
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
# 内存使用率 > 80% 时扩容
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleUp:
stabilizationWindowSeconds: 30 # 扩容冷却 30 秒
policies:
- type: Percent
value: 100 # 每次最多扩容 100%
periodSeconds: 15
scaleDown:
stabilizationWindowSeconds: 300 # 缩容冷却 5 分钟
八、热点数据问题
8.1 热点 key 探测
热点数据问题:
┌─────────────────────────────────────────────────────────────┐
│ 热点数据问题 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 什么是热点数据? │
│ - 某条数据的访问量远高于其他数据 │
│ - 例如:秒杀商品、明星结婚官宣、热门新闻 │
│ │
│ 热点数据的危害: │
│ - 单机 Redis 无法承受(热点 key 独享 CPU) │
│ - 热点 key 所在节点成为瓶颈 │
│ - 可能导致集群雪崩 │
│ │
│ 热点探测方案: │
│ 1. 客户端统计:本地累加 + 定时上报 │
│ 2. 代理层统计:Twemproxy / Codis 统计 │
│ 3. 服务端统计:Redis 4.0+ 的 MONITOR 命令(生产慎用) │
│ 4. 独立探测系统:京东 / 阿里有专门的热点探测服务 │
│ │
└─────────────────────────────────────────────────────────────┘
// 热点 key 探测实现
@Service
public class HotKeyDetector {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 本地窗口计数器
private final Map<String, AtomicLong> localCounter = new ConcurrentHashMap<>();
// 滑动窗口:统计最近 1 秒的请求数
public void recordAccess(String key) {
// 本地计数
localCounter.computeIfAbsent(key, k -> new AtomicLong(0))
.incrementAndGet();
// 定时上报(每 100ms)
reportHotKeys();
}
@Scheduled(fixedRate = 100)
public void reportHotKeys() {
if (localCounter.isEmpty()) return;
Map<String, Long> snapshot = new HashMap<>();
localCounter.forEach((key, counter) -> {
long count = counter.getAndSet(0);
if (count > 0) {
snapshot.put(key, count);
}
});
if (!snapshot.isEmpty()) {
// 上报到 Redis 聚合
String hashKey = "hotkey:counter:" + LocalDateTime.now().format(
DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
);
redisTemplate.opsForHash().putAll(hashKey, snapshot);
redisTemplate.expire(hashKey, 1, TimeUnit.HOURS);
}
}
// 获取热点 key
public Set<String> getHotKeys(int topN) {
Set<String> hotKeys = new HashSet<>();
// 聚合所有窗口的计数
// 取 Top N
return hotKeys;
}
}
8.2 热点数据分散
热点分散策略:
┌─────────────────────────────────────────────────────────────┐
│ 热点分散方案 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 方案一:热点 key 加后缀分散 │
│ 原始 key: seckill:stock:1001 │
│ 分散后: seckill:stock:1001_0 │
│ seckill:stock:1001_1 │
│ seckill:stock:1001_2 │
│ seckill:stock:1001_3 │
│ │
│ 方案二:本地缓存 + 分布式锁 │
│ - 热点数据优先读本地缓存 │
│ - 本地缓存miss时加分布式锁只允许一个请求去加载 │
│ │
│ 方案三:读写分离 │
│ - 热点 key 单独部署到高性能机器 │
│ - 读写分离,读操作走多个从节点 │
│ │
└─────────────────────────────────────────────────────────────┘
// 热点 key 分散实现
@Service
public class HotKeyService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 热点 key 分片数量
private static final int HOT_KEY_SHARDS = 4;
/**
* 读取:随机分散到不同分片
*/
public String getHotValue(String key) {
int shard = ThreadLocalRandom.current().nextInt(HOT_KEY_SHARDS);
String shardedKey = key + "_" + shard;
return redisTemplate.opsForValue().get(shardedKey);
}
/**
* 写入:写入所有分片
*/
public void setHotValue(String key, String value) {
for (int i = 0; i < HOT_KEY_SHARDS; i++) {
String shardedKey = key + "_" + i;
redisTemplate.opsForValue().set(shardedKey, value);
}
}
/**
* 扣减:原子操作
*/
public Long decrement(String key) {
// 遍历所有分片,直到成功扣减
for (int i = 0; i < HOT_KEY_SHARDS; i++) {
String shardedKey = key + "_" + i;
Long result = redisTemplate.opsForValue().decrement(shardedKey);
if (result != null && result >= 0) {
return result;
}
// 回滚
redisTemplate.opsForValue().increment(shardedKey);
}
return -1L;
}
}
九、减库存问题
9.1 Redis Lua 原子扣减
-- Lua 脚本:保证库存扣减原子性
-- 防止并发场景下的超卖问题
-- KEYS[1]: 库存 key,如 seckill:stock:1001
-- ARGV[1]: 要扣减的数量,如 1
local stock = redis.call('GET', KEYS[1])
-- 库存不存在
if stock == false then
return -1
end
stock = tonumber(stock)
local deduct = tonumber(ARGV[1])
-- 库存不足
if stock < deduct then
return 0
end
-- 扣减库存
stock = stock - deduct
redis.call('SET', KEYS[1], stock)
-- 返回剩余库存
return stock
// Java 调用 Lua 扣减库存
@Service
public class StockService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private static final String DEDUCT_STOCK_LUA =
"local stock = redis.call('GET', KEYS[1]) " +
"if stock == false then return -1 end " +
"stock = tonumber(stock) " +
"local deduct = tonumber(ARGV[1]) " +
"if stock < deduct then return 0 end " +
"stock = stock - deduct " +
"redis.call('SET', KEYS[1], stock) " +
"return stock";
/**
* 扣减库存
* @return >0 扣减成功,返回剩余库存;0 库存不足;-1 商品不存在
*/
public Long deductStock(String key, int count) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>(DEDUCT_STOCK_LUA, Long.class);
Long result = stringRedisTemplate.execute(script, List.of(key), String.valueOf(count));
return result;
}
}
9.2 乐观锁兜底
// MySQL 乐观锁兜底
@Service
public class StockDbService {
@Autowired
private StockMapper stockMapper;
/**
* 乐观锁扣减库存
* SQL: UPDATE stock SET stock = stock - #{count}, version = version + 1
* WHERE sku_id = #{skuId} AND stock >= #{count} AND version = #{version}
*/
public boolean deductStockWithOptimisticLock(Long skuId, int count) {
// 1. 查询当前库存
Stock stock = stockMapper.selectBySkuId(skuId);
if (stock == null || stock.getStock() < count) {
return false;
}
// 2. 乐观锁更新
int affected = stockMapper.deductWithVersion(skuId, count, stock.getVersion());
if (affected == 0) {
// 乐观锁冲突,重试
log.warn("库存扣减冲突,重试: skuId={}", skuId);
return deductStockWithOptimisticLock(skuId, count);
}
return true;
}
}
<!-- StockMapper.xml -->
<update id="deductWithVersion">
UPDATE stock
SET stock = stock - #{count},
version = version + 1,
updated_at = NOW()
WHERE sku_id = #{skuId}
AND stock >= #{count}
AND version = #{version}
</update>
十、压测与监控
10.1 本地压测工具
压测工具对比:
┌─────────────────────────────────────────────────────────────┐
│ 压测工具对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ JMeter │
│ - 优点:功能强大,图形界面,支持复杂场景 │
│ - 缺点:重量级,资源消耗大 │
│ - 适用:完整链路压测、复杂业务场景 │
│ │
│ wrk / wrk2 │
│ - 优点:轻量级,高性能,C 语言实现 │
│ - 缺点:功能单一,不支持复杂场景 │
│ - 适用:接口性能基准测试 │
│ │
│ ab (Apache Bench) │
│ - 优点:简单易用,Apache 项目自带 │
│ - 缺点:功能有限,不支持持续压测 │
│ - 适用:快速单接口压测 │
│ │
│ vegeta │
│ - 优点:Go 语言,可编程,输出丰富 │
│ - 缺点:学习曲线较陡 │
│ - 适用:Gopher 的选择 │
│ │
└─────────────────────────────────────────────────────────────┘
# wrk 压测示例
# 200 个并发,持续压测 30 秒
wrk -t4 -c200 -d30s --latency http://localhost:8080/api/products
# 结果分析:
# Running 30s test @ http://localhost:8080/api/products
# 4 threads and 200 connections
# Thread Stats Avg Stdev Max +/-Stdev
# Latency 45.32ms 12.45ms 198.00ms 87.00%
# Req/Sec 4.52k 234.89 5.12k 70.00%
# Latency Distribution
# 50% 44.00ms
# 75% 52.00ms
# 90% 60.00ms
# 99% 98.00ms
# 135623 requests in 30.01s, 45.23MB read
# Socket errors: connect 0, read 0, write 0, timeout 5
# Non-2xx or 3xx responses: 12
# Requests/sec: 4518.23
# Transfer/sec: 1.51MB
// JMeter Java Request 实现自定义压测
public class SeckillSampler implements JavaSamplerClient {
@Override
public SampleResult runTest(JavaSamplerContext context) {
SampleResult result = new SampleResult();
result.sampleStart();
try {
// 执行秒杀请求
HttpResponse response = HttpUtil.post(
"http://localhost:8080/api/seckill/do",
JsonUtil.toJson(new SeckillRequest(skuId, userId))
);
result.setResponseCode(response.getStatusCode() + "");
result.setResponseData(response.getBody(), "utf-8");
if (response.getStatusCode() == 200) {
result.setSuccessful(true);
} else {
result.setSuccessful(false);
}
} catch (Exception e) {
result.setSuccessful(false);
result.setResponseMessage(e.getMessage());
} finally {
result.sampleEnd();
}
return result;
}
}
10.2 监控告警体系
监控四大黄金指标:
┌─────────────────────────────────────────────────────────────┐
│ 监控四大黄金指标 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 延迟 (Latency) │
│ - 接口响应时间分布 │
│ - p50/p95/p99/p999 │
│ - 告警:p99 > 2s │
│ │
│ 2. 流量 (Traffic) │
│ - QPS / TPS │
│ - 并发连接数 │
│ - 告警:QPS 突增 50% │
│ │
│ 3. 错误 (Errors) │
│ - 5xx 错误率 │
│ - 超时率 │
│ - 告警:错误率 > 1% │
│ │
│ 4. 饱和度 (Saturation) │
│ - CPU / 内存利用率 │
│ - 磁盘 IO / 网络 IO │
│ - 线程池队列满 │
│ - 告警:CPU > 80% │
│ │
└─────────────────────────────────────────────────────────────┘
// Prometheus + Micrometer 监控指标
@Service
public class MetricsService {
private final MeterRegistry registry;
// 计数器:QPS
private final Counter requestCounter = Counter.builder("http.requests.total")
.tag("uri", "/api/products")
.register(registry);
// 计时器:延迟分布
private final Timer requestTimer = Timer.builder("http.requests.latency")
.tag("uri", "/api/products")
.register(registry);
// 仪表盘:当前并发
private final Gauge concurrentRequests = Gauge.builder("http.requests.concurrent")
.register(registry);
// Histogram:响应时间分布
private final DistributionSummary responseSize = DistributionSummary.builder("http.response.size")
.register(registry);
public void recordRequest(HttpServletRequest request, long duration) {
// 记录 QPS
requestCounter.increment();
// 记录延迟
requestTimer.record(duration, TimeUnit.MILLISECONDS);
// 记录响应大小
responseSize.record(response.getContentLength());
}
}
# Prometheus 告警规则
groups:
- name: high-concurrency-alerts
rules:
# 高延迟告警
- alert: HighLatency
expr: histogram_quantile(0.99, rate(http_requests_latency_seconds_bucket[5m])) > 2
for: 2m
labels:
severity: critical
annotations:
summary: "High latency detected"
description: "p99 latency is s"
# 高错误率告警
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.01
for: 1m
labels:
severity: critical
annotations:
summary: "High error rate"
description: "5xx error rate is %"
# CPU 告警
- alert: HighCPU
expr: rate(process_cpu_seconds_total[1m]) > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "High CPU usage"
description: "CPU usage is %"
十一、高频面试题
Q1: 高并发系统的核心设计思想是什么?
高并发系统的核心是分层防护、层层过滤。把流量挡在离用户最近的那一层,数据库只承受 1% 的压力。具体手段:CDN 扛静态资源、Nginx 限流、网关层熔断降级、应用层多级缓存、数据库读写分离 + 分库分表。每一层都有明确的目标:CDN 扛 60% 静态请求、Nginx 再扛 20% 恶意请求、应用层缓存扛 15% 热点读、数据库只承受 5% 的写。
Q2: 如何衡量一个系统的并发能力?
核心指标有四个:① QPS:每秒请求数,衡量吞吐;② 响应时间分布:p50/p95/p99/p999,关注长尾延迟;③ 并发数:同时处理的请求数,与 QPS 和响应时间相关;④ 错误率:5xx 占比、超时率。计算公式:并发数 = QPS × 平均响应时间。最佳线程数 = CPU 核心数 × 期望利用率 × (1 + 等待时间/处理时间)。
Q3: 限流算法有哪些?令牌桶和漏桶的区别是什么?
限流算法有计数器、滑动窗口、令牌桶、漏桶四种。令牌桶允许一定程度的突发流量(桶内有令牌),适合保护自己的接口;漏桶以固定速率漏出,不允许突发,适合保护下游 MQ/数据库。简单记:令牌桶 = 配额消费,漏桶 = 队列排队。
Q4: 缓存穿透、击穿、雪崩的区别是什么?
三个问题的区别:① 缓存穿透:查询不存在的数据,直接打穿到数据库,解决方案是布隆过滤器 + 缓存空值;② 缓存击穿:热点 key 过期瞬间,大量并发同时打穿到数据库,解决方案是互斥锁 + 逻辑过期;③ 缓存雪崩:大量缓存同时过期,解决方案是随机 TTL + 持久化 + 多级缓存。
Q5: 如何保证 Redis 和数据库的数据一致性?
没有完美的方案,只有权衡:① 旁路缓存:更新数据库后删除缓存,适合读多写少场景;② 双写:更新数据库后同时写缓存,适合一致性要求高的场景,但有并发问题;③ 延迟双删:更新数据库后删缓存,延迟 500ms 再删一次,应对并发读旧值。无论哪种方案都无法 100% 保证一致,只能降低不一致的时间窗口。
Q6: MQ 消息可靠性如何保证?
消息可靠性需要保证三个环节:① 生产端:同步发送 + SendCallback 确认,RocketMQ 可用事务消息;② 存储端:主从复制 + 同步刷盘;③ 消费端:手动 ACK + 消息幂等(用 Redis 做去重)。任何环节出问题都可能丢消息,生产环境必须全链路保障。
Q7: 如何设计一个抗住 100 万 QPS 的系统?
这是一个综合题,考察全链路能力。回答思路:① CDN + 就近接入:扛静态资源和第一波流量;② Nginx 限流:令牌桶,限制到 10 万 QPS;③ 网关层:熔断降级 + 鉴权 + 风控;④ 应用层:多级缓存(本地 + Redis),热点数据预热,限流 + 隔离;⑤ MQ 削峰:异步处理非核心链路;⑥ 数据库:读写分离 + 分库分表 + 乐观锁;⑦ 弹性扩缩容:K8s HPA 自动扩容;⑧ 监控告警:全链路监控 + 异常告警。
参考链接: