一、高并发查询延迟的典型症状
早上九点打卡后,电商平台的商品搜索突然变慢,客服电话被打爆。技术团队查看监控面板时,发现数据库CPU使用率飙升到90%,大量查询堆积在等待队列。这种场景下,典型的症状包括:
- 响应时间曲线出现明显毛刺
- 数据库连接数接近配置上限
- 慢查询日志开始出现大量记录
举个具体例子,当秒杀活动开始时,商品库存查询的RT(响应时间)从平时的20ms暴涨到800ms。通过PolarDB控制台的性能洞察功能,我们发现瓶颈出现在如下SQL:
-- 问题SQL:缺少索引的全表扫描
SELECT * FROM inventory
WHERE product_id IN ('SKU001','SKU002','SKU003') -- 热门商品ID列表
AND warehouse_id = 'EAST_WH' -- 固定查询条件
ORDER BY rest_amount DESC -- 按剩余库存排序
LIMIT 100; -- 分页限制
这个查询在低并发时表现良好,但当并发请求达到500QPS时,就出现了明显的性能衰减。问题的根源在于:虽然product_id字段有索引,但组合查询条件没有合适的复合索引。
二、索引优化的艺术
解决这类问题的第一板斧永远是索引优化。针对上述场景,我们创建了如下复合索引:
-- 优化方案1:创建覆盖索引
CREATE INDEX idx_inventory_cover ON inventory
(warehouse_id, product_id, rest_amount) -- 按查询顺序建立复合索引
INCLUDE (version, modified_time); -- 包含查询需要的其他字段
这个设计有几点精妙之处:
- 把等值查询条件warehouse_id放在最左列
- 将IN列表的product_id作为第二列
- 包含排序字段rest_amount实现索引排序
- 通过INCLUDE子句避免回表
优化后的执行计划显示,查询类型从"ALL"(全表扫描)变成了"index"(索引扫描),扫描行数从50万降到了300行。但索引不是银弹,我们还需要考虑:
-- 注意事项:索引过多会影响写入性能
-- 每个INSERT/UPDATE需要维护所有相关索引
-- 建议单表索引数量控制在5个以内
三、连接池与参数调优
当并发连接突增时,连接池配置不当会成为新的瓶颈。某次大促期间,我们遇到连接等待超时的问题,调整方案如下:
-- 连接池参数优化(以Java应用为例)
spring.datasource.hikari:
maximum-pool-size: 100 # 原配置50
minimum-idle: 20 # 原配置10
connection-timeout: 3000 # 等待连接超时3秒
validation-timeout: 1000 # 连接校验超时1秒
max-lifetime: 1800000 # 连接最大存活30分钟
同时调整PolarDB实例参数:
-- PolarDB参数调整(通过控制台修改)
SET GLOBAL thread_pool_size = 64; -- 线程池大小
SET GLOBAL table_open_cache = 4000; -- 表缓存数量
SET GLOBAL innodb_io_capacity = 2000; -- IO吞吐能力
特别注意:连接池不是越大越好。当连接数超过数据库处理能力时,反而会导致上下文切换开销增加。我们通过以下公式计算合理值:
建议最大连接数 = (CPU核心数 * 2) + 有效磁盘数
四、读写分离与缓存策略
对于读多写少的场景,PolarDB的读写分离功能能显著提升吞吐量。某内容平台实施读写分离后,查询性能提升3倍:
// Spring Boot配置读写分离(示例代码)
@Configuration
public class DataSourceConfig {
@Bean
@Primary
public DataSource routingDataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource());
targetDataSources.put("slave1", slave1DataSource());
RoutingDataSource routingDataSource = new RoutingDataSource();
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(masterDataSource());
return routingDataSource;
}
// 通过注解实现读写分离
@Transactional(readOnly = true)
public List<Content> getHotContents() {
// 该方法会自动路由到只读实例
}
}
配合多级缓存策略效果更佳:
- 热点数据使用Redis缓存
- 查询结果缓存5秒防雪崩
- 本地缓存兜底
// 多级缓存实现示例
public Content getContent(String id) {
// 第一层:本地缓存
Content content = localCache.get(id);
if (content != null) return content;
// 第二层:Redis缓存
content = redisTemplate.opsForValue().get("content:" + id);
if (content != null) {
localCache.put(id, content);
return content;
}
// 第三层:数据库查询
content = contentMapper.selectById(id);
if (content != null) {
redisTemplate.opsForValue().set("content:"+id, content, 5, TimeUnit.SECONDS);
}
return content;
}
五、SQL改写与分页优化
分页查询是大并发场景的另一个杀手。某次优化中,我们发现以下分页查询在翻到100页后性能急剧下降:
-- 原始低效分页查询
SELECT * FROM orders
WHERE user_id = 'U1001'
ORDER BY create_time DESC
LIMIT 10 OFFSET 1000; -- 越往后越慢
优化方案是使用"游标分页":
-- 优化后的游标分页(假设上次最后一条记录的create_time是2023-06-01 12:00:00)
SELECT * FROM orders
WHERE user_id = 'U1001'
AND create_time < '2023-06-01 12:00:00' -- 游标条件
ORDER BY create_time DESC
LIMIT 10; -- 固定每页大小
对于报表类查询,我们采用物化视图预计算:
-- 创建定时刷新的物化视图
CREATE MATERIALIZED VIEW mv_sales_daily
REFRESH COMPLETE START WITH SYSDATE NEXT SYSDATE+1 -- 每天刷新
AS
SELECT product_id, SUM(amount) as total_sales
FROM orders
WHERE order_date >= TRUNC(SYSDATE)
GROUP BY product_id;
六、总结与最佳实践
经过多次实战优化,我们总结出PolarDB高并发优化的"四要四不要"原则:
要:
- 要给高频查询创建合适的覆盖索引
- 要合理设置连接池和数据库参数
- 要善用读写分离和缓存机制
- 要对大数据集查询做分页优化
不要:
- 不要盲目增加索引数量
- 不要使用全表扫描的SQL
- 不要过度依赖数据库层缓存
- 不要在事务中执行耗时操作
最后记住,性能优化是持续的过程。建议每月做一次完整的SQL审计,每季度进行一次压力测试,这样才能在流量洪峰来临时稳如泰山。
评论