秒杀场景对 MySQL 来说长期以来都像一场蓄意谋杀。平时跑得好好的实例一到抢购瞬间几千几万个请求同时砸向同一行库存TPS 立刻断崖锁等待拉满连接数打爆搞不好直接把业务拖死。市面上常规做法无非是上缓存、搞队列、限流削峰把流量挡在数据库前面。但流量真的进了 MySQL 之后内核怎么扛住这波并发才是真正见功夫的地方。这次聊的就是小红书 MySQL 内核秒杀能力重磅再升级这个事背后MySQL 在以什么思路对抗秒杀场景以及我们能直接抄走的优化手段。这篇文章不讲空话核心聊三块秒杀场景下 MySQL 到底卡在哪些环节、内核层优化大概会动哪些地方、以及作为 DBA 或后端开发有哪些参数和 SQL 写法是你现在就可以拿去用的。无论你是被秒杀压测折磨的开发者还是正在调优 MySQL 的 DBA这篇应该都能给你点参考。1. 秒杀场景下 MySQL 到底卡在哪里想看懂内核优化先得搞清楚瓶颈在哪。MySQL 在秒杀场景里频繁跪下不是单一原因而是好几个环节同时被顶到极限。1.1 热点行锁库存行的单车道秒杀业务最典型的操作就是对同一个 SKU 的库存字段做扣减。假设一个商品有 1000 件库存50 万人同时抢最终落到数据库的扣减请求可能也有几万甚至几十万。InnoDB 的行锁机制决定了同一时刻只能有一个事务持有某一行记录的锁。也就是说UPDATE sku SET stock stock - 1 WHERE id 123 这条语句无论多少并发过来真正执行时都是一条一条排着队过的。你可以把它想象成一座独木桥只容一个人通过桥头挤了十万人。每秒能过多少人取决于一个人过桥的时间有多快。MySQL 这边一个行锁持有时间通常也就几微秒到几十微秒但问题是锁等待、唤醒、重新竞争这个过程开销远比想象中大。当一个事务释放行锁时其他等待在这个锁上的事务会被唤醒然后去抢锁。抢不到的继续睡过去等下一次唤醒。这种反复的睡眠与唤醒在 CPU 层面就是频繁的上下文切换和 mutex 竞争吞吐量自然上不去。这里的核心矛盾在于热点行锁把原本可以并行的事务强制串行化了。内核优化再怎么改也无法完全消除这种串行但可以缩短锁持有时间、降低锁等待唤醒的开销让过桥的效率从每次 50 微秒降到 20 微秒整体 TPS 就能翻倍不止。1.2 日志落盘redo 与 binlog 的双重拖累行锁只是第一关。就算事务执行得飞快提交阶段还有一个隐形瓶颈日志刷盘。MySQL 默认情况下innodb_flush_log_at_trx_commit1意味着每次事务提交redo log 必须 fsync 到磁盘才算完事。同时如果开启了 binlogsync_binlog1 时 binlog 也要同步落盘。也就是说一个事务提交至少要等两次 fsync 完成。磁盘的 fsync 耗时虽然只有几毫秒但在高并发下这就成了串行化的另一处瓶颈。现代 SSD 的 fsync 性能已经很强但禁不起量变引起质变。粗略算一笔账假设单次 fsync 平均延迟 1 毫秒理论上每秒最多做 1000 次串行 fsync。算上组提交的批量合并效果实际每秒能支持的提交数会高一些但想要支撑每秒几万的事务提交纯靠每个事务都立即刷一次盘这条路子是走不通的。所以秒杀场景里一个特别重要的内核优化方向就是组提交group commit的改进。把短时间内的多个事务提交合并到一次 fsync 里做极大地摊薄了刷盘成本。这个机制 MySQL 本身就有但内部实现里仍有不少可以优化的地方尤其是 binlog 与 InnoDB 引擎层 redo 的提交协调顺序。1.3 连接风暴与线程争抢第三条命门是连接。秒杀开始时应用层的连接池会瞬间把数据库连接数拉满新的请求还在源源不断地创建连接。MySQL 是线程模型一个连接对应一个线程。连接一多线程切换开销、内存占用、mutex 争抢全部上来。举个例子假设单条秒杀 SQL 执行只需要 0.1 毫秒但如果线程调度不过来一个请求从进入到执行完成可能实际花了 20 毫秒其中大部分时间是在排队等 CPU。这种情况下数据库的 CPU 可能根本没有跑满但吞吐已经上不去了因为时间都耗在线程调度和锁等待上。这就是为什么每次秒杀大促前DBA 们总是反复强调连接数一定要控制住。与其让一万个连接抢 CPU不如固定两百个连接让每个连接满负荷干活。2. 这次内核升级的优化方向拆解既然瓶颈清楚接下来看内核优化可以怎么对症下药。注意不同团队对 MySQL 内核的优化思路差异很大但方向上通常都会围绕锁、日志、线程调度、内存访问这四个维度展开。2.1 为什么选择改内核而不是堆机器先说一个很多人会问的问题秒杀扛不住加一台机器不就行了或者干脆把库存操作全部丢到 Redis 里MySQL 只做异步落库这个思路没毛病也确实是大厂的常见做法。但它的前提是业务允许库存数据先放缓存最终再同步回数据库。很多场景其实并不允许这么做。比如电商平台的支付环节库存扣减必须保证强一致Redis 里的扣减一旦和数据库对不上账就算不平。还有一些内部系统逻辑就是直接读写 MySQL重构或引入新存储的成本非常高。改内核的思路则不一样在不动业务架构的前提下把 MySQL 自身的处理能力往上抬。秒杀流量进来照样是 MySQL 扛但能扛的峰值更高、延迟更稳。这就是内核升级的核心价值让数据库本身更抗造。2.2 锁机制优化从全局排队走向精细化调度这次升级中最受关注的是对锁机制和热行等待的优化。InnoDB 的行锁实现本质上是一套基于 hash 表的锁结构每个锁都有对应的等待队列。当一个事务持有锁并提交时它需要去遍历等待队列逐个唤醒等待的事务。听起来很简单但在高并发下这个操作成了热点。多个 CPU 核心同时操作同一个锁对象的等待队列时就需要加 mutex 保护。mutex 竞争激烈时这个锁对象本身反而成了新的瓶颈。内核优化的方向通常是两条一是优化 mutex 的实现比如从普通的 pthread mutex 改成自旋锁、或者采用更高效的排队锁算法减少线程睡眠唤醒带来的开销二是优化锁唤醒策略比如带优先级地唤醒、批量唤醒尽量避免唤醒风暴——也就是所有等待线程同时醒过来抢一个锁结果只有一个抢到其他人又睡过去。说直白点就是把原来大家一起抢、抢不到再睡的粗暴方式改成更精细的排队调度让锁的交接过程更加丝滑。压测数据往往在这里会有非常明显的改善同样的并发下锁等待次数大幅下降TPS 明显提升。2.3 日志组提交与刷盘调度重构redo log 和 binlog 的组提交优化是内核升级另一个高收益点。MySQL 8.0 的组提交已经做得很成熟但细看实现还是有大量可调的空间。比如 binlog_group_commit_sync_delay 这个参数含义是让 binlog 提交后延迟多少微秒再 fsync这样可以把更多事务凑在一起刷盘。这个参数设得太大事务延迟会增加设得太小组提交效果不明显。内核升级后往往可以动态调整这个参数甚至根据当前负载自动调整合并策略不再需要 DBA 手动试参数。另一个方向是 redo log 的刷盘优化。刷盘不一定非要每次都把整个 buffer 写到磁盘有时候只需要写一部分。内核层面的优化点包括批量写、按块对齐、减少不必要的 write 系统调用、以及让 redo 写入更适配 SSD 的 IO 特性。这些优化叠加起来效果非常可观原本 commit 延迟 5 毫秒优化后能压到 1 毫秒以内。2.4 NUMA 与线程模型隐藏的内存瓶颈这个点很多人容易忽略。现在的服务器基本都是多路 CPU 的 NUMA 架构内存访问是有远近之分的。如果 MySQL 的线程频繁跨 NUMA 节点访问内存延迟会高出一截尤其在高并发场景下这种隐藏开销会被放大。内核升级里常见的做法是优化 MySQL 的线程绑核与内存分配策略。让活跃的 worker 线程尽量绑定在同一颗 CPU 上buffer pool 的内存也优先从本 NUMA 节点分配。听起来只是小细节但在每秒几万事务的压力下积少成多P99 延迟的改善非常明显。另外大页内存HugePage也是经典优化项。MySQL 默认使用 4KB 的小页buffer pool 几十 GB 的情况下TLB 缓存的命中率很低。换成 2MB 甚至 1GB 的大页后TLB miss 显著减少CPU 访问内存的效率更高。虽然这不是纯 MySQL 内核层面的改动但需要内核和数据库两个层面配合算得上系统性优化。3. 关键参数与配置实操可以直接抄作业内核优化是底层工程但落到日常工作中我们绝大多数人能做的是把参数和 SQL 调到最优。下面这套配置思路适用于典型的秒杀类业务可以直接拿去做压测基线。3.1 秒杀业务 MySQL 参数基线先说一个原则不要照搬网上任何一套参数务必结合自己的压测结果做微调。下面的数值是我在类似场景下踩坑后觉得比较稳的起点。参数推荐值说明max_connections500~1000秒杀场景宁可拒绝连接也不能让数据库被连接拖垮innodb_buffer_pool_size物理内存的 60%~75%buffer pool 大一些减少磁盘 IOinnodb_buffer_pool_instances8~16降低 buffer pool 内部 mutex 竞争innodb_flush_log_at_trx_commit1默认强一致场景不要降级报错数据会丢sync_binlog1默认要做组提交优化而不是盲目改成 0binlog_group_commit_sync_delay50~200 微秒根据延迟要求调整越大吞吐越高binlog_group_commit_sync_no_delay_count10~30 个事务达到事务数阈值就触发刷盘innodb_thread_concurrency64~128限制同时执行事务的线程数避免过多线程切换lock_wait_timeout5 秒秒杀场景锁等待要快速失败别干等innodb_deadlock_detectON默认秒杀下死锁检测可能有开销但关闭风险很高重点说一下 innodb_flush_log_at_trx_commit。很多性能优化文章会推荐把它改成 2这相当于把 redo 落盘从每次提交改为每秒一次。系统崩溃时最多丢 1 秒的事务数据。秒杀类业务如果对账严格千万别改。要优化宁愿走组提交合并也不要牺牲这个保证。3.2 SQL 层防超卖的正确姿势配置调好了SQL 写不好照样白搭。秒杀防超卖的标准姿势是这一条UPDATE sku_stock SET stock stock - 1 WHERE sku_id 123 AND stock 0;这条语句的巧妙之处在于stock 0 条件让扣减操作在无库存时直接失败通过 affected rows 是否为 1 判断是否扣减成功。因为 UPDATE 会加行锁整个检查库存 扣减是原子操作不会出现两个事务同时看到 stock1 然后都扣成负数的情况。反面案例是两步走-- 错误示范 SELECT stock FROM sku_stock WHERE sku_id 123; -- 假设拿到 stock 5然后在应用层判断大于 0 UPDATE sku_stock SET stock stock - 1 WHERE sku_id 123;这个写法在并发下全是坑两个请求同时读到 stock5都通过检查都去更新结果库存变成 3但明明卖了 2 件。即便你加上 SELECT ... FOR UPDATE也只是把锁持有时间变长热点行锁的压力更大并发能力反而下降。压测时你会发现UPDATE ... WHERE stock 0 这种写法配合事务里尽量只做这一件事锁持有时间最短吞吐量远高于先查后改的方式。3.3 内核升级后的验证方法改了内核、调了参数怎么知道真的变强了不能只看业务感受要看指标。压测时的核心关注项有四个TPS / QPS整体吞吐是否提升P99 / P999 延迟峰值延迟是否稳定有没有毛刺Innodb_row_lock_current_waits当前行锁等待的事务数数值越低越好Innodb_row_lock_time_avg平均行锁等待时间反映锁竞争程度可以直接用这条 SQL 看锁相关状态SHOW GLOBAL STATUS LIKE Innodb_row_lock%; SHOW GLOBAL STATUS LIKE Innodb_data_fsyncs; SHOW GLOBAL STATUS LIKE Threads_running;重点盯住 Threads_running。如果这个值长期大于 CPU 核数的好几倍说明线程调度已经过载光是参数调整恐怕不够还得从连接池和限流端想办法。4. 常见问题与排查技巧实录内核升级以后日常踩坑的节奏也会变。这里分享几个我在实际运维中遇到的问题和排查思路特别是升级后容易出现的意外。4.1 秒杀期间死锁变多了这算是内核优化后最容易碰到的怪问题。原本锁粒度粗的时候大家排队排得明明白白不会死锁优化后事务执行变快反而暴露出应用层的锁顺序问题。比如两条 SQL 都去更新 sku_stock但其中一个事务是先更新 sku 表、再更新库存表另一个事务反着来两个事务互相持有对方需要的锁死锁就来了。排查方法很简单出现死锁后立刻执行。SHOW ENGINE INNODB STATUS;看 LATEST DETECTED DEADLOCK 部分里面会明确记录两个事务分别持有和等待什么锁。常规解法是统一所有事务里的对象更新顺序比如都先更新 sku 表再更新库存表或者都只更新库存表。秒杀场景最简单的方式就是一个事务里只执行一条 UPDATE 库存语句不动其他表死锁概率会大幅下降。4.2 主从延迟突然变高内核优化提升了主库的吞吐但也可能让从库跟不上节奏。道理很朴素主库每秒能提交一万个事务从库的 SQL 线程只有一个应用日志的速度有限延迟自然拉大。处理思路有两个层面。首先是确认从库的并行复制参数有没有打开。MySQL 8.0 里以 slave_parallel_workers 为核心默认值并不高。压测或大促期间建议调大SET GLOBAL slave_parallel_workers 16; SET GLOBAL slave_parallel_type LOGICAL_CLOCK;改完注意观察 Seconds_Behind_Master它降到接近 0延迟才算压住。更复杂的场景是binlog 提交顺序被内核优化改动后从库的并行复制策略可能需要重新适配。如果延迟依然很夸张就得考虑对账方案秒杀结束后以主库数据为准做一次库存核对。4.3 连接被打满服务直接无响应连接数打满的原因不一定是 QPS 太高很多时候是应用连接池的获取连接超时时间设置过长。数据库侧已经拒绝新连接了应用还在那里傻傻地等。我的排查习惯是先看 processlistSHOW FULL PROCESSLIST;重点区分两种状态大量连接卡在 Query 等待还是大量连接处于 Sleep 状态。如果是 Query 等待说明 SQL 执行慢锁竞争或 IO 有瓶颈如果是 Sleep说明连接被应用借走但没有释放可能是连接池泄漏或业务代码里的事务没及时提交。秒杀场景比较有效的兜底手段是在数据库前面加一层连接管理机制比如让 max_connections 适当收紧并用 Haproxy 或应用层连接池做排队。宁可让少量请求快速失败也不能让所有请求堵在数据库门口。4.4 内核升级的副作用旧优化手段可能失效这是最容易被忽视的一点。很多 DBA 习惯用小技巧来缓解秒杀压力比如在秒杀前重启数据库清理碎片、比如故意在应用层 sleep(1) 限流、比如把库存放到 Redis 预扣然后再慢慢同步。内核升级后这些手段的效果可能全部变样。比如内核优化把锁等待时间压缩到极低那么应用层 sleep(1) 反而成了最大的性能瓶颈白白把 TPS 拉低一大截。再比如以前靠死锁重试逻辑来兜底的业务升级后死锁频率变了重试参数也需要重新评估。所以每次内核升级不能只盯着数据库本身的指标还要把应用层的旧有行为重新过一遍限流策略、连接池配置、事务边界、超时设置都要配合新的数据库特性做新一轮压测调整。5. 实操心得与一个小技巧说说我个人感受。单靠改参数解决秒杀问题天花板很明显单靠改架构把压力全卸给缓存复杂度又太高。真正稳妥的路径恰恰是把内核层的能力压榨出来再配合应用侧的规范化写法两边一起使劲。这轮内核升级里给我印象最深的不是某个参数翻了倍而是行锁竞争曲线变得平滑了。以前秒杀开始的那 10 秒Innodb_row_lock_current_waits 能冲到几十甚至上百升级后压在个位数这个变化直接决定了 P99 延迟不会出现夸张的尖刺。最后分享一个小技巧。压测秒杀场景时别只盯着平均延迟多看看 P999。平均延迟可以很漂亮但只要出现几个 300 毫秒以上的请求用户的体感就是卡了一下。我自己会在压测脚本里同时记录 P999 和 Innodb_row_lock_current_waits 的峰值只要这两个指标在秒杀最高峰没有明显抬升基本就能判断这轮优化到位了。MySQL 内核优化这事没有一招制敌的银弹但每往前走一步数据库能扛的极限就高一分。如果你也在折腾秒杀场景不妨先把自己手上的参数和 SQL 写法摆到台面上检查一遍很多时候问题出在应用层而不是内核本身。 SEO 优化官网定制响应式建站教育培训建站