秒杀系统高并发优化实战(C++ / Drogon):3.11 X-Forwarded-For 取首段的陷阱:一次真实的注册频控绕过
3.10 那篇(注册安全加固:同 IP 注册频控 + 反代真实客户端 IP 解析)里,我为了拿到反代后的客户端 IP,写了一段"取 X-Forwarded-For 首段"的代码,并留下一句结论:最左首段才是最原始客户端。 这篇是勘误。 那句话不是"不够严谨",是错的——而且错得挺危险:它等于把同 IP 注册频控的 key,直接交给了调用方决定。攻击者不需要任何工具,一行请求头就能让频控失效。 更值得记的是,我当初为什么会这么写——不是没查,而是查错了地方。 配套代码:src/controllers/UserController.cc 的 clientIp()(改用 drogon::plugin::RealIpResolver::GetRealAddr)、config.json 的 plugins 段(trust_ips / from_header)、src/service/RegisterGuard.{h,cc}(被绕过的同 IP 频控);验证脚本 scripts/verify-311-real-ip.sh(...
秒杀系统高并发优化实战(C++ / Drogon):收官篇 · 6-8 章规划与预计实现(MQ 削峰 / Lua 预扣 / 防刷限流)
系列写到第五章「读性能优化」时,项目在 v0.2.0 收官了:代码定格在「直打数据库 + Redis 读缓存」这一档,第六至八章(MQ 削峰 / Lua 原子预扣 / 防刷限流)不再落地实现。 但设计不该烂尾。这一篇把剩下三章一次性讲完——不贴落地代码,只讲"如果继续做,会怎么做、为什么这么做、预计能拿到什么、会踩哪些坑"。 本篇性质说明:本篇没有配套落地代码(仓库止于 v0.2.0,见 docs/PLAN.md ADR-10)。文中所有架构、接口、Key 均沿用前五章既有约定,属预计方案;出现的 QPS 数字均为预期目标,不是实测值。判断依据只有一条:它们能不能把已经证明过的方法论继续用下去。 一、先交代:为什么停在第五章 理由 说明 验收指标的性质变了 项目的核心命题是"QPS 跃迁来自架构决策,而不是换框架或堆中间件"。前五章已经把这件事证明完整:440 QPS → 万级,全部来自读缓存这一层架构改动 取舍密度会被稀释 前五章每章都有真实两难(超卖怎么防、Redis 同步客户端为什么不能用、缓存失...
秒杀系统高并发优化实战(C++ / Drogon):1.2 为什么是 Drogon——C++ 框架横评与工业界的真实选择
上一篇 1.1 里我用五句话交代了"为什么选 Drogon",但那五句太像宣传册了。真到了面试或者技术评审上,一定会有人追问一句:"工业界到底谁在用 Drogon?" 这个问题很扎心,而且必须正面回答。这篇文章就干三件事:把 Drogon 和它的同类放到一张桌子上比,把"工业界实际用什么"讲清楚,最后交代我在这个项目上做这个选型时放弃了什么。 一、先回答最扎心的那个问题:工业界真用 Drogon 吗?诚实答案分三层。 第一层:大厂的 C++ 后端,几乎不使用"Web 框架"这个词。 你去问百度、腾讯、字节的 C++ 团队用什么,答案不会是 Drogon,也不会是 oat++,而是 RPC 框架——brpc、Sogou Workflow(及其上的 SRPC)、TARS、gRPC。原因很简单:大厂内部服务之间走的是二进制协议 + 服务发现,HTTP 只是南北向入口那一层,而入口通常由 Nginx / OpenResty / Envoy / Go 网关承担,轮不到 C++ 业务...
秒杀系统高并发优化实战(C++ / Drogon):5.1 缓存 Key 设计规范与接口基线压测
阶段一我们打出基线 QPS≈371 / 0 超卖,根因钉死在 seckill_sku 单行锁串行化——那是写接口(下单)的问题。但同一时刻还有两个读接口在被反复调用:一万个用户同时刷新列表,MySQL 就把同一条 SELECT 执行一万遍,返回一模一样的结果。读请求和写请求是两种完全不同的动物,这一章我们专门收拾读接口。 本文是「秒杀系统(C++ / Drogon)」系列的第五章第一篇。配套资产:docs/CACHE-DESIGN.md(缓存规范全文)、src/service/CacheKeys.h(Key 唯一构造入口)、src/service/SkuCache.*(Redis 异步封装)、scripts/read-bench.sh + jmeter/read-baseline.jmx(读接口基线压测)。代码对应阶段二 v0.2.x。真实编译与压测在 WSL(i7-14650HX + 本地 MySQL + 本地 Redis)完成。 一、为什么现在才加缓存,加在哪 是什么:读接口(GET /api/seckill/list、GET /api/seckill...
秒杀系统高并发优化实战(C++ / Drogon):5.2 加 Redis 缓存:商品列表接口
4.1 我们把商品列表接口(GET /api/seckill/list)跑通了,但那时它每次都直连 MySQL。到了第五章,第一件事就是给这个被刷新最频繁的接口套一层 Redis 缓存。它的 key seckill:sku:v1:list 是「全量商品」的聚合值,是热点中的热点——值得专门写一篇讲清楚它和详情缓存在取舍上的不同。 本文是「秒杀系统(C++ / Drogon)」系列的第五章第二篇。配套代码:src/service/SeckillService.cc::listSkus/queryListFromDb、src/service/SkuCache.*、src/main.cc 缓存配置解析。代码对应阶段二 v0.2.x。Key 规范与 TTL 抖动见上一篇 5.1。 一、列表缓存:是什么、坑在哪、本质一句话 是什么:列表接口返回全部商品(LIMIT 100),结果对所有用户一致、允许短暂陈旧——教科书级的缓存对象。 坑:列表是一个全量聚合 key,任意一件 sku 成交都改变了它的内容。如果「写后失效」策略选错(把列表也一起删),在写 QPS 高时这个 key...
秒杀系统高并发优化实战(C++ / Drogon):5.4 缓存一致性:为什么先删库后删缓存,以及延迟双删
5.2 / 5.3 给读接口加了缓存,但缓存是副本——副本和真相源(MySQL)之间随时可能不一致。这一篇把缓存层最容易被问倒的问题讲透:写操作之后缓存怎么办?为什么顺序错了会出事故?单次删除还留了什么缝?延迟双删补的是哪条缝? 配套代码在 5.4 已落地为可开关的 double_delete_ms(默认关),这篇讲清它到底在防什么、以及为什么默认不开。 本文是「秒杀系统(C++ / Drogon)」系列第五章第四篇。配套代码:src/service/SkuCache.*(失效策略 + DelayDeleter 延迟删除线程)、config.json 的 cache.double_delete_ms。代码对应阶段二 v0.2.x。 一、缓存一致性:是什么、坑在哪、本质一句话 是什么:读接口走缓存后,系统里同一份数据存在两个地方——MySQL(真相)和 Redis(副本)。写操作发生的那一刻,两处就不一致了。所谓缓存一致性,就是管理"副本何时作废、何时重建、作废后会不会读到脏值"这一整套时序问题。 坑(最常见的两处):① 顺序反了——先删...
秒杀系统高并发优化实战(C++ / Drogon):5.3 加 Redis 缓存:商品详情接口(含空值哨兵防穿透)
4.2 把商品详情接口(GET /api/seckill/{skuId})跑通了,主键点查、直连 MySQL。这一篇给它套缓存,并顺手落地一个常被放到 5.6 才讲的能力——空值哨兵防缓存穿透。原因很实际:详情接口的 {skuId} 是用户可控的,攻击者拿随机 id 狂打,缓存层若不做处理,会退化成「每次都穿透到 DB」,等于没缓存。所以防穿透和详情缓存是同一道题的正反两面,一起讲。 本文是「秒杀系统(C++ / Drogon)」系列的第五章第三篇,也是 5.1/5.2/5.3 读缓存三篇的收口。配套代码:src/service/SeckillService.cc::detailSku/queryDetailFromDb、src/service/SkuCache.*(setNull/kNullValue)。代码对应阶段二 v0.2.x。Key 规范、TTL 抖动、列表缓存见 5.1/5.2。 一、详情缓存 + 空值哨兵:是什么、坑在哪、本质一句话 是什么:详情接口按 skuId 主键点查...
秒杀系统高并发优化实战(C++ / Drogon):5.5 缓存预热与雪崩:冷启动的回写风暴,和流量进来前该做的事
5.2 / 5.3 把读缓存架起来了,TTL 抖动也顺带做了——那防的是运行中的大批 key 同时过期。但还有一个更隐蔽的时刻:服务刚启动、缓存全空的那一刻。洪峰第一波请求全部 miss、全部回源、全部回写——缓存不但没挡住流量,反而给数据库来了一次"回写风暴"。这一篇讲缓存预热:为什么它是运营动作而不是服务行为,冷启动、雪崩、击穿三个词到底谁是谁,以及 5.5 落地成代码的预热端点长什么样。配套代码 POST /api/cache/warm(scripts/cache-warm.sh)在 5.5 已落地。 本文是「秒杀系统(C++ / Drogon)」系列第五章第五篇。配套代码:SeckillService::warmCache、src/main.cc 的 /api/cache/warm 路由、scripts/cache-warm.sh。代码对应阶段二 v0.2.x。 一、冷启动:是什么、坑在哪、本质一句话 是什么:进程刚 run() 起来时,Redis 里没有任何 seckill:sku:* key。此时用户洪峰正好杀到,每一个读请...
秒杀系统高并发优化实战(C++ / Drogon):5.6 防缓存穿透:空值策略——把"查无此物"也缓存下来
缓存防的是"重复查同一份数据"。但如果用户查的根本不是数据呢?商品详情接口的 {skuId} 是用户可控的,拿一个不存在的 id 狂刷,缓存里永远没有这个 key——每一次请求都穿透到 MySQL 查一个空结果。这一篇把缓存穿透讲完整:攻击者怎么打、空值策略为什么有效、TTL 定多长是安全与一致性的折中、以及它和 5.7 布隆过滤器怎么分工。代码(SkuCache::setNull 空值哨兵)早在 5.3 就随详情缓存一起落地了,本篇是把"为什么这么做"补全。 本文是「秒杀系统(C++ / Drogon)」系列第五章第六篇。配套代码:src/service/SeckillService.cc::queryDetailFromDb(查无此物 → cache_->setNull)、src/service/SkuCache.cc::setNull(__nil__ 哨兵 + 60s TTL)。代码对应阶段二 v0.2.x。 一、缓存穿透:是什么、坑在哪、本质一句话 是什么:攻击者(或手滑的用户)请求一个数据库...
秒杀系统高并发优化实战(C++ / Drogon):5.7 防缓存穿透:自实现布隆过滤器——海量随机 id 的终结者
5.6 的空值哨兵把"同一个不存在 id"挡在了 Redis 内,但它有个软肋:打过来的 id 如果每个都不一样,哨兵就要为每个不存在的 id 占一个 Redis key——内存随攻击流量线性涨。20 万商品的种子数据下,这个软肋变成现实威胁:id 是连续整数,攻击者随便枚举就能制造海量"新不存在的 id"。这一篇落地布隆过滤器:在进程内用约 1MB 内存维护"真实存在的 sku id 全集"的紧凑摘要,集合外的 id 直接 404——连 Redis 都不打。配套代码 src/service/BloomFilter.h(自实现,约 60 行核心)+ POST /api/cache/warm {"rebuild_bloom":true} 在 5.7 已落地。 本文是「秒杀系统(C++ / Drogon)」系列第五章第七篇。配套代码:src/service/BloomFilter.h、SeckillService::rebuildBloom / bloomAllows / ...
秒杀系统高并发优化实战(C++ / Drogon):5.8 多级缓存:自实现本地 LRU 做 L1
5.25.7 一直在优化"哪些读可以不打 MySQL"。但仔细想想,Redis 命中本身也有成本:一次网络往返 0.20.5ms。洪峰期所有用户刷同一批热 key,这个往返是纯开销——数据明明就在本进程里能算出来,为什么每次都要绕一圈网络?这一篇落地多级缓存:进程内加一层自实现的 LRU 做 L1,读路径变成 L1 本地内存 → L2 Redis → L3 MySQL。同时回答两个最尖锐的问题:L1 的一致性怎么保?L1 到底对什么样的流量有效? 配套代码 src/service/LocalLruCache.h(自实现)+ scripts/local-bench.sh(对比压测)在 5.8 已落地。 本文是「秒杀系统(C++ / Drogon)」系列第五章第八篇(阶段二收尾)。配套代码:src/service/LocalLruCache.h、SkuCache 内嵌 L1(读 L1→L2→L3,写/失效同步本地)、config cache.local_enabled / local_capacity。代码对应阶段二 v0.2.x。 一、为...
秒杀系统高并发优化实战(C++ / Drogon):3.1 用户注册接口:PBKDF2 加盐哈希 + 自实现 JWT + 可吊销会话
10000 个人抢 100 件商品,最后卖出去 137 件——这句"段子"我们在表设计那篇就破了。但秒杀真正的第一道门,不是库存,是身份:你是谁?你有没有资格抢?如果你连"注册"这一步都把明文密码写进库,那后面无论 Redis 预扣、Lua 原子、MQ 削峰做得再漂亮,用户表一旦泄露就是全裸——所有"高并发安全"都建立在沙滩上。 本文是「秒杀系统(C++ / Drogon)」系列第三章(登录模块)的开篇。配套仓库 seckill-cpp。当前阶段一 v0.1.x 已落地登录 / 短信模块,目录为 sql/user_schema.sql、src/service/UserService.{h,cc}、src/controllers/UserController.{h,cc}、src/service/password.{h,cc}、src/service/Jwt.{h,cc}、src/service/SessionStore....
秒杀系统高并发优化实战(C++ / Drogon):3.10 注册安全加固:同 IP 注册频控 + 反代真实客户端 IP 解析
验证码改成自签发 / 日志模式后(3.3),"送达"不再是外部约束——注册接口等于裸奔在公网:攻击者拿一堆手机号从同一个 IP 批量注册,就能刷出成千上万个账号。这篇补两道闸门:① 同 IP 注册频控堵批量刷号;② 反代真实客户端 IP 解析,否则频控的 key 拿到的是代理 IP,等于没防。 配套代码:src/service/RegisterGuard.{h,cc}(check / markSuccess + kCheckScript / kMarkScript)、src/controllers/UserController.cc 的 clientIp()、UserService::registerUser 最前的频控闸门。阈值走 config.json 的 register_limit(max_per_ip=5 / window_seconds=3600)。 ⚑ 2026-09-17 勘误(重要)本文初版主张「取 X-Forwarded-For 首段即最原始客户端」,这个结论是错的。XFF ...
秒杀系统高并发优化实战(C++ / Drogon):3.2 用户登录接口:密码比对 + 自实现 HS256 签发 + Redis 可吊销会话
注册接口建好了"身份",这一篇让身份"进门"——登录接口。它要做三件事:比对密码、签发 JWT、把会话写进 Redis。前两件是安全,第三件是"可吊销"——没有它,JWT 一旦签发就只能在 exp 到点后自己失效,改密码/封号都拦不住旧 Token,等于门没锁。 配套代码:src/service/UserService.cc(login)、src/service/Jwt.{h,cc}、src/service/SessionStore.{h,cc}、src/controllers/UserController.cc(login)。延续 3.1 的"服务层只做业务、控制器只做协议转换"分层。 一、登录接口:是什么、坑在哪、本质一句话是什么:登录 = 用"手机号 + 密码"证明你是你,成功后发一个不可伪造的凭证(JWT),并在 Redis 留下一条可吊销的会话记录,让服务端随时能把这把钥匙作废。 坑在哪: 密码明文比对的时...
秒杀系统高并发优化实战(C++ / Drogon):3.3 短信验证码接口:生成 / 限流 / 存储 / 腾讯云直连
注册/登录要不要验证码?要——但验证码接口本身就是个高价值攻击面:它既能当"免费短信轰炸机"打别人的手机,又能被脚本爆破出 6 位验证码。这一篇把验证码接口做完整:生成(CSPRNG)→ 限流(冷却 + 每日上限)→ 存储(Redis)→ 发送(腾讯云直连),每一道都对应一个具体的攻击面。 配套代码:src/service/SmsService.{h,cc}(生成/限流/存储/校验)、src/service/SmsSender.{h,cc}(腾讯云直连)、src/controllers/SmsController.cc、src/service/SmsSender.cc 的 TC3 签名。 一、短信验证码接口:是什么、坑在哪、本质一句话是什么:POST /api/sms/send 给指定手机号生成 6 位验证码,限流后存入 Redis(有效期 + 重发冷却 + 每日上限),再通过腾讯云短信 API 发出去。校验在注册(3.1)/登录(3.2)流程里调 verifyCod...
秒杀系统高并发优化实战(C++ / Drogon):3.4 注册验证码校验 & Token 持久化到 Redis
3.1 把注册流程的"验证码分支"留了空(先查重再哈希再落库,requireSmsOnRegister=true 时走 sms_->verifyCode);3.2 把"登录成功写 Redis 会话"说了。这一篇把这两件事的接缝焊死:验证码怎么校验、Token 怎么持久化、退出怎么吊销,以及它们背后的 key 约定与配置开关。 配套代码:src/service/UserService.cc(registerUser 的 verifyCode 分支、login 的 sessions_->save、logout 的 sessions_->revoke)、src/service/SessionStore.{h,cc}、配置项 requireSmsOnRegister / requireSmsOnLogin。 一、本篇在链路里的位置:是什么、坑在哪、本质一句话是什么:把"验证码校验"接到注册/登录流程里(挡脚本批量注册),把"登录签发的 JWT"的 ...
秒杀系统高并发优化实战(C++ / Drogon):3.5 验证码安全加固:Redis Lua 原子校验 + 每日发送上限 + 试错上限
3.3 把短信接口的流程跑通了(生成→限流→存储→发送)。这一篇专门挖安全加固这一层:为什么限流和校验必须写成 Lua、每日上限的 key 为什么要计算"距次日 0 点"的 TTL、6 位验证码在数学上意味着什么、以及"发送失败不回滚配额"这个有意的取舍。 配套代码:src/service/SmsService.cc 的 kSendScript / kVerifyScript / secondsUntilTomorrow / genNumericCode。 一、为什么需要加固:三个攻击面 攻击面 后果 加固手段 短信轰炸 拿接口打别人手机,败坏口碑 + 账单是你的 重发冷却 + 每日上限(都在 kSendScript) 验证码爆破 6 位仅 1M 组合,脚本几十分钟撞出 校验次数上限(kVerifyScript 的 maxVerifyAttempts) 一码多用 并发下同一码被兑换两次 校验与消费同一 Lua 原子完成 二、发送侧 Lua:冷却 + 每日上限原子化kSendScript...
秒杀系统高并发优化实战(C++ / Drogon):3.6 登录安全加固:密码错误次数限制 + 账号临时锁定
登录接口(3.2)能比对密码了,但还差最后一道防线的"门闩":密码错误次数限制 + 账号临时锁定。没有它,攻击者可以对一个手机号无限次试密码(撞库);有了它,连续错 N 次就临时锁几分钟,把撞库成本抬高到不划算。 配套代码:src/service/LoginGuard.{h,cc}(checkLocked / onFailure / onSuccess + kOnFailureScript / kCheckScript)、UserService::login 里埋的 guard_-> 调用。 一、是什么、坑在哪、本质一句话是什么:LoginGuard 用 Redis 记录每个手机号的密码失败次数,连续失败达 max_fail(默认 5)就 SETEX login:lock:{phone} 锁定 lock_seconds(默认 600);登录成功清计数;登录前先查锁定。 坑在哪: 计数放进程内存:多实例部署时每台各记各的,"5 次就锁"变成"5×N 次...
秒杀系统高并发优化实战(C++ / Drogon):3.7 退出登录接口:校验 Token + 删 Redis 会话即吊销
3.2 把"登录签发 + 写 Redis 会话"讲完,3.4 把"退出即吊销"预览了一句。这一篇把退出登录做成完整接口:POST /api/user/logout,从 Authorization: Bearer 取 Token、验签、删 sess:{jti}——删掉那一刻,旧 Token 立即失效。 配套代码:src/controllers/UserController.cc(logout + extractBearerToken)、src/service/UserService.cc(logout / authenticate)、src/service/SessionStore.cc(revoke)。 一、是什么、坑在哪、本质一句话是什么:退出登录 = 客户端带上 Token,服务端验签通过后删除 Redis 里的 sess:{jti}。JWT 本身无法被"改",但此后每次鉴权都会因 EXISTS sess:{jti} 失败而被拒。 坑...
秒杀系统高并发优化实战(C++ / Drogon):3.8 代码细节重构:抽出 seckill::http 统一 JSON 响应 helper
阶段一的代码越写越多,SeckillController / UserController / SmsController 每个 handler 都在手写 newHttpJsonResponse + setStatusCode 的样板,而且"什么业务码配什么 HTTP 状态"各写各的,容易不一致。这一篇做一件小但重要的重构:把统一 JSON 响应抽成 seckill::http 命名空间(src/util/HttpJson.h),所有控制器复用。顺带把重构中踩到的两个 Drogon 1.9.10 编译坑一并记下。 配套代码:src/util/HttpJson.h、src/controllers/SeckillController.cc(三处 handler 改用 reply / replyData)。 一、为什么要重构:重复样板 + 状态码不一致重构前,每个 handler 都是这套: 123456// 重构前:每个 handler 重复且易错Json::Value root; root["code"] =...
秒杀系统高并发优化实战(C++ / Drogon):3.9 验证码接口安全加固:滑块验证码(设计篇,代码推迟到前端阶段)
⚠️ 本文是设计篇,不落地代码。 滑块验证码的完整实现(前端滑块 + 后端校验)推迟到「开始有前端」阶段再做。本篇只把后端必须自写的部分设计清楚、把契约定下来,避免到时候返工。原因见第六节「为什么代码推迟」。 3.3~3.5 做的是短信验证码(防脚本批量注册)。短信验证码有个软肋:它是"知识因子"——6 位数字,真能拦住人肉,但拦不住"养一批号 + 接码平台"的专业黄牛。滑块验证码补的是另一层:行为因子——你拖动滑块的轨迹,机器人很难逼真模仿。这一篇讲它的设计。 一、为什么是滑块:人机区分的本质短信验证码回答"你有没有这个手机号",滑块回答"你是不是真人在操作"。两者维度不同: 短信 = 拥有因子(你有这个号,接得到码) 滑块 = 行为因子(你的拖动轨迹像人) 黄牛可以批量为号接码,但很难批量为每个号生成"像人"的拖动轨迹。所以滑块是挡自动化脚本的更前沿一道,通常放在"发短信/登录/下单"之前做前置人机校验。 二、...
秒杀系统高并发优化实战(C++ / Drogon):4.1 秒杀商品列表接口开发
登录模块(第三章)做完,进入第四章——基础秒杀功能(阶段一:Drogon + MySQL 直连)。先来最轻松的一个:商品列表接口 GET /api/seckill/list。它只读、不碰事务、不校验 token,是秒杀系统里最"安静"的一条路,但有几个 C++/Drogon 细节值得讲。 配套代码:src/service/SeckillService.cc(listSkus)、src/controllers/SeckillController.cc(listSkus)、src/main.cc 路由注册。 一、是什么、坑在哪、本质一句话是什么:商品列表 = 从 seckill_sku 读出所有在售商品(id/name/stock/total/起止时间),前端用来展示"有哪些可以抢"。只读查询,不分页(阶段一 LIMIT 100)。 坑在哪: DATETIME 直接绑成 tm/Date:Drogon ORM 把 DATETIME 列默认当 tm 或 Date 类型,序列...
秒杀系统高并发优化实战(C++ / Drogon):4.2 秒杀商品详情接口开发
列表是"有哪些可以抢",详情是"这一个还能抢多少"。GET /api/seckill/{skuId} 按 id 查单个商品——它引出一个 Drogon 路由的关键细节:路径参数怎么取、静态路径和参数路径撞名怎么办。 配套代码:src/service/SeckillService.cc(detailSku)、src/controllers/SeckillController.cc(detailSku)、src/main.cc 三参 handler 路由。 一、是什么、坑在哪、本质一句话是什么:商品详情 = 按 skuId 从 seckill_sku 查出单条商品,前端用来展示"这个商品的库存/时间"。只读、按主键查。 坑在哪: 路径参数取错形式:Drogon 三参 handler (req, callback, path) 只在该路由有 {} 占位符时才正确绑定第 3 参;无占位符路由用三参会被 fromRequest<std::string> ...
秒杀系统高并发优化实战(C++ / Drogon):4.3 秒杀下单接口开发(基础版,同步落库)
阶段一的核心来了。秒杀下单 POST /api/seckill 要解决两个会要命的问题:超卖(100 件卖成 137 件)和重复下单(同一人刷多单)。这一篇把"阶段一基础版"的打法讲透:单行原子扣减 + 同一事务 + ON DUPLICATE KEY 幂等。 配套代码:src/service/SeckillService.cc(doSeckill)、src/controllers/SeckillController.cc(seckill)、sql/schema.sql(uk_user_sku)。 一、是什么、坑在哪、本质一句话是什么:下单 = 扣减库存(若还有)+ 落一条订单,两者要么全成、要么全回滚;同一用户对同一商品只能成一单。 坑在哪: 先 SELECT 再 UPDATE:并发下读到的都是旧值,必然超卖——这是秒杀第一大坑。 扣库存和落订单不包事务:中途崩溃留下"库存扣了但没订单"的不一致。 裸 INSERT 做幂等:唯一键冲突抛异常,外层 catch 当成 DB 错误回滚——但此时库存已经被扣了,且这本该是"...
秒杀系统高并发优化实战(C++ / Drogon):4.4 压测:100 用户抢 10 件,核对不超卖
4.3 把"原子扣减 + 事务 + 幂等"写完了,但代码说"不超卖"不算数——压测说不算超卖才算数。这一篇用压测 harness 跑"100 用户抢 10 件",再用 MySQL 直接核对 sold == orders == 10,证明原子 UPDATE ... WHERE stock>0 在高并发下真的扛住了。 配套脚本:scripts/jmeter-baseline.sh(correct 模式)、scripts/smoke-seckill.sh。核对 SQL 见 4.3 / README「快速查看数据」。 一、压测目标:用"卖出去的件数"反证不超卖超卖的定义就一句话:卖出的件数 > 库存。所以验证不超卖,不是看返回码,而是看 MySQL 里的真相源: 1234-- 卖出件数 = 总量 - 剩余;订单数 = 落库行数。两者必须相等,且 ≤ 总量SELECT (SELECT total FROM seckill_sku WHERE id=1) - (SELECT stock ...

