📖 开场:消防演习
想象学校的消防演习 🚒:
不演习(真火灾慌乱):
markdown
真实火灾 🔥
↓
学生:不知道怎么逃 😱
老师:不知道怎么组织 😱
↓
乱成一团 💀
结果:
- 伤亡惨重 ❌
- 损失巨大 ❌
经常演习(有条不紊):
markdown
消防演习 🧯
↓
学生:按演习路线疏散 🚶
老师:按预案组织 👨🏫
↓
有序撤离 ✅
真实火灾时:
↓
按演习经验应对
↓
安全撤离 ✅
结果:
- 零伤亡 ✅
- 损失最小 ✅
这就是压测:提前发现系统瓶颈!
🤔 为什么需要全链路压测?
问题:生产事故不可预测 💀
markdown
双11前夕:
开发:代码没问题 ✅
测试:功能测试通过 ✅
↓
双11当天:
流量暴增100倍 🔥
↓
服务器崩溃 💀
数据库挂了 💀
订单丢失 💀
结果:
- 损失千万 ❌
- 用户投诉 ❌
有压测:
diff
双11前1个月:
全链路压测 🧪
↓
模拟100倍流量
↓
发现问题:
- 数据库连接池不够 ⚠️
- Redis内存不足 ⚠️
- 接口响应慢 ⚠️
↓
提前优化 ✅
双11当天:
↓
系统稳定运行 ✅
🎯 核心挑战
挑战1:数据隔离 🔒
markdown
问题:
压测流量 → 写入真实数据库 💀
↓
生产数据被污染 ❌
解决:
影子表/影子库 ✅
挑战2:流量染色 🎨
arduino
问题:
如何区分压测流量和真实流量?
解决:
请求头标记:X-Stress-Test: true ✅
挑战3:全链路 🔗
markdown
压测必须覆盖:
用户 → 网关 → 订单服务 → 库存服务 → 数据库
↓ ↓ ↓
限流 缓存 消息队列
所有环节都要测!✅
🎯 核心设计
设计1:流量染色 🎨
java
@Component
public class StressTestInterceptor implements HandlerInterceptor {
private static final String STRESS_TEST_HEADER = "X-Stress-Test";
/**
* ⭐ 检查是否是压测流量
*/
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// 1. 检查请求头
String stressTest = request.getHeader(STRESS_TEST_HEADER);
if ("true".equals(stressTest)) {
// ⭐ 压测流量,标记到ThreadLocal
StressTestContext.setStressTest(true);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
// 清理ThreadLocal
StressTestContext.clear();
}
}
/**
* ⭐ 压测上下文(ThreadLocal)
*/
public class StressTestContext {
private static final ThreadLocal<Boolean> STRESS_TEST = new ThreadLocal<>();
public static void setStressTest(boolean isStressTest) {
STRESS_TEST.set(isStressTest);
}
public static boolean isStressTest() {
return Boolean.TRUE.equals(STRESS_TEST.get());
}
public static void clear() {
STRESS_TEST.remove();
}
}
设计2:影子表 👻
数据库路由
java
@Component
public class StressTestDataSourceRouter {
/**
* ⭐ 根据是否压测,选择数据源
*/
public DataSource getDataSource() {
if (StressTestContext.isStressTest()) {
// 压测流量 → 影子库
return shadowDataSource;
} else {
// 正常流量 → 正式库
return normalDataSource;
}
}
}
表名路由
java
@Aspect
@Component
public class StressTestTableAspect {
/**
* ⭐ 拦截Mapper方法,修改表名
*/
@Around("@annotation(org.apache.ibatis.annotations.Select) || " +
"@annotation(org.apache.ibatis.annotations.Insert) || " +
"@annotation(org.apache.ibatis.annotations.Update) || " +
"@annotation(org.apache.ibatis.annotations.Delete)")
public Object aroundMapper(ProceedingJoinPoint joinPoint) throws Throwable {
if (StressTestContext.isStressTest()) {
// ⭐ 压测流量,修改SQL中的表名
// t_order → t_order_shadow
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof String) {
String sql = (String) args[i];
args[i] = replaceShadowTable(sql);
}
}
}
return joinPoint.proceed(args);
}
/**
* 替换为影子表
*/
private String replaceShadowTable(String sql) {
// t_order → t_order_shadow
return sql.replaceAll("t_order(?!_shadow)", "t_order_shadow")
.replaceAll("t_user(?!_shadow)", "t_user_shadow");
}
}
MyBatis拦截器(推荐)⭐⭐⭐
java
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare",
args = {Connection.class, Integer.class})
})
@Component
public class StressTestSqlInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
if (StressTestContext.isStressTest()) {
// ⭐ 压测流量,修改SQL
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// 获取BoundSql
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
// 替换表名
String newSql = replaceShadowTable(sql);
// 反射修改SQL
Field sqlField = BoundSql.class.getDeclaredField("sql");
sqlField.setAccessible(true);
sqlField.set(boundSql, newSql);
}
return invocation.proceed();
}
private String replaceShadowTable(String sql) {
// 正则替换表名
return sql.replaceAll("\bt_order\b", "t_order_shadow")
.replaceAll("\bt_user\b", "t_user_shadow");
}
}
设计3:消息队列隔离 📨
java
@Service
public class OrderMQService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* ⭐ 发送订单消息(压测流量发送到影子Topic)
*/
public void sendOrderMessage(Order order) {
String topic;
if (StressTestContext.isStressTest()) {
// 压测流量 → 影子Topic
topic = "order-topic-shadow";
} else {
// 正常流量 → 正式Topic
topic = "order-topic";
}
rocketMQTemplate.syncSend(topic, order);
}
}
/**
* ⭐ 消费影子Topic
*/
@Component
@RocketMQMessageListener(
topic = "order-topic-shadow",
consumerGroup = "order-consumer-shadow"
)
public class ShadowOrderConsumer implements RocketMQListener<Order> {
@Override
public void onMessage(Order order) {
// 标记为压测流量
StressTestContext.setStressTest(true);
try {
// 处理订单(会写入影子表)
orderService.process(order);
} finally {
StressTestContext.clear();
}
}
}
设计4:Redis隔离 💾
java
@Service
public class StressTestRedisService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* ⭐ 获取Redis Key(压测流量添加后缀)
*/
private String getKey(String key) {
if (StressTestContext.isStressTest()) {
return key + ":shadow"; // user:123 → user:123:shadow
}
return key;
}
public void set(String key, String value) {
redisTemplate.opsForValue().set(getKey(key), value);
}
public String get(String key) {
return redisTemplate.opsForValue().get(getKey(key));
}
}
设计5:压测平台 🖥️
java
@RestController
@RequestMapping("/stress-test")
public class StressTestController {
@Autowired
private StressTestService stressTestService;
/**
* ⭐ 创建压测任务
*/
@PostMapping("/task")
public Result<Long> createTask(@RequestBody StressTestTask task) {
// 1. 校验参数
if (task.getTargetQPS() <= 0) {
return Result.fail("目标QPS必须大于0");
}
// 2. 创建压测任务
Long taskId = stressTestService.createTask(task);
return Result.success(taskId);
}
/**
* ⭐ 启动压测
*/
@PostMapping("/task/{taskId}/start")
public Result<Void> startTask(@PathVariable Long taskId) {
stressTestService.startTask(taskId);
return Result.success();
}
/**
* ⭐ 停止压测
*/
@PostMapping("/task/{taskId}/stop")
public Result<Void> stopTask(@PathVariable Long taskId) {
stressTestService.stopTask(taskId);
return Result.success();
}
/**
* 查询压测报告
*/
@GetMapping("/task/{taskId}/report")
public Result<StressTestReport> getReport(@PathVariable Long taskId) {
StressTestReport report = stressTestService.getReport(taskId);
return Result.success(report);
}
}
设计6:压测执行器 ⚙️
java
@Service
public class StressTestExecutor {
@Autowired
private RestTemplate restTemplate;
/**
* ⭐ 执行压测
*/
public void execute(StressTestTask task) {
// 目标QPS
int targetQPS = task.getTargetQPS();
// 计算每秒发送请求数
int requestsPerSecond = targetQPS;
// 计算请求间隔(毫秒)
long interval = 1000 / requestsPerSecond;
// 压测持续时间(秒)
int duration = task.getDuration();
long startTime = System.currentTimeMillis();
long endTime = startTime + duration * 1000;
// 统计
AtomicInteger totalCount = new AtomicInteger(0);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failCount = new AtomicInteger(0);
// ⭐ 多线程发送请求
ExecutorService executor = Executors.newFixedThreadPool(10);
while (System.currentTimeMillis() < endTime) {
executor.submit(() -> {
try {
// 构造请求
HttpHeaders headers = new HttpHeaders();
headers.set("X-Stress-Test", "true"); // ⭐ 压测标记
HttpEntity<String> request = new HttpEntity<>(headers);
// 发送请求
long start = System.currentTimeMillis();
ResponseEntity<String> response = restTemplate.exchange(
task.getUrl(),
HttpMethod.GET,
request,
String.class
);
long cost = System.currentTimeMillis() - start;
totalCount.incrementAndGet();
if (response.getStatusCode().is2xxSuccessful()) {
successCount.incrementAndGet();
} else {
failCount.incrementAndGet();
}
// 记录响应时间
recordResponseTime(task.getId(), cost);
} catch (Exception e) {
totalCount.incrementAndGet();
failCount.incrementAndGet();
e.printStackTrace();
}
});
// 控制QPS
try {
Thread.sleep(interval);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
executor.shutdown();
// 生成报告
generateReport(task.getId(), totalCount.get(),
successCount.get(), failCount.get());
}
}
🎓 面试题速答
Q1: 什么是全链路压测?
A : 模拟真实流量,覆盖所有环节:
markdown
全链路:
用户 → 网关 → 订单服务 → 库存服务 → 数据库
↓ ↓ ↓
限流 缓存 消息队列
压测覆盖所有环节 ✅
目的:
- 发现系统瓶颈
- 验证容量规划
- 提前优化
Q2: 如何区分压测流量和真实流量?
A : 流量染色:
java
// 请求头标记
headers.set("X-Stress-Test", "true");
// ThreadLocal存储
StressTestContext.setStressTest(true);
// 后续所有操作都能判断
if (StressTestContext.isStressTest()) {
// 压测流量处理
}
Q3: 如何保证数据隔离?
A : 影子表/影子库:
sql
方案1:影子表
t_order → t_order_shadow(同库不同表)
方案2:影子库
order_db → order_db_shadow(不同库)
实现:MyBatis拦截器修改SQL
Q4: 消息队列如何隔离?
A : 影子Topic:
java
String topic;
if (StressTestContext.isStressTest()) {
topic = "order-topic-shadow"; // 影子Topic
} else {
topic = "order-topic"; // 正式Topic
}
rocketMQTemplate.send(topic, message);
Q5: Redis如何隔离?
A : Key添加后缀:
java
private String getKey(String key) {
if (StressTestContext.isStressTest()) {
return key + ":shadow";
}
return key;
}
// user:123 → user:123:shadow
Q6: 如何清理压测数据?
A : 定时清理:
java
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点
public void cleanShadowData() {
// 清理影子表
jdbcTemplate.execute("TRUNCATE TABLE t_order_shadow");
// 清理影子Redis
Set<String> keys = redisTemplate.keys("*:shadow");
redisTemplate.delete(keys);
}
🎬 总结
markdown
全链路压测系统核心
┌────────────────────────────────────┐
│ 1. 流量染色 ⭐ │
│ - 请求头标记 │
│ - ThreadLocal传递 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 2. 影子表 👻 │
│ - MyBatis拦截器 │
│ - 修改表名/库名 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 3. MQ隔离 │
│ - 影子Topic │
│ - 影子Consumer │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 4. Redis隔离 │
│ - Key添加:shadow后缀 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 5. 压测平台 │
│ - 创建任务 │
│ - 启动/停止 │
│ - 生成报告 │
└────────────────────────────────────┘
🎉 恭喜你!
你已经完全掌握了全链路压测系统的设计!🎊
核心要点:
- 流量染色:请求头标记 + ThreadLocal传递
- 影子表:MyBatis拦截器修改SQL
- MQ隔离:影子Topic
- Redis隔离:Key添加:shadow后缀
- 压测平台:任务管理 + 执行器
下次面试,这样回答:
"全链路压测系统通过流量染色和数据隔离实现。流量染色在请求头添加X-Stress-Test标记,拦截器检测到后存入ThreadLocal,后续所有操作都能判断是否压测流量。
数据隔离使用影子表实现。通过MyBatis拦截器拦截SQL执行,检测到压测流量时,正则替换表名,如t_order替换为t_order_shadow。这样压测数据写入影子表,不污染生产数据。影子表结构与正式表完全相同,但数据完全隔离。
消息队列隔离使用影子Topic。判断压测流量时,消息发送到order-topic-shadow而非order-topic。创建影子Consumer消费影子Topic,消费时标记ThreadLocal为压测流量,后续写入影子表。
Redis隔离通过Key后缀实现。封装Redis操作,getKey方法判断压测流量时,给key添加':shadow'后缀。如user:123变为user:123:shadow,实现数据隔离。
压测平台提供任务管理。创建压测任务配置目标QPS、持续时间、压测URL。执行器多线程发送请求,请求头添加压测标记,控制发送间隔实现目标QPS。统计成功数、失败数、响应时间生成压测报告。定时任务清理影子数据。"
面试官:👍 "很好!你对全链路压测的设计理解很深刻!"
本文完 🎬
上一篇 : 223-设计一个分布式Session管理方案.md
下一篇 : 225-设计一个灰度发布系统.md
作者注 :写完这篇,我觉得压测太重要了!🧪
如果这篇文章对你有帮助,请给我一个Star⭐!