最近在排查一个线上数据库性能问题时,遇到了一个很有意思的现象:原本运行正常的业务代码,在新增了一个写操作后,突然出现了慢查询。具体表现为:
java复制@Transactional(rollbackFor = Exception.class)
public void serviceMethod() {
// 1.查询业务 - 正常
select();
// 2.新增的写操作
insertOrUpdate();
// 3.查询其他表业务 - 出现慢SQL
selectAgain();
}
我们的数据库使用的是阿里云PolarDB,开启了读写分离功能,并且配置了列存节点。在业务代码2(写操作)上线前,代码1和3都能正常运行且没有慢查询;但在加入写操作后,代码3开始出现明显的性能下降。
出现问题的SQL语句如下:
sql复制SELECT * FROM table WHERE create_time < '2026-01-20' AND user_id = xxx LIMIT 1;
这个查询看起来很简单,但实际执行时却出现了性能问题。特别值得注意的是LIMIT 1这个条件,后面我们会发现它在这个问题中扮演了关键角色。
通过EXPLAIN分析发现,这个SQL被路由到了列存节点上执行,测试环境中的查询时间约为30ms,但无法完全复现线上更严重的慢查询现象。
提示:测试环境与生产环境的数据量、负载情况可能存在差异,这解释了为什么无法完全复现线上问题。
基于经验,我们临时将查询修改为:
sql复制SELECT COUNT(*) FROM table WHERE create_time < '2026-01-20' AND user_id = xxx;
这种形式的查询会被强制路由到列存节点执行,暂时解决了性能问题。但这只是一个临时方案,我们需要深入理解背后的原因。
阿里云PolarDB采用了一写多读的架构,主要包含以下节点类型:
这种架构下,如何正确路由查询请求对性能至关重要。
在我们的案例中,观察到了以下路由行为:
java复制@Transactional(rollbackFor = Exception.class)
public void serviceMethod() {
// 1. 初始查询 - 正常路由
select * from t_a where ...
// 2. 写操作 - 必须路由到主库
insert into t_b ...
// 3. 后续查询 - 可能路由到主库或列存节点
select t_c where .... limit 1
// 4. 聚合查询 - 通常路由到列存节点
select count(*) t_c where ....
}
PolarDB的"事务拆分"功能会将一个事务中的不同操作路由到不同节点:
列存节点特别适合处理分析型查询,但并非所有查询都适合。PolarDB会根据查询特征自动判断是否应该路由到列存节点:
在我们的案例中,LIMIT 1导致查询被认为是一个"点查",不适合列存节点处理,因此路由决策出现了问题。
结合上述机制,我们可以总结问题根源:
LIMIT 1使优化器认为这是一个点查,不适合列存节点基于以上分析,我们制定了以下解决方案:
sql复制/*+TDDL:cmd_extra(ENABLE_HLL=true)*/
SELECT * FROM table WHERE create_time < '2026-01-20' AND user_id = xxx LIMIT 1;
sql复制-- 改为聚合查询
SELECT COUNT(*) FROM table WHERE create_time < '2026-01-20' AND user_id = xxx;
-- 或者去掉LIMIT 1(如果业务允许)
SELECT * FROM table WHERE create_time < '2026-01-20' AND user_id = xxx;
java复制// 将写操作和读操作拆分到不同事务中
public void writeOperation() {
// 写操作
insertOrUpdate();
}
@Transactional(readOnly = true)
public void readOperation() {
// 读操作
selectAgain();
}
读写分离配置:
事务设计原则:
查询优化建议:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 查询突然变慢 | 路由到不合适的节点 | 检查执行计划,确认路由目标 |
| 事务中读操作性能差 | 写操作导致后续读操作路由到主节点 | 拆分事务或使用只读事务 |
| 列存节点查询性能差 | 查询不适合列存处理 | 重写查询或使用HINT |
| 路由不一致 | PolarDB优化器决策变化 | 使用HINT固定路由 |
PolarDB的路由决策过程可以简化为以下步骤:
列存节点最适合以下场景:
而不适合:
不同的隔离级别也会影响路由决策:
在实际应用中,需要根据业务需求选择合适的隔离级别。
sql复制SELECT a.*, b.*
FROM table_a a JOIN table_b b ON a.id = b.a_id
WHERE a.create_time > '2023-01-01'
LIMIT 100;
这种JOIN查询加LIMIT的组合也经常出现路由问题,解决方案类似:
有些业务需要在同一事务中执行分析和点查操作,这时可以考虑:
大批量的INSERT...SELECT操作也经常遇到路由问题,解决方案包括:
在实际工作中,理解数据库的路由机制对于性能优化至关重要。PolarDB作为一款云原生数据库,提供了灵活的路由策略,但也需要开发者理解其工作原理才能充分发挥性能优势。