活动系统运行了几个月后,最先被抱怨的不是下单,而是后台订单列表。
运营同学按时间筛选订单,页面从 200ms 变成 3 秒,再后来变成 8 秒。开发第一反应通常是:SQL 慢了,优化一下。
但数据库问题很少只有“SQL 慢”这么简单。
订单表变大了,索引可能不合适;分页越来越深,数据库要跳过大量记录;事务越来越长,写入开始互相等待;报表查询混在在线库里,和正常业务抢资源。
数据库高并发问题,本质上是越来越多压力落到了一个不该无限承压的地方。
这种问题在现场排查时,经常会有一个误会。
业务同学看到页面慢,会觉得“是不是服务器不行了”。开发同学第一反应是“SQL 要优化”。DBA 看完监控,可能会说连接数、锁等待、临时表都在涨。
每个人说的都对,但如果只盯着其中一个点,很容易修完一处,另一处继续冒烟。
我现在看数据库高并发问题,会先把链路画出来:请求从哪里来,哪些查询被放大了,哪些写入不能被影响,哪些后台任务可以让路。数据库慢,很多时候只是最后被看见的地方。
先看压力从哪里来,再决定是优化 SQL、调整索引、拆查询,还是把报表和历史数据请出去。
一、慢 SQL 是信号,不是全部原因
慢 SQL 最容易被发现,因为它会出现在慢查询日志里。
但慢 SQL 只是信号。背后可能是没有合适索引、扫描行数太多、返回字段太多、排序和分组成本高、关联表过多,或者统计信息不准确。
我的习惯是先看执行计划,而不是马上改 SQL。
执行计划能告诉你这条 SQL 有没有走索引、扫描了多少行、有没有 filesort、有没有临时表。没有这个信息,所谓优化很容易变成猜。
还有一点很重要:不要只看平均耗时。平均值很温柔,P95、P99 才更接近用户真实感受。
二、索引设计看的是访问路径
索引不是越多越好。
索引能减少查询扫描范围,但也会增加写入成本,占用存储空间,还会让优化器在多索引之间做选择。
订单列表这种场景,常见查询条件可能是用户、状态、时间、订单号。索引应该围绕这些高频访问路径设计,而不是看到哪个字段常用就给哪个字段建一个单列索引。
常见问题包括:
- 查询条件没有命中索引。
- 对索引列做函数或隐式类型转换。
- 联合索引顺序不合理。
- 模糊查询前缀是
%。 - 低区分度字段单独建索引。
索引设计不是给字段贴标签,而是看业务到底怎么查。
三、深分页和大表会慢慢把成本推高
很多后台列表一开始都支持跳页。
select * from orders order by id limit 1000000, 20;这类查询看起来只取 20 条,实际数据库要先跳过前面 100 万条。页码越深,成本越高。
更稳的方式是基于游标或上一页最后一个 ID 继续查:
select * from orders where id > ? order by id limit 20;这也提醒我们:有些性能问题不是数据库一个人的问题,产品交互也要参与设计。如果业务并不真的需要跳到第 50000 页,就不要为了一个完整分页控件把数据库拖下水。
四、写入慢,经常和锁、事务有关
读慢影响体验,写慢影响吞吐。
高并发写入下,锁冲突会变明显。多个事务修改同一批数据,或者一个事务持有锁太久,其他请求就只能等待。
大事务尤其危险。它可能包含大量更新、远程调用、复杂计算,甚至等待外部系统响应。事务越长,锁持有时间越长,回滚成本越高。
我的原则是:事务里只放必须保证一致性的数据库操作。
发送 MQ、调用第三方接口、生成报表、写日志,这些都不应该随手塞进主事务里。主事务越轻,系统越容易稳住。
五、不要一上来就分库分表
当单库读压力大,可以考虑读写分离。但它会带来主从延迟,所以强一致读不能随便走从库。
当单表数据太大、单库写入压力太高,可以考虑分库分表。但这一步成本很高:分片键选择、跨分片查询、全局 ID、数据迁移、事务处理、运维复杂度都会上来。
冷热数据分离往往更温和。
比如最近 3 个月订单放在线表,历史订单归档到历史表或归档库。这样既减少在线表压力,也保留历史查询能力。
我一般按这个顺序治理:
SQL 和索引优化
-> 数据模型调整
-> 缓存和读写分离
-> 冷热分离
-> 分库分表越靠后的方案,改造成本越高,后遗症也越多。
数据库治理最怕一步跳到重方案。很多系统先把前几步做扎实,就已经能稳很久。
执行计划、索引、分页、返回字段,先把单次查询成本降下来。
缓存、读写分离、报表库,让数据库少承担不该承担的读。
冷热分离、分库分表属于高成本动作,要等瓶颈足够明确。
六、可以带走的判断清单
- 接口慢,先看执行计划,不要凭感觉改 SQL。
- 平均耗时不够,要看 P95、P99。
- 索引围绕访问路径设计,不是围绕字段数量设计。
- 深分页要警惕,必要时改成游标分页。
- 事务里只放必须一致的操作。
- 分库分表是高成本方案,不要当成第一反应。
- 历史数据多时,先考虑冷热分离。
数据库优化最怕两个极端:一个是只改 SQL,不看系统链路;另一个是一上来就分库分表,把复杂度提前引爆。
本系列:
暂无评论