秒杀系统高并发优化实战(C++ / Drogon):4.5 压测:验证重复下单(uk_user_sku 幂等,0 重复)
超卖防住了(4.4),第二个要命的问题是重复下单——同一用户连点 N 次,刷出 N 单。这一篇专门压"重复":同一个 (userId, skuId) 并发发一堆请求,再用 MySQL 核对"这个用户只有 1 张订单",证明 uk_user_sku + ON DUPLICATE KEY 的幂等兜住了。 配套代码:sql/schema.sql(uk_user_sku)、src/service/SeckillService.cc(步骤 2 的 ON DUPLICATE KEY UPDATE)、scripts/jmeter-baseline.sh 的并发重复场景。 一、重复下单的两层防护重复下单有两层防线,各管一段: 应用层在途闸门(4.8 的 InflightGuard):同一 (userId, skuId) 已有请求在处理时,后续请求连 DB 都不打直接判 DUPLICATE_ORDER。挡的是"并发窗口内的重复"。 数据库唯一键 uk_user_sku:串行发出的两次下单(第一次已完成、订单已落库),应用层闸门拦不住...
秒杀系统高并发优化实战(C++ / Drogon):4.6 解决超卖:SQL 条件判断(行锁内 WHERE stock>0)
4.4 用压测证明"不超卖"了,4.3 也讲了我们用的是 UPDATE ... WHERE stock>0。这一篇把防超卖的三条主流路线摆在一起对比,说清为什么阶段一选"SQL 条件判断"而不是乐观锁/悲观锁——这是秒杀系统设计里最经典的一道"功能抉择"。 配套代码:src/service/SeckillService.cc(步骤 1 的原子 UPDATE)、sql/schema.sql(seckill_sku)。 一、三条路线速览 路线 核心思想 代表写法 乐观锁 带版本号 CAS,提交时校验版本没被改 UPDATE ... SET stock=stock-1, version=version+1 WHERE id=? AND version=? 悲观锁 先锁住行,再读再改,事务结束才放 SELECT ... FOR UPDATE 然后 UPDATE SQL 条件判断(本文采用) 把"是否还有库存"塞进 UPDATE 的 WHERE UPDATE ... SET...
秒杀系统高并发优化实战(C++ / Drogon):4.7 基线性能测试(官方 JMeter QPS≈371 + 阶段一总结)
4.1~4.6 把基础秒杀的下单链路(列表 / 详情 / 下单 / 不超卖 / 不重复 / 超卖路线抉择)全部跑通并验证了正确性。这一篇给阶段一打一个可量化的基线:到底能扛多少 QPS、延迟长什么样、超卖/重复到底有没有。基线是后面每一次优化的"参照物"——没有它,你根本说不清"阶段二缓存预扣减到底快了多少"。 配套资产:scripts/jmeter-baseline.sh(双通道压测)、jmeter/seckill-baseline.jmx(官方 JMeter 脚本)、jmeter/out/report/index.html(HTML 报告)。压测在 WSL(i7-14650HX + 本地 MySQL)由真机跑出。 一、为什么必须先打基线整个项目的验收硬指标是:每一阶段 QPS 提升一个量级 + 不超卖 + 不重复下单。阶段一(v0.1.x)是"Drogon + MySQL 直连"的最朴素形态,它的任务不是冲吞吐,而是: 把正确性钉死:不超卖、不重复下单...
秒杀系统高并发优化实战(C++ / Drogon):4.8 应用层锁(mutex / 自旋 / 原子三后端)
4.3 已经靠 uk_user_sku 唯一键保证"不重复下单",但那个兜底发生在 MySQL 行锁之后——每个重复请求都得真打一次 DB 才被拒。这一篇在数据库之前加一道"在途闸门":同一个用户在同一个商品上的并发重复请求,还没进 DB 就被应用层挡掉。我们做了 mutex / 自旋 / 原子三种后端,并实测对比它们的开销与收益。 配套代码:src/service/InflightGuard.h(三后端闸门);压测:scripts/lock-bench.sh 2000 100 4、GET /api/lock/stats。 一、先拆一个致命误区:锁到底该锁在哪最直觉的写法是拿 std::mutex 把整个 doSeckill 包起来,锁在 handler 开头、在 DB 回调里解锁。这在 Drogon 里是灾难,两个独立原因: 阻塞 IO 线程:Drogon 的 handler 跑在 IO 线程(本项目 threads_num=4)。一个请求持锁等 MySQL 往返(几 ms),就把这 1/4 的请求全卡...
秒杀系统高并发优化实战(C++ / Drogon):2.3 接入 MySQL 的第一个坑——getDbClient 为什么返回空
服务正常启动,日志干干净净地打出 starting on :8080,然后第一个请求进来,进程当场 SIGSEGV。gdb 抓到的栈只有函数名、没有行号,SeckillService::doSeckill 里的 db_->newTransactionAsync(...) 一声不吭地踩在空指针上——数据库客户端是空的,代码却把它当成已经连上数据库在用。 这不是什么玄学段错误,而是 Drogon 接入 MySQL 时几乎人人都会踩、却极少被写进教程的一个启动期大坑。本文把完整排查链展开:为什么服务能起来、为什么偏偏在请求时才崩、以及官方文档里那句被大多数人忽略的话到底是什么意思。 本文是「秒杀系统(C++ / Drogon)」系列的实战篇。配套仓库 seckill-cpp(GitHub: https://github.com/Hespethorn/seckill-cpp),修复涉及 src/main.cc 与 src/service/SeckillService.cc。真实编译在 WSL 完成,MySQL 走 sql/schema.sql 建库建表。 一、是什么:...
秒杀系统高并发优化实战(C++ / Drogon):2.4 Drogon 多模块项目搭建(CMake + 子项目)
服务能跑、压测能过,可换台机器 cmake --build 之后报 No database is supported——Drogon 启动不报任何错,数据库客户端却永远是空的。这种"编译不报错、运行才炸"的坑,根子不在业务代码,而在项目搭建这道关:C++ 的依赖是 ABI 级别的,探测失败不会红字报错,只会静默给你装一个"缺胳膊少腿"的版本。本文把 seckill-cpp 当前的工程结构、CMake 两层写法、以及依赖安装里的暗坑一次讲透。 配套仓库 seckill-cpp(GitHub: https://github.com/Hespethorn/seckill-cpp),本文对应阶段一 v0.1.x,全部脚本与 CMake 见根目录 CMakeLists.txt、src/CMakeLists.txt、scripts/setup-wsl.sh、scripts/build-wsl.sh。真实编译在 WSL(Ubuntu 22.04+)完成。 一、项目搭建是什么、坑在哪、本质一句话 是什么:把 Drogon、MySQL 客户端、jsonc...
秒杀系统高并发优化实战(C++ / Drogon):2.5 完善项目基础设施(配置 / 日志 / 全局异常)
秒杀接口上线后的第一个周末,运维问了我三个问题:端口在哪个文件改?请求失败到底是"抢光了"还是"系统错了"?出问题的时候日志在哪看?——三个问题我愣了一分钟。功能写完了,但**配置、日志、异常这三件"看不见的基础设施"**决定了一个系统能不能被运维、被排查、被信任。本文讲 seckill-cpp 在这三件事上的落地:config.json 怎么组织、Drogon 内置日志怎么用、错误如何从 SeckillService 一路映射成客户端看得懂的 HTTP 语义。 配套仓库 seckill-cpp(GitHub: https://github.com/Hespethorn/seckill-cpp),本文对应阶段一 v0.1.x,代码见 config.json、src/main.cc、src/controllers/SeckillController.cc、src/service/SeckillService.cc、src/controllers/HealthController.cc。 一、基础设施是什么、坑在哪、本质...
秒杀系统高并发优化实战(C++ / Drogon):2.6 整合高性能异步日志(spdlog + 环形缓冲,替代 Log4j2+Disruptor)
秒杀开始的第 3 秒,接口 RT 突然从 8ms 涨到 200ms,CPU 没满、MySQL 没满——最后定位到是日志在拖后腿:log_level 一开高,每个请求都在 IO 线程上同步写文件,日志磁盘成了隐藏的串行点。Java 生态有 Log4j2 + Disruptor(无锁环形队列)这套成熟异步日志方案,C++ 这边我把等价物自己写了一遍:一个互斥锁版的环形缓冲负责排队,spdlog 负责格式化与落盘。本文讲清楚环形缓冲为什么是异步日志的地基、三个关键取舍,以及 seckill-cpp 里刚落地的真实实现。 配套仓库 seckill-cpp(GitHub: https://github.com/Hespethorn/seckill-cpp),本文对应 main 分支 v0.1.1 之后的新实现,代码见 src/logging/ring_buffer.h、src/logging/AsyncLogger.h、src/logging/AsyncLogger.cc、src/logging/LogStream.h(流式宏),集成见 src/main.cc、src/service/S...
秒杀系统高并发优化实战(C++ / Drogon):2.2 后端接口梳理与设计——GET 还是 POST,防的从来不是抓包
"为避免抓包,理论上还是所有接口都用 POST 吧。"——这句话我在设计秒杀接口时认真纠结过。直觉上 POST 把参数藏进了 body,好像比 GET 安全;但真去翻抓包工具的输出,会发现 Wireshark 抓 HTTP 明文时,URL 和 body 是一个 TCP 流里的前后两段,POST 一点便宜都占不到。 这个误区如果不澄清,接口设计会跑偏成"全 POST"——然后为这个错误的理由付出真实的代价:读接口失去缓存能力、日志里看不到请求参数排障变难、REST 语义崩坏。本文把秒杀系统的接口清单、GET/POST 的取舍逻辑、以及"防抓包"真正的解法一次性讲透。 本文是「秒杀系统(C++ / Drogon)」系列的接口设计篇。配套仓库 seckill-cpp(GitHub: https://github.com/Hespethorn/seckill-cpp),当前 v0.1.x 已落地 /api/health 与 /api/seckill 两个接口,路由挂在 src/main.cc。 一、接口设...
秒杀系统高并发优化实战(C++ / Drogon):2.1 数据库表设计
10000 个人抢 100 件商品,最后卖出去 137 件——这不是段子,是把"查库存、判库存、扣库存"写成三条 SQL 之后的必然结果。很多人第一反应是"加锁",但锁加在哪一层、加在什么时候,其实在建表的那一刻就已经决定了。表结构不是一个中性的容器,它直接决定了你的并发上限:库存字段放在哪张表、有没有那条唯一索引、时间字段用什么类型,每一个选择都在后面以"QPS"或"Bug"的形式还回来。 本文是「秒杀系统(C++ / Drogon)」系列的第二章。配套仓库 seckill-cpp(GitHub: https://github.com/Hespethorn/seckill-cpp),本文对应阶段一的 v0.1.1,表结构见 sql/schema.sql、应用账号见 sql/init_user.sql。真实编译在 WSL 完成。 一、秒杀系统的表设计:是什么、坑在哪、本质一句话 是什么:秒杀场景下,表设计要回答三个问题——库存记在哪(真相源)、谁买过怎么判(幂等)、活动什么时候有效(时间...
秒杀系统高并发优化实战(C++ / Drogon):1.1 专栏介绍
你用 C++ 写过后端吗?大多数人一聊高并发就想到 Java 的那套中间件全家桶,但其实用 Drogon + Redis + RabbitMQ 也能撸出一个 30000+ QPS 的秒杀系统,而且能顺手把 epoll、协程、无锁队列、内存池 这些 Java 教程很少展开的系统底层一次讲透。本文是整个「秒杀系统(C++ / Drogon)」系列的开篇——先把业务挑战和4 阶段架构演进讲明白,后面每篇你都知道自己在"第几层"。 这是我个人用 C++ 从零搭建高并发秒杀系统的实战记录。重点不在堆功能,而是每一步的项目思考、实现路径,以及关键功能抉择上的权衡——为什么先用数据库直连而不是一上来就上缓存?为什么用 Redis Lua 而不是分布式锁?为什么异步下单而不是同步落库?完整 8 章路线图见同目录 内容规划.md。 一、秒杀系统是什么,为什么值得用 C++ 做 是什么:秒杀系统是在极短时间内承接海量并发请求、完成"查库存 → 扣库存 → 下单"的电商子系统。淘宝双十一、京东 618、小米抢购、茅台预约,背后都是它在撑。 坑:它不...

