在“双十一”“618”等大促场景中,一个看似简单的操作——扣减商品库存,往往成为压垮 MySQL 数据库的“最后一根稻草”。成千上万用户同时抢购同一爆款商品,导致对同一行记录(如 stock = stock - 1)的并发更新,瞬间引发严重的行锁争用(Row Lock Contention),进而造成 CPU 飙升、连接池耗尽、响应超时,甚至服务雪崩。
本文将以一个真实电商秒杀系统为蓝本,深度复盘 MySQL 在热点更新下的崩溃过程,并系统性地拆解一套可落地、可扩展、经生产验证的优化方案。
假设商品库存表如下:
CREATE TABLE product_stock (
id BIGINT PRIMARY KEY,
product_id BIGINT NOT NULL,
stock INT NOT NULL,
version INT DEFAULT 0 -- 乐观锁版本号
);秒杀逻辑(伪代码):
// 1. 查询当前库存
Stock stock = select stock from product_stock where product_id = 1001;
// 2. 判断是否 > 0
if (stock > 0) {
// 3. 扣减库存
update product_stock set stock = stock - 1 where product_id = 1001;
}问题在哪?
UPDATE 时,都会竞争同一行的排他锁(X Lock);📉 实测数据:在 10,000 QPS 下,未优化的 MySQL 实例 CPU 达 95%+,P99 延迟 > 5s,失败率超 40%。
limit_req);-- stock.lua
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) > 0 then
return redis.call('DECR', KEYS[1])
else
return -1
end✅ 效果:99% 的请求在 Redis 层被拦截或拒绝,MySQL QPS 降至百位级。
将 1000 件库存拆成 10 个逻辑段,每段 100 件:
INSERT INTO product_stock_bucket (product_id, bucket_id, stock)
VALUES (1001, 1, 100), (1001, 2, 100), ..., (1001, 10, 100);扣减时随机选一个非空桶更新:
UPDATE product_stock_bucket
SET stock = stock - 1
WHERE product_id = 1001 AND bucket_id = ? AND stock > 0;✅ 效果:锁竞争从 1 行分散到 10 行,吞吐提升近 10 倍。
✅ 适用场景:对实时性要求不高的活动(如抽签式秒杀)。
由于 Redis 与 MySQL 存在短暂不一致,需设计兜底机制:
product_id 分库分表,天然隔离热点;指标 | 优化前 | 优化后 |
|---|---|---|
秒杀 QPS | 12,000 | 15,000+ |
MySQL CPU | 95%+ | < 30% |
P99 延迟 | 5.2s | 80ms |
超卖率 | 0.7% | 0% |
系统可用性 | 82% | 99.99% |
✅ 核心手段:Redis Lua 预扣 + 库存分桶 + 异步对账。
高并发下的热点更新问题,本质是资源争用与流量洪峰的矛盾。试图让 MySQL 单点硬抗万级并发,无异于螳臂当车。真正的解法,在于分层卸载、分散竞争、异步兜底。
记住:
“最好的数据库优化,是让请求根本不到达数据库。”
在秒杀这场“战争”中,Redis 是盾,MQ 是渠,分桶是分流阀,而 MySQL,只需安静地做那个最终的“记账员”。如此,方能在流量洪峰中,稳如磐石。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。