一、高并发查询延迟的典型症状

早上九点打卡后,电商平台的商品搜索突然变慢,客服电话被打爆。技术团队查看监控面板时,发现数据库CPU使用率飙升到90%,大量查询堆积在等待队列。这种场景下,典型的症状包括:

  1. 响应时间曲线出现明显毛刺
  2. 数据库连接数接近配置上限
  3. 慢查询日志开始出现大量记录

举个具体例子,当秒杀活动开始时,商品库存查询的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);       -- 包含查询需要的其他字段

这个设计有几点精妙之处:

  1. 把等值查询条件warehouse_id放在最左列
  2. 将IN列表的product_id作为第二列
  3. 包含排序字段rest_amount实现索引排序
  4. 通过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() {
        // 该方法会自动路由到只读实例
    }
}

配合多级缓存策略效果更佳:

  1. 热点数据使用Redis缓存
  2. 查询结果缓存5秒防雪崩
  3. 本地缓存兜底
// 多级缓存实现示例
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高并发优化的"四要四不要"原则:

要:

  1. 要给高频查询创建合适的覆盖索引
  2. 要合理设置连接池和数据库参数
  3. 要善用读写分离和缓存机制
  4. 要对大数据集查询做分页优化

不要:

  1. 不要盲目增加索引数量
  2. 不要使用全表扫描的SQL
  3. 不要过度依赖数据库层缓存
  4. 不要在事务中执行耗时操作

最后记住,性能优化是持续的过程。建议每月做一次完整的SQL审计,每季度进行一次压力测试,这样才能在流量洪峰来临时稳如泰山。