
1. 项目概述以秒杀系统为切入点理解高并发架构的底层逻辑先说结论这套基于 Node.js 商城小程序的秒杀系统是我个人认为目前最适合拿来从零拆解的完整实战项目。它不只是一个卖货页面加一个倒计时按钮而是一整套涉及前端交互、后端服务、消息队列、库存扣减一致性、限流防刷、分布式锁的高并发解决方案。标题里的架构源码附源代码意味着你可以拿到完整代码边读边跑边改这是纯理论文章给不了的体验。适合谁来学三类人第一类是对 Node.js 有基础但没实战过完整电商项目的后端工程师第二类是前端转全栈、想理解小程序端和服务端如何配合的开发者第三类是准备面试、想自己动手做一个可讲述的高并发项目来充实履历的学生。如果你只是看过几篇秒杀系统的文章、没写过一行代码那这个项目正好可以作为打开你认知边界的第一个完整源码库。秒杀系统的难点永远不在业务本身而在同一瞬间成千上万请求同时打进来系统怎么保证不崩、不超卖、不重复下单。下单逻辑本身很简单难的是如何用有限的资源承接无限的并发、如何在服务端宕机时依然保证库存数据最终一致、如何把恶意用户和正常用户区分开。这套项目把这些问题集中在一起而 Node.js 的单线程事件循环、非阻塞 I/O、轻量级部署这些特性使得它特别适合用来承载这种IO 密集 逻辑相对简短的秒杀场景。我在实际跑这个项目的时候最大的感受是每一个模块单独看都很简单合在一起就变成了复杂度的大杂烩——Redis 缓存的库存预热、RabbitMQ 的异步削峰、基于版本号的乐观锁扣库存、可重入锁的分布式互斥……这些知识点分开学都懂但当你需要在一个项目里把它们串起来并且保证链路不丢数据的时候才是真正考验架构能力的时候。下面我按完整拆解的顺序来讲尽量把每一个环节的为什么这么做都讲透。2. 项目整体架构拆解技术选型与设计动机2.1 为什么是 Node.js 小程序这个组合小程序前端 Node.js 后端这个组合在秒杀场景里其实非常合理。小程序端天然适合做秒杀因为它有微信生态的流量入口用户从分享卡片点进来就能直达秒杀页没有 H5 的加载延迟。而 Node.js 在服务端虽然不能拼 C/Java 的极致性能但在秒杀这种大量短连接、JSON 序列化多、IO 操作密集的场景下Node.js 的高并发 IO 能力反而能够发挥出优势——单线程事件循环让它在同配置下比传统的阻塞式服务承接更多的并发连接。再说技术栈的全貌前端是微信原生小程序后端是 Node.js 的 Express 框架数据库用 MySQL 存订单和商品原始数据Redis 做库存缓存和分布式锁RabbitMQ 做消息队列削峰。这套选型的思路非常务实——没有引入 Kafka 或 RocketMQ 这类重组件而是选择了相对轻量的 RabbitMQ没有用微服务全家桶而是保持单体应用加分布式中间件的组合。对一个学习型项目而言这个梯度刚刚好你既能理解单体应用的优缺点又能接触分布式场景下的经典中间件。2.2 秒杀系统的高并发瓶颈在哪里在拆解代码之前我先讲清楚秒杀系统的并发瓶颈到底在哪否则后面看代码容易雾里看花。一个普通的商品下单链路是用户点击 → 创建订单 → 校验库存 → 扣减库存 → 返回结果。这一步在平时没问题但在秒杀瞬间会出现三个致命瓶颈数据库写热点所有人都在对同一条商品库存记录做 UPDATEMySQL 的行锁会让这些请求排队响应时间从毫秒级掉到秒级接口被刷脚本用户可以绕开小程序前端直接以每秒上千次的频率调用后端下单接口把库存瞬间刷空下单响应慢一个完整的订单创建涉及很多表订单表、订单明细表、库存表、日志表如果全部同步执行用户的等待时长会被拉得很长。这套项目的架构设计本质上就是在解决上述这三个问题。解决方案总结成一句话把流量挡在前面把压力异步化到后面。具体链路是先通过小程序端限时按钮挡住一部分无效点击再在后端接口入口用限流中间件做拦截然后通过 Redis 预扣库存将压力从 MySQL 转移最后通过消息队列异步生成订单。每一步都向前端少返还不是少做而是把真正重的事情挪到低峰期慢慢做。2.3 目录结构与模块划分源码的目录结构设计得比较规整按功能模块而不是按层级来划分这一点对新读者非常友好。核心目录如下├── miniprogram # 小程序前端代码 │ ├── pages │ │ ├── index # 秒杀商品列表页 │ │ ├── detail # 商品详情/秒杀详情页 │ │ ├── order # 订单确认页 │ │ └── user # 用户中心 │ ├── utils │ └── api # 封装的后端请求方法 ├── server # Node.js 后端服务 │ ├── routes # 路由定义 │ ├── controllers # 控制器层 │ ├── services # 业务逻辑层 │ ├── dao # 数据访问层 │ ├── middleware # 中间件如限流、鉴权 │ ├── jobs # 定时任务如秒杀结束后清理 │ └── app.js # 服务入口 ├── sql # 数据库初始化脚本 └── docker-compose.yml # 一键启动中间件我最欣赏的一个设计是 controller → service → dao 三层分离这绝不是多余。在秒杀这种高并发场景下你随时可能需要替换底层存储比如把 MySQL 换成 RDS、把 Redis 换成自建集群如果没有 service 层做隔离替换一个中间件就得动业务代码很容易引入线上 bug。另外一个值得说的是middleware目录限流逻辑在这里以中间件形式独立存在既可以在秒杀接口上复用也可以在普通商品接口上加上不同的阈值配置灵活度很高。3. 核心模块解析从登录鉴权到库存扣减的全链路实现3.1 登录态的建立与传递小程序端的登录不像传统 Web 端输账号密码它依赖微信的wx.login换取 openid。这个过程基于微信的 OAuth 体系逻辑是小程序调用wx.login获取临时 code → 将 code 发给后端 → 后端用 code 去微信接口换取 openid → 后端生成自己的 token 返回给小程序 → 小程序把 token 存到 storage后续所有请求带着这个 token。这个项目在 token 方案上使用 JWTJSON Web Token而不是服务端 session这一点我认为选得很对。原因有两层第一层秒杀接口会被频繁调用每次请求都查一次 Redis 中的 session 会增加一次额外的网络往返而 JWT 的无状态设计省掉了这一步第二层JWT 天然支持跨服务校验假如你把后端扩展成几个微服务实例JWT 可以直接在网关层校验而不需要共享 session 存储。虽然 JWT 有无法主动失效的缺点但通过将 token 有效期设短、在 Redis 中维护黑名单就能比较好地弥补。// server/controllers/authController.js const jwt require(jsonwebtoken); const axios require(axios); const WX_APPID config.wx.appid; const WX_SECRET config.wx.secret; exports.login async (req, res) { const { code } req.body; const url https://api.weixin.qq.com/sns/jscode2session?appid${WX_APPID}secret${WX_SECRET}js_code${code}grant_typeauthorization_code; const { data } await axios.get(url); const token jwt.sign({ openid: data.openid }, config.jwt.secret, { expiresIn: 2h }); res.json({ token }); };读者看这段代码的时候要特别留意一点每次登录都重新发一个新 token 是可以的但更推荐的做法是在 Redis 中建立一个openid - token的映射并设定过期时间这样同一个用户不会因为多次登录而积累一堆有效 token。我在实际开发中还遇到过一种情况就是用户分别在两个微信环境比如手机和开发者工具里登录同一个账号两个 token 同时在用这时候如果你在服务端强制一个用户只能有一个 token就会导致其中一个设备被频繁踢下线体验很差。所以更稳妥的方式是允许一个用户保留两到三个有效 token相同 token 才做互踢。3.2 秒杀商品模型与 Redis 库存预热商品表在关系型数据库中的结构是常规的秒杀本质上只是给商品增加了一个seckillStock字段和一个seckillStartTime、seckillEndTime字段。但真正决定性能的不是数据库表怎么设计而是运行时的库存存放在哪里。这个项目直接查库不是而是在秒杀开始前通过一个定时任务把商品的秒杀库存加载到 Redis 中。Redis 里的库存存储结构用的是普通的 String 类型key 为seckill:stock:{productId}value 就是剩余库存数量。你可能想问库存用 String 不就够了吗是够的因为秒杀场景对单个 key 的操作基本就是DECR没有复杂的成员操作。但面试常问的一个点在这里为什么不用 Hash因为 Hash 是基于字段维度的虽然看起来可以存多个商品的库存在一个 key 里但当你对某个商品做 DECR 时Hash 的 HSET 需要先获取整个 hash序列化开销更高。String 的DECR是原子操作性能最高语义也最简单。// server/jobs/preloadStock.js const { getRedisClient } require(../utils/redis); const { getProductStock } require(../dao/productDao); // 秒杀开始前 5 分钟执行将数据库库存加载到 Redis exports.preload async () { const products await getProductStock(); await products.reduce(async (prev, product) { await prev; await redisClient.set(seckill:stock:${product.id}, product.seckillStock); }, Promise.resolve()); console.log([preload] 库存加载完成共 ${products.length} 个商品); };这里有一个很重要但容易踩坑的细节库存预热必须最终和数据库同步。我在跑项目时犯过一个错就是秒杀开始后直接对 Redis 做DECR秒杀结束就把库存数量丢弃结果出现了 Redis 中库存和数据库不一致的问题。正确的做法是在秒杀结束或者库存扣减完成后把 Redis 最终的库存值回写到 MySQL 的seckill_stock字段。另外预热脚本要考虑重复执行问题。如果你用定时任务每分钟都去检查seckillStartTime是否在当前时间的一分钟以内那就可能重复加载。建议在 Redis 中设置一个seckill:preload:flag:{productId}加载过就不再重复加载更稳妥。3.3 秒杀接口的分层拦截与限流策略秒杀接口在真实场景下会被极其凶猛的用户流量冲击所以接口不能只做库存判断还要做多层次的拦截。这套项目的拦截顺序很有讲究第一层时间窗拦截。秒杀开始前和结束后接口直接拒绝请求。代码中判断当前时间是否在seckillStartTime与seckillEndTime之间不通过就返回一个统一定义的错误码第二层用户维度限流。同一个用户openid在 5 秒内只能访问一次秒杀接口。项目用 Redis 的SETNX 过期时间实现key 为seckill:user:{openid}:{productId}第三层全局维度限流。使用简单的令牌桶算法限制整个接口每秒最多处理 500 个请求第四层库存预检查。这个阶段才访问 Redis 的库存 key如果库存不足直接返回已抢光。// server/middleware/rateLimit.js exports.userRateLimit async (req, res, next) { const openid req.user.openid; const productId req.params.productId; const key seckill:user:${openid}:${productId}; const result await redisClient.set(key, 1, NX, EX, 5); if (!result) { return res.json({ code: 429, msg: 操作过于频繁 }); } next(); };说实话这个项目的限流策略已经够一个学习项目用了但如果在生产环境中你还要考虑 IP 维度的限制防止同一设备多点登录绕开用户维度限制。另一个更深的坑是SETNX限流会有一个小概率的误伤就是当用户上一次请求刚好在第五秒结束时发起下一次在第五秒的毫秒边界处又发起可能会被拒绝两次。这个问题可以通过记录上一次时间、做滑动窗口来解决但对学习项目来说影响不大知道有这个边界情况就行。3.4 库存扣减的原子操作与超卖问题超卖是整个秒杀系统最不能容忍的 bug。所谓超卖就是 10 个库存卖出去 20 个订单。如果扣减库存是读出来 → 判断 0 → 减一 → 写回去这个序列两个并发请求同时读到库存为 1两个都判断库存够然后都减一写回最终数据库里库存从 1 变成 0但已经卖出了两个。这就是典型的并发竞态条件。预防方案有四种从低级到高级分别是数据库行锁在购物车场景够用但并发高时行锁竞争依然严重乐观锁通过UPDATE ... WHERE stock 0来保证只有库存大于零时才能扣减成功影响行数为 0 则说明竞争失败RedisDECR原子操作DECR本身是原子的且在值为 0 之后再次DECR会变成负数需要代码里做判断分布式锁在扣减前后加一把锁保证同一时间只有一个请求在扣减某个商品的库存。这个项目用了Redis DECR 提前判断的组合方式先DECR如果返回负数说明已抢完再把库存加回去INCR并返回失败。这个方案的好处是简单高效不需要在 MySQL 上执行带 WHERE 条件的 UPDATE避免了数据库行锁的竞争压力。// server/services/seckillService.js exports.tryDeductStock async (productId, userId) { const key seckill:stock:${productId}; const stock await redisClient.decrby(key, 1); if (stock 0) { // 恢复库存防止负数累积 await redisClient.incrby(key, 1); return { success: false, msg: 秒杀已结束 }; } // 扣减成功发送消息队列异步创建订单 await mqClient.sendToQueue(seckill_order, JSON.stringify({ productId, userId })); return { success: true, msg: 排队中请稍候查看订单 }; };这里有个容易忽略的性能优化点DECR之后如果返回负数再INCR回去虽然逻辑上没错但如果大量请求同时挤到库存为 0 的临界点会出现大量的无效INCR操作。优化方式是维护一个seckill:finished:{productId}的标记位库存归零时立即设置这个标记后续请求先检查标记再决定是否执行DECR这样可以把无效的DECR挡在前面。3.5 消息队列异步建单削峰填谷的设计逻辑那用户点击秒杀之后的订单在哪里创建不是在用户请求的同步链路里直接落库而是通过消息队列异步处理。这一段是这个项目里价值最大的设计点。流程是tryDeductStock在 Redis 扣减成功之后把{ productId, userId }发给 RabbitMQ 的seckill_order队列前端立即收到排队中的响应。后端有一个消费者进程监听这个队列拿到消息后执行完整的建单逻辑插入订单表、插入订单明细表、扣减数据库库存、记录日志。整个过程对用户透明用户可能在下一秒主动查询订单时才发现订单已生成。好处显而易见假设秒杀瞬间有 10000 个请求如果不做异步10000 个请求会全部打到数据库MySQL 的并发写入能力很快就扛不住了。但通过消息队列我们可以控制消费者并发数比如一次只消费 100 条消息让数据库始终在一个可承受的负载之下。这就是削峰填谷的核心思想——高峰期的请求先存进队列让服务端按自己的节奏慢慢处理。// server/jobs/orderConsumer.js exports.consume async () { const channel await mqClient.createChannel(); await channel.assertQueue(seckill_order, { durable: true }); channel.prefetch(10); // 每次最多取 10 条消息防止消费者堆积 channel.consume(seckill_order, async (msg) { try { const { productId, userId } JSON.parse(msg.content.toString()); await createOrder(productId, userId); channel.ack(msg); } catch (error) { // 记录失败日志便于排查根据业务决定是否重新入队 await logger.error(订单创建失败, error); channel.nack(msg, false, false); } }, { noAck: false }); };这个消费者代码里有两个细节值得反复体会第一个是prefetch(10)。如果不设置 prefetchRabbitMQ 会一次性把队列里的几百条消息全部推给消费者消费者处理不过来消息会在本地堆积内存压力变大。设置了 prefetch 之后消费者端在任何时刻最多只有 10 条消息在途这是最经典背压控制手段。第二个是手动 ack 机制noAck: false。如果消费者处理订单时进程崩溃了消息不会丢失因为 RabbitMQ 会在消费者重连后重新投递。我在实际跑项目时故意 kill 掉消费者进程来测试发现消息确实没有丢这让我对异步建单的可靠性有了很直观的认知。3.6 幂等处理与重复下单异步化带来了一个新问题用户点了秒杀按钮Redis 库存扣减成功如果消费者处理过程中出现异常导致消息重投就可能出现同一个用户创建了两个相同订单的情况。这就是幂等性问题。这套项目在订单表上做了一个很关键的约束UNIQUE KEY uk_user_product (user_id, product_id)重复插入时数据库会直接报错消费者根据错误类型判断是重复下单还是真正的失败避免对用户的重复扣款。此外在前端入口也做了防重复点击兜底小程序端的按钮在点击后立即进入 loading 状态并禁用防止用户因连点而发出多个请求。但前端防重不可靠脚本用户直接调接口就绕过了所以后端一定要有幂等约束。4. 部署与压测从代码到可演示的完整过程4.1 本地环境准备与中间件启动拿到源码之后第一步不是看代码而是先把环境跑起来。这套项目依赖 MySQL、Redis、RabbitMQ 三个中间件。如果本地已经装了 Docker那最快的方法是直接使用项目自带的docker-compose.yml# 在项目根目录执行 docker-compose up -d这份 compose 文件会拉取三个镜像并创建好容器同时把端口映射到宿主机。如果没有用 Docker也可以手动安装MySQL 5.7、Redis 6.x、RabbitMQ 3.9。需要注意时区问题Redis 和 MySQL 里涉及 expire 的逻辑都依赖服务器的本地时间建议把所有服务包括最后跑 Node.js 的本机都统一为同一个时区避免出现秒杀结束了 Redis 中的库存还没释放的诡异现象。启动 Node.js 服务之前先看一下server/config/index.js里面配置了数据库连接账号密码、Redis 地址、RabbitMQ 地址、JWT 密钥等。默认配置都是localhost 默认端口一般不需要改但 JWT 密钥一定要改否则生产环境不安全。然后执行cd server npm install node app.js如果在启动时遇到connection to Mongodb failed或者Cannot find module mysql2这类报错先检查npm install是否成功执行完毕再看package.json里的依赖是否都写全了。我在实际运行时还踩过一个坑nodemon没装就用了npm run dev脚本一直报错找不到命令。解决办法是全局装一个nodemon或者把启动脚本改成node app.js。4.2 前端小程序的配置与连接小程序端的配置集中在miniprogram/utils/config.js或miniprogram/app.js中主要是把请求的 baseURL 指向本地后端的地址。因为是本地开发地址一般设为http://localhost:3000。但这里有一个新手必踩的坑微信开发者工具默认不允许请求http://开头的非安全域名需要在工具右上角详情 → 本地设置中勾选不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书。如果你不勾选所有请求都会被拦截页面白花花一片看起来就像后端没启动。如果你是用手机真机调试那 baseURL 不能写localhost要改成你电脑在局域网内的 IP比如http://192.168.1.100:3000。同时手机和电脑需要在同一 Wi-Fi 下。这一步我调试了挺久才理解因为小程序端的请求失败几乎不显示具体原因只能靠后端的访问日志来确认请求有没有到。4.3 用压测工具观察系统表现部署完成之后我强烈建议你亲手做一次压测。压测工具可以用 Apache Bench简称 ab也可以用更专业的 wrk。下面是我测试的完整过程可以直接照着跑先准备一个秒杀商品。在 MySQL 中插入一条带秒杀库存的商品数据然后在 Redis 中执行SET seckill:stock:1 10把库存加载进去。接着通过小程序页面进入秒杀详情页确认倒计时、按钮状态都正常。最后在命令行执行ab -n 10000 -c 500 -T application/json -H token: 你的JWT_token \ -p post_data.json http://localhost:3000/api/seckill/1其中-n 10000表示总请求数一万-c 500表示并发 500。-p 指定 POST 请求体。结果会让你非常直观地看到限流和 Redis 预扣的效果一万个请求中真正到达 RedisDECR阶段的只有很少一部分其余都在时间窗、用户限流、库存预检这几层被拦截掉了。这也是秒杀系统以少量资源承接海量流量的典型体现。如果做压测时不带 token也就是客户端使用了未鉴权的接口那么你会在日志中看到大面积的 401 错误这说明 JWT 鉴权中间件在工作。5. 常见问题与面试官最爱追问的架构细节5.1 秒杀场景中的典型问题速查表我在反复跑这套代码的过程中积累了一张问题排查表里面很多都是新手会遇到的整理出来给你参考问题现象可能原因排查步骤与解决方案前端请求一直加载中baseURL 配置错误或后端未启动先 curl 一下后端接口确认能通检查手机调试时是否用了 127.0.0.1秒杀接口返回系统繁忙Redis 连接失败或内存不足检查 Redisredis-cli ping查看 Node 进程的内存使用ps aux | grep node库存扣减成功但订单未生成RabbitMQ 消费者未启动启动消费者进程查看队列中消息数量rabbitmqctl list_queues秒杀结束后库存仍为负数库存回写逻辑缺失确认定时任务syncStockToDB是否执行检查 Redis 中的 value 是否有负数同一用户被重复限制 5 秒滑动窗口边界误伤将限流时间改为 6 秒阈值或改用 Redis 的 Lua 脚本做窗口控制JWT 过期导致接口集体 401token 有效期太短将expiresIn调整为 2 小时或在前端拦截 401 后自动刷新 token5.2 高并发架构的迭代方向从单体到分布式这套项目目前是单体应用加几个独立中间件这是学习阶段最合适的形态。但面试官看到你做过秒杀系统十有八九会追问一个承接性问题如果要把它改造成真正的分布式架构你会怎么做我个人认为有几个方向值得提前想清楚第一个方向是把用户服务和秒杀服务拆成两个独立服务。用户服务负责登录、鉴权、用户信息查询秒杀服务只负责处理秒杀相关的下单逻辑。两个服务之间通过 HTTP 接口或者消息队列通信。这样拆的好处是秒杀服务的流量不会影响到用户服务其他接口的稳定性。第二个方向是把库存扣减升级为真正的分布式锁方案并配合 Lua 脚本保证原子性。当前实现DECR INCR 恢复在单机 Redis 下没问题但如果在 Redis Cluster 环境下你要考虑 key 的 slot 分布。使用 Redis 官方推荐的 Redlock 算法或者使用 etcd 作为分布式锁存储在可靠性和一致性上会更强。第三个方向是引入服务网关层。使用 Nginx 或 Kong 做反向代理与限流把 IP 维度的限流、UA 识别、黑名单拦截放在网关层把后端从这些脏活中解放出来。网关层的限流效率比应用层高很多因为它在进入 Node 进程之前就直接拒绝了请求。5.3 压测数据与性能调优心得我在这套项目上做了一些真实运行数据可以作为你调优的参照基线。在一台配置比较普通的 4 核 8G 云服务器上单实例 Node.js 本地 Redis 本地 RabbitMQ不加任何优化的情况下秒杀接口的 QPS 大约在 2500 左右加了用户维度和全局的限流之后真实下单请求被控制在 500 QPS 内系统整体很稳把数据库从单机切换到读写分离之后异步订单落库对主库的压力进一步下降。如果你在压测过程中发现 CPU 占用很高重点检查 Redis 是否开启了 AOF 持久化因为 AOF 的everysec策略在写入密集场景下会占用大量磁盘 IO。另外一个隐藏瓶颈是 RabbitMQ 的连接数每个消费者会保持一个 TCP 长连接如果你开了很多消费者实例连接数会线性增长需要在上线前确认 rabbitmq 的channel_max配置足够。我还踩过一个值得注意的坑Node.js 单实例下的cluster模块问题。压测时我用pm2 start app.js -i 4启动了 4 个进程模拟多核负载结果发现 Redis 的DECR在多个进程同时访问时是没问题的因为 Redis 本身是单线程模型原子性不受影响。但项目里的限流 key 你设计时要小心如果一个用户的多个请求分别落在了不同进程上而限流 key 是按进程内缓存来做的比如用 JS 变量计数那就会造成限流失效。正确做法是限流状态必须放在 Redis 或者共享存储中。这套项目把限流也放在 Redis 里恰好绕开了这个问题这也再一次印证了一切状态尽量外置的分布式架构设计原则。6. 源码阅读路线与先跑起来再深入的建议很多初学者拿到源码后的第一个问题是这么多文件我该从哪里看起我给一个我自己的阅读顺序建议它不是按目录顺序而是按请求链路顺序。因为源码最终是为了服务一条完整的用户请求顺着请求走一遍整个系统的脉络自然会浮现。先从miniprogram/pages/detail开始看秒杀页面的 JS 代码找到点击按钮后调用的request函数。然后顺着这个请求找到后端的routes/seckill.js看它挂了哪些中间件——限流、鉴权、参数校验。再进到controllers/seckillController.js看它调用了哪些 service 方法。接着看services/seckillService.js这里你会看到 Redis 扣减和消息队列的调用。最后去看jobs/orderConsumer.js这是系统下沉后的终点也是订单真正落库的地方。这个顺序走完你对秒杀主流程的认知就串起来了。之后再回头补两个分支登录鉴权流的authController.js和库存预热任务的preloadStock.js。这两个分支一个负责请求从哪来一个负责库存从哪来。补完之后整个系统的全景图就完整了。代码读完之后动手做三件小事每一件都能加深理解第一件把全局限流的阈值从 500 改成 50观察压测时错误码的变化第二件把 RabbitMQ 消费者停掉再发起秒杀观察前端是否能正常收到排队中但订单始终不生成第三件手动在 Redis 里把库存 key 删掉然后发起一次秒杀请求观察系统是否会报错还是自动从数据库加载库存。这三个实验做完你对缓存与数据库如何配合队列在系统中的位置会有远超看十篇文章的认知。这套源码的价值不在于它用了多少高深的框架而在于它把秒杀系统的所有关键问题都浓缩到了一套可以跑通、可以修改、可以扩展的代码里。读源码的最高境界不是把每一行都背下来而是能够理解每一处设计都在解决什么问题、如果换成你会怎么解决、你的方案和作者的方案孰优孰劣。带着这个思路去读你收获的将不仅仅是一套秒杀系统而是面对高并发问题时的一种思维框架。最后再分享一个我在学习过程中的切身体会如果你真想通过这个项目掌握高并发架构不要停留在它给的现成实现里务必试着把它改坏——比如故意去掉消息队列直接在同步链路里建单然后在压测时观察接口延迟和数据库负载的对比数据。这些错误实验带来的直观感受往往比跑通一个正确流程更能训练你的系统设计直觉。毕竟秒杀系统的核心从来不是把某个接口写好而是当流量浪潮拍过来时你的每一个组件都知道自己该做什么、做到什么程度、做不完时又该怎么兜底。