1. 问题背景与案例概述
最近接手了一个电商平台的数据库性能优化项目,该系统日均订单量约50万,高峰期QPS达到3000+。随着业务量增长,用户开始频繁抱怨"商品详情页加载慢"和"订单提交卡顿"。通过监控系统发现,部分核心SQL查询响应时间从原来的200ms飙升到5s以上,严重影响了用户体验。
这个案例中,我们面对的是一个典型的OLTP系统性能瓶颈问题。数据库使用的是MySQL 5.7,采用主从架构,服务器配置为16核64G内存,SSD存储。问题主要集中在商品查询和订单处理两个业务模块。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 性能诊断方法论
2.1 监控指标分析
首先我们检查了关键性能指标:
- CPU使用率长期维持在80%以上
- 磁盘I/O等待时间超过30ms
- 缓冲池命中率只有85%(理想值应>95%)
- 临时表创建次数异常高
通过SHOW GLOBAL STATUS命令发现几个异常值:
sql复制Created_tmp_disk_tables = 1200/sec
Select_scan = 500/sec
Innodb_row_lock_waits = 200/sec
2.2 慢查询日志分析
启用慢查询日志(设置long_query_time=1s)后,发现主要慢查询集中在以下几个模式:
- 多表关联查询商品信息
sql复制SELECT p.*, s.stock, d.discount
FROM products p
LEFT JOIN stock s ON p.id = s.product_id
LEFT JOIN discounts d ON p.category = d.category
WHERE p.status = 1
ORDER BY p.sales DESC
LIMIT 100;
- 订单统计报表查询
sql复制SELECT user_id, COUNT(*) as order_count, SUM(amount) as total_amount
FROM orders
WHERE create_time BETWEEN '2023-07-01' AND '2023-07-31'
GROUP BY user_id
HAVING COUNT(*) > 5;
2.3 EXPLAIN执行计划解析
对第一个慢查询执行EXPLAIN分析:
| id | select_type | table | type | key | rows | Extra |
|---|---|---|---|---|---|---|
| 1 | SIMPLE | p | ALL | NULL | 200K | Using where |
| 1 | SIMPLE | s | ref | PRIMARY | 1 | NULL |
| 1 | SIMPLE | d | ALL | NULL | 100 | Using join buffer |
关键问题点:
- products表全表扫描(type=ALL)
- discounts表没有有效索引
- 使用了join buffer(内存临时表)
3. 优化方案设计与实施
3.1 索引优化策略
针对发现的索引问题,我们实施了以下优化:
- 为products表添加组合索引:
sql复制ALTER TABLE products
ADD INDEX idx_status_sales (status, sales DESC);
- 为discounts表添加category索引:
sql复制ALTER TABLE discounts
ADD INDEX idx_category (category);
- 优化后的EXPLAIN结果:
| id | select_type | table | type | key | rows | Extra |
|---|---|---|---|---|---|---|
| 1 | SIMPLE | p | ref | idx_status_sales | 50K | Using index |
| 1 | SIMPLE | s | ref | PRIMARY | 1 | NULL |
| 1 | SIMPLE | d | ref | idx_category | 1 | NULL |
3.2 查询重写优化
对于复杂的统计查询,我们进行了以下改造:
原始查询:
sql复制SELECT user_id, COUNT(*) as order_count, SUM(amount) as total_amount
FROM orders
WHERE create_time BETWEEN '2023-07-01' AND '2023-07-31'
GROUP BY user_id
HAVING COUNT(*) > 5;
优化方案1:使用覆盖索引
sql复制ALTER TABLE orders
ADD INDEX idx_user_create_amount (user_id, create_time, amount);
SELECT user_id, COUNT(*) as order_count, SUM(amount) as total_amount
FROM orders USE INDEX (idx_user_create_amount)
WHERE create_time BETWEEN '2023-07-01' AND '2023-07-31'
GROUP BY user_id
HAVING COUNT(*) > 5;
优化方案2:分阶段处理(对于大数据量更有效)
sql复制-- 第一阶段:筛选符合条件的user_id
CREATE TEMPORARY TABLE temp_users AS
SELECT user_id
FROM orders
WHERE create_time BETWEEN '2023-07-01' AND '2023-07-31'
GROUP BY user_id
HAVING COUNT(*) > 5;
-- 第二阶段:计算详细统计
SELECT o.user_id, COUNT(*) as order_count, SUM(amount) as total_amount
FROM orders o JOIN temp_users t ON o.user_id = t.user_id
WHERE o.create_time BETWEEN '2023-07-01' AND '2023-07-31'
GROUP BY o.user_id;
3.3 数据库参数调优
根据服务器配置调整了关键参数:
ini复制innodb_buffer_pool_size = 48G # 总内存的75%
innodb_log_file_size = 2G # 原512M
innodb_flush_method = O_DIRECT
innodb_read_io_threads = 8
innodb_write_io_threads = 8
query_cache_type = 0 # 关闭查询缓存
4. 优化效果验证
优化前后关键指标对比:
| 指标 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| 商品查询平均响应时间 | 4200ms | 320ms | 92% |
| 订单统计查询时间 | 8500ms | 1200ms | 86% |
| CPU使用率 | 85% | 45% | 47% |
| 缓冲池命中率 | 85% | 98% | 15% |
| 磁盘临时表创建次数 | 1200/s | 50/s | 96% |
5. 进阶优化技巧
5.1 读写分离架构
对于报表类查询,我们将其路由到只读副本执行:
java复制// Spring Boot配置示例
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return RoutingDataSourceBuilder.create()
.addReader("reader", "jdbc:mysql://slave1:3306/db")
.addReader("reader", "jdbc:mysql://slave2:3306/db")
.setWriter("jdbc:mysql://master:3306/db")
.build();
}
5.2 查询缓存策略
对于热点数据实现应用层缓存:
java复制// Redis缓存示例
public Product getProductWithCache(Long id) {
String key = "product:" + id;
Product product = redisTemplate.opsForValue().get(key);
if (product == null) {
product = productMapper.selectById(id);
redisTemplate.opsForValue().set(key, product, 5, TimeUnit.MINUTES);
}
return product;
}
5.3 分库分表策略
对于订单表实施按月分表:
sql复制-- 创建分表
CREATE TABLE orders_202307 (
id BIGINT PRIMARY KEY,
user_id BIGINT,
amount DECIMAL(10,2),
create_time DATETIME
) ENGINE=InnoDB;
-- 使用视图统一查询接口
CREATE VIEW orders AS
SELECT * FROM orders_202307 UNION ALL
SELECT * FROM orders_202308;
6. 常见误区与避坑指南
-
过度索引陷阱
曾遇到一个案例,某表创建了20多个索引,导致写入性能下降60%。建议:- 单表索引不超过5-6个
- 定期使用
SELECT * FROM sys.schema_unused_indexes检查未使用索引 - 组合索引字段数不超过3个
-
OR条件优化
发现很多开发喜欢写:sql复制SELECT * FROM products WHERE category = 'electronics' OR price > 1000;优化方案:
sql复制SELECT * FROM products WHERE category = 'electronics' UNION ALL SELECT * FROM products WHERE price > 1000 AND (category <> 'electronics' OR category IS NULL); -
LIMIT分页性能
常见的深分页问题:sql复制SELECT * FROM orders ORDER BY id LIMIT 100000, 20;优化方案:
sql复制SELECT * FROM orders WHERE id > (SELECT id FROM orders ORDER BY id LIMIT 100000, 1) ORDER BY id LIMIT 20; -
隐式类型转换
发现一个VARCHAR字段存储数字,但查询时用数字比较:sql复制SELECT * FROM products WHERE code = 123; -- code是VARCHAR类型这会导致索引失效,应该保持类型一致:
sql复制SELECT * FROM products WHERE code = '123';
7. 监控与持续优化
建立完善的监控体系:
-
部署Prometheus + Grafana监控:
- 关键指标:QPS、TPS、慢查询数、连接数、缓冲池命中率
- 设置告警阈值(如慢查询>1s的超过50条/分钟)
-
定期执行
pt-index-usage分析索引使用情况 -
使用
pt-query-digest分析慢查询日志:bash复制pt-query-digest /var/lib/mysql/slow.log --limit=10 -
每月执行一次
ANALYZE TABLE更新统计信息
通过这个案例,我们总结出数据库性能优化的黄金法则:监控先行、索引为本、查询为要、参数为辅。每个优化方案实施后都要进行充分测试,避免引发新的性能问题。
