大家好,我是数据库小学妹👋 我踩过的坑,你别再踩。
有阵子交易接口开始零星超时。开发查了半天,代码没动,链路也没改,后来把我拉去看数据库。我第一反应是找慢 SQL,翻出来一条报表查询,一扫上千万行。我给它加索引、改写法,折腾一通,确实快了些。
可接口该超时还是超时。结果问题不在我盯的地方。那条报表 SQL 本身不慢,要读的量也不大,麻烦在于,它跟交易抢同一份资源。它一跑起来,IO和连接就被占住,交易只能排队。光优化一条SQL,解决不了这种资源挤占。得把它俩分开。
数据库是一口共享的锅
数据库从来都是共享资源。一个实例里,内存、IO、连接数,是所有会话一起分的。一条重查询能把这些吃光,把别人饿死。这是隔离的问题,怪不到哪段代码头上。压力大和压力集中,本来就是两个问题。隔离解决的是后者,让一件事不去踩另一件事。想明白这层,我就一道道加防线。
四道防线
最后落成四道,从入口到实例一层层往内收。
| 防线 | 挡什么 | 手段 |
|---|---|---|
| 入口层 | 异常流量、连接洪水 | 网关与连接池限流、连接数上限 |
| 语句层 | 一条跑太久的查询 | 超时参数、资源组、大查询熔断 |
| 负载层 | 报表、批处理与交易混跑 | 读写分离、只读副本专供 |
| 实例层 | 分析型负载吃交易资源 | 从库扛分析、冷数据进列存 |
语句层:给查询设一条死线
最便宜的一道防线,是给语句设超时。查询跑过一条线就直接掐,别让它拖着。
-- MySQL:只作用于只读 SELECT,超过 3 秒就中断
SET SESSION max_execution_time = 3000;
-- PostgreSQL:整条语句的执行上限
SET statement_timeout = '3s';
要分得更细,可以用资源组。MySQL 8.0 有 resource groups,把线程按业务分组,给不同组配不同的 CPU 权重和线程优先级。让报表那组天然排在交易后面,抢不过。
-- MySQL 8.0 建一个给报表用的低优先级资源组
CREATE RESOURCE GROUP rg_report
TYPE = USER
VCPU = 4
THREAD_PRIORITY = 19;
资源组也有边界。VCPU 给的是一条上限线,不保证独占,别的组闲下来你才能多用。THREAD_PRIORITY 管的是调度顺序,数字越小越优先,我把报表组排到最低。真到 CPU 打满,它顶多是让报表线程多等几拍。重活压过来,光靠资源组顶不住,得靠超时加限流一起管。
超时参数还有盲区。max_execution_time 只管只读 SELECT,写语句它管不着。它掐的也只是语句真正执行的那段,卡在锁等待上不算。前面的事务攥着锁不放,后面的语句排再久也不触发它。这种等锁,MySQL 得另配 innodb_lock_wait_timeout。PG 那边 statement_timeout 从语句开始计时、把等锁也算进去,抢锁还能单独用 lock_timeout 限。想连写一起兜,最终还是得靠连接层或中间件。
中间件层:把报表和交易分到不同后端
再往里,我会用中间件做路由。ProxySQL 的查询规则可以按语句指纹,把报表类流量导到专门的后端组,交易类走主库。两边各有各的连接池,互不抢占。
可中间件怎么知道哪条是报表?靠它自己猜 SQL 长得像什么,容易漏,也容易误伤。我更愿意让应用在报表查询上挂个注释标签,中间件的规则直接认这个标签,比认 SQL 长相稳当。
这也是读写分离真正的用法。读别一股脑丢给从库,得先把"谁可以容忍延迟、谁必须实时"分清楚。报表党容忍得了主从延迟,交易不行。这条线划对了,隔离就成了一大半。
还有个坑是刚写完就读。主库写、从库读,主从有延迟,读到的可能还是旧数据,业务上就是"下完单查不到订单"。这种读必须强制回主库,通常按事务边界来切,事务里的读一律走主库,别交给从库。
熔断与降级别做成一断到底
防线再往里,就是熔断。某个指标破了线,先把这类负载挡掉,给个降级结果,别让整个库陪着一起慢。
先把三个词分清楚,很多人混着用。限流是在入口按量放行,还没到库就把多余的挡掉。熔断是发现不对劲就紧急刹车,指标破线立刻停掉这类调用。降级是刹车之后给个替补,让调用方拿到一个能用的结果,别干等,也别直接报错。三者各管一段,得配着用。
熔断最容易踩的坑是只断不合。一断到底,正常流量也被挡在外面。我做的时候会留半开状态,隔一段时间放一小股流量过去试,通了再逐步放量。触发条件也要提前想清楚,看连接数、看慢查询占比、还是看 P99,每个指标对应挡哪一类负载,都得先定下来。
阈值别只靠感觉定。我用滑动窗口统计,窗口里慢查询占比过了线,同时请求数够一个最小样本量,才判它触线,样本太少容易误伤。半开也别一步到位:先放一小股过去,稳住了再翻倍,一次全放开很可能二次雪崩。
实例层:把分析搬离主库
四道里最里面一层,是把分析型负载整个挪走,不让它碰交易实例。报表、统计、跑批这类活,数据量大、跑得久,搁主库上就是天天跟交易抢资源。我会把它们放到只读副本,再往上走是列存,分析走列式存储,扫全表也快,主库这边基本不受影响。
这层有代价:数据得多存一份,同步还有延迟,得接受"报表看到的是几分钟前的数"。可只要业务分得清哪些要实时、哪些能等,这层最彻底,它从根上把两类负载隔开了。
一张负载分级表
分到最后,我会落一张表,把每种负载的通道和优先级写死。评审的时候对着这张表吵,比空谈"要隔离"有用得多。
| 负载类型 | 走哪个后端 | 超时 | 优先级 |
|---|---|---|---|
| 交易 | 主库 | 短 | 最高 |
| 报表 | 只读副本 | 中 | 低 |
| 批处理 | 独立从库 | 长 | 最低 |
| 运维查询 | 专用只读库 | 中 | 低 |
怎么证明隔离真的生效
防线搭完不算完,得能证明它管用。我盯几个数:交易侧的 P99 有没有被大查询拖高,连接等待队列有没有堆积,慢查询占比有没有降下来。数据库的等待事件最直白,锁等待、IO 等待一多,就说明还有负载在互相踩。
光看日常还不够,我定期做演练:压一条扫全表的大查询进来,盯着交易 P99。曲线稳,说明隔离扛住了;跟着抖,防线就有漏。演练比任何配置评审都说明问题。
避坑清单
别把从库当廉价备胎。有人觉得从库只读,随便配个小规格就行,结果报表全压上去,从库先被打垮,主从延迟越拖越长,最后连读都读不动。从库的规格得按它要扛的读量来定,别拿主库砍一刀就顶上。
限流阈值别拍脑袋。设得太紧,正常尖峰也被当成异常挡掉。我一般拿历史峰值的两倍起步,再按实际调,而不是直接写一个整数。
最后一条是我之前搞错过的。我一开始把隔离做在应用层,每个服务自己判断要不要限流。结果是各判各的,谁都不肯先让,等于没隔离。后来收到数据库侧和中间件侧,由统一入口来管,才真正生效。隔离这件事,得有个统一的地方说了算。
写在最后
治理高并发,光把每条 SQL 调快没用。得先保证重要的事不被不重要的事耽误。优化能让所有人快一点,隔离决定的是紧张时刻谁先、谁后。资源就那么多,一到峰值总有人要等,好的隔离就是把队伍提前排明白。这事平时看不出差别,真出了故障,谁的交易还站得住,一眼就清楚。它的性价比,扛过故障的人心里最有数。
你们库里的报表和交易,现在是分道的,还是一起跑的?评论区聊聊。
我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋