Files
gym-manage/docs/06-IMPLEMENTATION/IMPL-003-预约高峰期性能优化方案.md
T
张翔 dec9085205 docs: 创建P0和P1改进项实现方案
- IMPL-001: 响应式编程培训方案
- IMPL-002: 敏感数据加密存储方案
- IMPL-003: 预约高峰期性能优化方案
- IMPL-004: 支付接口幂等性校验方案
2026-04-05 16:48:27 +08:00

655 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)