dec9085205
- IMPL-001: 响应式编程培训方案 - IMPL-002: 敏感数据加密存储方案 - IMPL-003: 预约高峰期性能优化方案 - IMPL-004: 支付接口幂等性校验方案
655 lines
14 KiB
Markdown
655 lines
14 KiB
Markdown
# IMPL-003: 预约高峰期性能优化方案
|
||
|
||
> 文档编号: GYM-IMPL-003
|
||
> 版本: v1.0
|
||
> 日期: 2026-04-05
|
||
> 作者: 张翔
|
||
> 状态: 正式发布
|
||
|
||
---
|
||
|
||
## 文档修订历史
|
||
|
||
| 版本 | 日期 | 作者 | 修订内容 |
|
||
|------|------|------|---------|
|
||
| v1.0 | 2026-04-05 | 张翔 | 创建预约高峰期性能优化方案 |
|
||
|
||
---
|
||
|
||
## 一、需求分析
|
||
|
||
### 1.1 问题背景
|
||
|
||
预约高峰期QPS仅500-1000,距离目标2000+差距较大,影响用户体验和业务转化。
|
||
|
||
### 1.2 性能问题分析
|
||
|
||
#### 当前性能指标
|
||
|
||
| 指标 | 目标值 | 实际值 | 差距 |
|
||
|------|-------|-------|------|
|
||
| QPS | 2000+ | 500-1000 | 4倍 |
|
||
| 响应时间(P99) | ≤200ms | 600-1000ms | 5倍 |
|
||
| 成功率 | ≥99% | 95-97% | 2-4% |
|
||
|
||
#### 性能瓶颈识别
|
||
|
||
**瓶颈1:数据库查询慢**
|
||
- 缺少索引
|
||
- 全表扫描
|
||
- 慢SQL
|
||
|
||
**瓶颈2:无缓存机制**
|
||
- 每次查询都访问数据库
|
||
- 热点数据未缓存
|
||
- 缓存命中率低
|
||
|
||
**瓶颈3:同步阻塞**
|
||
- 预约高峰期并发处理能力不足
|
||
- 线程池满载
|
||
- 响应时间过长
|
||
|
||
**瓶颈4:缺少消息队列**
|
||
- 无法削峰填谷
|
||
- 流量直接冲击数据库
|
||
- 系统稳定性差
|
||
|
||
### 1.3 成功标准
|
||
|
||
- QPS≥2000
|
||
- 响应时间(P99)≤200ms
|
||
- 成功率≥99%
|
||
- 缓存命中率≥80%
|
||
- 数据库主从延迟≤1秒
|
||
- 消息队列无积压
|
||
|
||
---
|
||
|
||
## 二、技术方案设计
|
||
|
||
### 2.1 优化策略组合
|
||
|
||
#### 策略1:引入Redis缓存
|
||
|
||
**缓存对象**:
|
||
|
||
| 缓存对象 | TTL | 缓存键格式 | 说明 |
|
||
|---------|-----|-----------|------|
|
||
| 课程信息 | 1小时 | course:{id} | 课程详情 |
|
||
| 会员信息 | 30分钟 | member:{id} | 会员信息 |
|
||
| 教练信息 | 1小时 | coach:{id} | 教练信息 |
|
||
| 预约名额 | 5分钟 | reservation:quota:{courseId}:{date} | 剩余名额 |
|
||
|
||
**缓存策略**:
|
||
|
||
```
|
||
Cache-Aside模式:
|
||
1. 读取时先查缓存
|
||
2. 缓存未命中则查数据库
|
||
3. 查询结果写入缓存
|
||
4. 写入时更新缓存
|
||
```
|
||
|
||
**缓存架构**:
|
||
|
||
```
|
||
应用层 → Redis缓存 → 数据库
|
||
↓
|
||
本地缓存(可选)
|
||
```
|
||
|
||
---
|
||
|
||
#### 策略2:数据库读写分离
|
||
|
||
**架构设计**:
|
||
|
||
```
|
||
应用层
|
||
↓
|
||
ShardingSphere-JDBC(路由层)
|
||
↓ ↓
|
||
主库(写) 从库(读)
|
||
```
|
||
|
||
**实现方案**:
|
||
|
||
**主库**:处理写入操作
|
||
- INSERT
|
||
- UPDATE
|
||
- DELETE
|
||
|
||
**从库**:处理读取操作
|
||
- SELECT
|
||
|
||
**主从同步**:半同步模式
|
||
- 主库写入后等待至少一个从库确认
|
||
- 保证数据一致性
|
||
|
||
---
|
||
|
||
#### 策略3:引入消息队列削峰
|
||
|
||
**消息队列选型**:RabbitMQ
|
||
|
||
**削峰方案**:
|
||
|
||
```
|
||
预约请求流程:
|
||
1. 用户发起预约请求
|
||
2. 请求写入消息队列
|
||
3. 立即返回"预约中"状态
|
||
4. 后台消费者异步处理
|
||
5. 处理完成后通知用户
|
||
```
|
||
|
||
**消息队列架构**:
|
||
|
||
```
|
||
生产者 → Exchange → Queue → 消费者
|
||
↓
|
||
死信队列(失败重试)
|
||
```
|
||
|
||
**流量控制**:
|
||
|
||
- 消费速率:2000 TPS
|
||
- 队列容量:10000条消息
|
||
- 超出容量:拒绝请求
|
||
|
||
---
|
||
|
||
### 2.2 技术选型
|
||
|
||
| 组件 | 技术选型 | 版本 | 说明 |
|
||
|------|---------|------|------|
|
||
| 缓存 | Redis | 6.2+ | 高性能缓存 |
|
||
| 数据库中间件 | ShardingSphere-JDBC | 5.3+ | 读写分离路由 |
|
||
| 消息队列 | RabbitMQ | 3.9+ | 削峰填谷 |
|
||
|
||
---
|
||
|
||
## 三、代码结构设计
|
||
|
||
### 3.1 缓存层设计
|
||
|
||
#### 包结构
|
||
|
||
```
|
||
com.gym.manage.cache/
|
||
├── config/
|
||
│ ├── RedisConfig.java # Redis配置
|
||
│ └── CacheConfig.java # 缓存配置
|
||
├── service/
|
||
│ ├── CacheService.java # 缓存服务接口
|
||
│ └── CacheServiceImpl.java # 缓存服务实现
|
||
└── aspect/
|
||
└── CacheAspect.java # 缓存切面
|
||
```
|
||
|
||
#### 核心类设计
|
||
|
||
**RedisConfig**:
|
||
|
||
```java
|
||
@Configuration
|
||
public class RedisConfig {
|
||
|
||
@Bean
|
||
public RedisTemplate<String, Object> redisTemplate(
|
||
RedisConnectionFactory factory
|
||
) {
|
||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||
template.setConnectionFactory(factory);
|
||
|
||
// 使用Jackson序列化
|
||
Jackson2JsonRedisSerializer<Object> serializer =
|
||
new Jackson2JsonRedisSerializer<>(Object.class);
|
||
|
||
template.setKeySerializer(new StringRedisSerializer());
|
||
template.setValueSerializer(serializer);
|
||
template.setHashKeySerializer(new StringRedisSerializer());
|
||
template.setHashValueSerializer(serializer);
|
||
|
||
return template;
|
||
}
|
||
}
|
||
```
|
||
|
||
**CacheService**:
|
||
|
||
```java
|
||
@Service
|
||
public class CacheServiceImpl implements CacheService {
|
||
|
||
@Autowired
|
||
private RedisTemplate<String, Object> redisTemplate;
|
||
|
||
@Override
|
||
public <T> T get(String key, Class<T> type) {
|
||
Object value = redisTemplate.opsForValue().get(key);
|
||
return value != null ? (T) value : null;
|
||
}
|
||
|
||
@Override
|
||
public void set(String key, Object value, Duration ttl) {
|
||
redisTemplate.opsForValue().set(key, value, ttl);
|
||
}
|
||
|
||
@Override
|
||
public void delete(String key) {
|
||
redisTemplate.delete(key);
|
||
}
|
||
|
||
@Override
|
||
public boolean hasKey(String key) {
|
||
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
|
||
}
|
||
}
|
||
```
|
||
|
||
**CacheAspect**:
|
||
|
||
```java
|
||
@Aspect
|
||
@Component
|
||
public class CacheAspect {
|
||
|
||
@Autowired
|
||
private CacheService cacheService;
|
||
|
||
@Around("@annotation(cacheable)")
|
||
public Object around(ProceedingJoinPoint pjp, Cacheable cacheable)
|
||
throws Throwable {
|
||
|
||
String key = generateKey(cacheable.key(), pjp.getArgs());
|
||
|
||
// 先查缓存
|
||
Object cached = cacheService.get(key, cacheable.type());
|
||
if (cached != null) {
|
||
return cached;
|
||
}
|
||
|
||
// 缓存未命中,执行方法
|
||
Object result = pjp.proceed();
|
||
|
||
// 写入缓存
|
||
if (result != null) {
|
||
cacheService.set(key, result, Duration.ofMinutes(cacheable.ttl()));
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
private String generateKey(String pattern, Object[] args) {
|
||
// 生成缓存键
|
||
return String.format(pattern, args);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3.2 数据库层设计
|
||
|
||
#### 包结构
|
||
|
||
```
|
||
com.gym.manage.datasource/
|
||
├── config/
|
||
│ ├── MasterSlaveConfig.java # 主从配置
|
||
│ └── ShardingConfig.java # 分片配置
|
||
├── routing/
|
||
│ ├── DynamicDataSource.java # 动态数据源
|
||
│ └── DataSourceRouter.java # 数据源路由
|
||
└── annotation/
|
||
└── ReadOnly.java # 只读注解
|
||
```
|
||
|
||
#### 核心类设计
|
||
|
||
**MasterSlaveConfig**:
|
||
|
||
```java
|
||
@Configuration
|
||
public class MasterSlaveConfig {
|
||
|
||
@Bean
|
||
@ConfigurationProperties(prefix = "spring.datasource.master")
|
||
public DataSource masterDataSource() {
|
||
return DataSourceBuilder.create().build();
|
||
}
|
||
|
||
@Bean
|
||
@ConfigurationProperties(prefix = "spring.datasource.slave")
|
||
public DataSource slaveDataSource() {
|
||
return DataSourceBuilder.create().build();
|
||
}
|
||
|
||
@Bean
|
||
public DynamicDataSource dynamicDataSource(
|
||
@Qualifier("masterDataSource") DataSource master,
|
||
@Qualifier("slaveDataSource") DataSource slave
|
||
) {
|
||
Map<Object, Object> targetDataSources = new HashMap<>();
|
||
targetDataSources.put("master", master);
|
||
targetDataSources.put("slave", slave);
|
||
|
||
DynamicDataSource dynamicDataSource = new DynamicDataSource();
|
||
dynamicDataSource.setDefaultTargetDataSource(master);
|
||
dynamicDataSource.setTargetDataSources(targetDataSources);
|
||
|
||
return dynamicDataSource;
|
||
}
|
||
}
|
||
```
|
||
|
||
**DynamicDataSource**:
|
||
|
||
```java
|
||
public class DynamicDataSource extends AbstractRoutingDataSource {
|
||
|
||
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
|
||
|
||
@Override
|
||
protected Object determineCurrentLookupKey() {
|
||
return CONTEXT_HOLDER.get();
|
||
}
|
||
|
||
public static void setMaster() {
|
||
CONTEXT_HOLDER.set("master");
|
||
}
|
||
|
||
public static void setSlave() {
|
||
CONTEXT_HOLDER.set("slave");
|
||
}
|
||
|
||
public static void clear() {
|
||
CONTEXT_HOLDER.remove();
|
||
}
|
||
}
|
||
```
|
||
|
||
**DataSourceRouter**:
|
||
|
||
```java
|
||
@Aspect
|
||
@Component
|
||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||
public class DataSourceRouter {
|
||
|
||
@Around("@annotation(readOnly)")
|
||
public Object route(ProceedingJoinPoint pjp, ReadOnly readOnly)
|
||
throws Throwable {
|
||
|
||
try {
|
||
DynamicDataSource.setSlave();
|
||
return pjp.proceed();
|
||
} finally {
|
||
DynamicDataSource.clear();
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**ReadOnly注解**:
|
||
|
||
```java
|
||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||
@Retention(RetentionPolicy.RUNTIME)
|
||
@Documented
|
||
public @interface ReadOnly {
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3.3 消息队列层设计
|
||
|
||
#### 包结构
|
||
|
||
```
|
||
com.gym.manage.mq/
|
||
├── config/
|
||
│ └── RabbitMQConfig.java # RabbitMQ配置
|
||
├── producer/
|
||
│ └── ReservationProducer.java # 预约生产者
|
||
├── consumer/
|
||
│ └── ReservationConsumer.java # 预约消费者
|
||
└── message/
|
||
└── ReservationMessage.java # 预约消息
|
||
```
|
||
|
||
#### 核心类设计
|
||
|
||
**RabbitMQConfig**:
|
||
|
||
```java
|
||
@Configuration
|
||
public class RabbitMQConfig {
|
||
|
||
public static final String EXCHANGE = "reservation.exchange";
|
||
public static final String QUEUE = "reservation.queue";
|
||
public static final String ROUTING_KEY = "reservation.create";
|
||
|
||
@Bean
|
||
public DirectExchange exchange() {
|
||
return new DirectExchange(EXCHANGE);
|
||
}
|
||
|
||
@Bean
|
||
public Queue queue() {
|
||
return QueueBuilder.durable(QUEUE)
|
||
.withArgument("x-message-ttl", 60000) // 消息TTL: 1分钟
|
||
.withArgument("x-dead-letter-exchange", "reservation.dlx")
|
||
.build();
|
||
}
|
||
|
||
@Bean
|
||
public Binding binding() {
|
||
return BindingBuilder.bind(queue())
|
||
.to(exchange())
|
||
.with(ROUTING_KEY);
|
||
}
|
||
}
|
||
```
|
||
|
||
**ReservationProducer**:
|
||
|
||
```java
|
||
@Service
|
||
public class ReservationProducer {
|
||
|
||
@Autowired
|
||
private RabbitTemplate rabbitTemplate;
|
||
|
||
public void sendReservation(ReservationMessage message) {
|
||
rabbitTemplate.convertAndSend(
|
||
RabbitMQConfig.EXCHANGE,
|
||
RabbitMQConfig.ROUTING_KEY,
|
||
message
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
**ReservationConsumer**:
|
||
|
||
```java
|
||
@Service
|
||
public class ReservationConsumer {
|
||
|
||
@Autowired
|
||
private ReservationService reservationService;
|
||
|
||
@RabbitListener(queues = RabbitMQConfig.QUEUE)
|
||
public void handleReservation(ReservationMessage message) {
|
||
try {
|
||
// 处理预约
|
||
reservationService.processReservation(message);
|
||
|
||
} catch (Exception e) {
|
||
// 异常处理,消息进入死信队列
|
||
throw new AmqpRejectAndDontRequeueException(e);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 四、测试方案
|
||
|
||
### 4.1 性能测试
|
||
|
||
#### 测试工具
|
||
|
||
- JMeter:压力测试
|
||
- Gatling:性能测试
|
||
- Prometheus + Grafana:监控
|
||
|
||
#### 测试场景
|
||
|
||
**场景1:预约高峰期模拟**
|
||
|
||
```scala
|
||
scenario("预约高峰期")
|
||
.exec(http("预约课程")
|
||
.post("/api/reservations")
|
||
.body(StringBody("""{"courseId":1,"memberId":1}"""))
|
||
.check(status.is(200))
|
||
)
|
||
.inject(
|
||
rampUsersPerSec(100) to 2000 during (300 seconds)
|
||
)
|
||
.protocols(httpProtocol)
|
||
```
|
||
|
||
**场景2:缓存命中率测试**
|
||
|
||
```java
|
||
@Test
|
||
public void testCacheHitRate() {
|
||
int totalRequests = 1000;
|
||
int cacheHits = 0;
|
||
|
||
for (int i = 0; i < totalRequests; i++) {
|
||
Course course = courseService.getCourseById(1L);
|
||
if (cacheService.hasKey("course:1")) {
|
||
cacheHits++;
|
||
}
|
||
}
|
||
|
||
double hitRate = (double) cacheHits / totalRequests;
|
||
System.out.println("缓存命中率: " + (hitRate * 100) + "%");
|
||
assertTrue(hitRate >= 0.8, "缓存命中率应≥80%");
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4.2 压力测试
|
||
|
||
#### 测试步骤
|
||
|
||
1. 逐步增加并发数(500 → 1000 → 2000 → 3000)
|
||
2. 监控系统资源(CPU、内存、网络)
|
||
3. 记录性能指标(QPS、响应时间、成功率)
|
||
4. 识别系统极限
|
||
|
||
#### 性能指标
|
||
|
||
| 并发数 | QPS | 响应时间(P99) | 成功率 | CPU利用率 | 内存利用率 |
|
||
|--------|-----|--------------|--------|----------|-----------|
|
||
| 500 | 500 | 100ms | 99% | 30% | 50% |
|
||
| 1000 | 1000 | 150ms | 99% | 50% | 60% |
|
||
| 2000 | 2000 | 200ms | 99% | 70% | 70% |
|
||
| 3000 | 2500 | 300ms | 95% | 90% | 80% |
|
||
|
||
---
|
||
|
||
### 4.3 稳定性测试
|
||
|
||
#### 测试步骤
|
||
|
||
1. 持续运行2小时
|
||
2. 监控内存泄漏
|
||
3. 监控GC频率
|
||
4. 记录系统稳定性指标
|
||
|
||
#### 稳定性指标
|
||
|
||
- 内存占用稳定
|
||
- GC频率正常
|
||
- 无内存泄漏
|
||
- 无死锁
|
||
|
||
---
|
||
|
||
## 五、实施步骤
|
||
|
||
| 步骤 | 任务 | 负责人 | 完成时间 | 验收标准 |
|
||
|------|------|--------|---------|---------|
|
||
| 1 | Redis环境搭建 | 运维工程师 | 1天 | Redis服务正常运行 |
|
||
| 2 | 实现缓存服务 | 后端开发 | 2天 | 单元测试通过 |
|
||
| 3 | 数据库主从配置 | 运维工程师 | 1天 | 主从同步正常 |
|
||
| 4 | 实现读写分离 | 后端开发 | 3天 | 集成测试通过 |
|
||
| 5 | RabbitMQ环境搭建 | 运维工程师 | 1天 | RabbitMQ服务正常运行 |
|
||
| 6 | 实现消息队列 | 后端开发 | 2天 | 单元测试通过 |
|
||
| 7 | 性能测试 | 测试工程师 | 2天 | 性能指标达标 |
|
||
| 8 | 灰度发布 | 运维工程师 | 1天 | 灰度发布成功 |
|
||
|
||
---
|
||
|
||
## 六、验收标准
|
||
|
||
### 6.1 性能验收
|
||
|
||
- [ ] QPS≥2000
|
||
- [ ] 响应时间(P99)≤200ms
|
||
- [ ] 成功率≥99%
|
||
|
||
### 6.2 缓存验收
|
||
|
||
- [ ] 缓存命中率≥80%
|
||
- [ ] 缓存穿透防护有效
|
||
- [ ] 缓存雪崩防护有效
|
||
|
||
### 6.3 数据库验收
|
||
|
||
- [ ] 数据库主从延迟≤1秒
|
||
- [ ] 读写分离正常
|
||
- [ ] 数据一致性保证
|
||
|
||
### 6.4 消息队列验收
|
||
|
||
- [ ] 消息队列无积压
|
||
- [ ] 消息不丢失
|
||
- [ ] 消费速率达标
|
||
|
||
---
|
||
|
||
## 七、风险与应对
|
||
|
||
### 7.1 风险识别
|
||
|
||
**风险1:缓存穿透**
|
||
- 应对:布隆过滤器 + 空值缓存
|
||
|
||
**风险2:缓存雪崩**
|
||
- 应对:随机过期时间 + 多级缓存
|
||
|
||
**风险3:主从延迟**
|
||
- 应对:关键业务读主库 + 半同步复制
|
||
|
||
**风险4:消息队列积压**
|
||
- 应对:监控告警 + 动态扩容消费者
|
||
|
||
---
|
||
|
||
## 八、相关文档
|
||
|
||
- [改进路线图](../05-PLANS/改进路线图.md)
|
||
- [EVAL-002-性能与可扩展性评估报告](../03-EVALUATION/EVAL-002-性能与可扩展性评估报告.md)
|
||
- [ADR-002-响应式编程选型](../02-ARCHITECTURE/架构决策记录/ADR-002-响应式编程选型.md)
|