# 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 redisTemplate( RedisConnectionFactory factory ) { RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 使用Jackson序列化 Jackson2JsonRedisSerializer 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 redisTemplate; @Override public T get(String key, Class 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 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 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)