一、前言
秒杀是电商系统中最典型的高并发场景,也是营销模块里技术复杂度最高的功能之一。它涉及的不只是“把价格改低、把时间限定住”这么简单——库存防超卖、请求削峰、订单归属、平台审核,每一个环节都需要独立的设计。
LikeShop 的秒杀模块在架构上有一个关键特点:多商户版的秒杀不是商家“想上就上”,而是需要平台端和商家端协作完成。平台端负责设置秒杀时段和审核商品,商家端负责提交商品和设置秒杀价,审核通过后商品才能上线。这个协作机制决定了秒杀模块的配置链路和执行链路是分开的。
这篇文章就从多商户版的秒杀业务设计出发,把配置链路和执行链路完整拆开讲清楚。
二、多商户秒杀的业务设计:平台搭台,商家唱戏
LikeShop 多商户版的营销活动分为两大类:一类是平台设置,一类是商家设置、平台监管。限时秒杀属于后者——商家设置,平台监管。
这意味着秒杀活动的完整流程分为两个步骤:步骤一,平台端设置秒杀时段;步骤二,商家提交参与秒杀的商品,平台审核通过后方可上线。
2.1 平台端:设置秒杀时段
平台端运营人员在后台进入〖营销〗→〖限时秒杀〗→〖新增秒杀时段〗,填写开始时间点和结束时间点即可创建一个秒杀时段。
这里有一个值得注意的设计:秒杀时段不选择具体日期。因为秒杀活动是每天循环开展的,设置好时段后,每天的这个时间段都会自动开启秒杀活动。比如设置“14:00-16:00”,那么每天下午2点到4点都会有秒杀活动。
时段可以删除,但删除时段后该时段下的所有秒杀商品会被全部移除,操作需要谨慎。
2.2 商家端:提交秒杀商品
商家在商家后台提交需要参与秒杀的商品,所有规格都要填写秒杀价。如果某个规格被移除,则该规格按正常销售价出售。
商家提交后,商品进入“待审核商品”状态,等待平台审核。
2.3 平台端:审核秒杀商品
秒杀商品汇聚了各个商家提交的商品,分为四种状态:审核通过(秒杀中)、审核通过(非秒杀中)、待审核商品、审核拒绝商品。
| 状态 | 含义 |
|---|---|
| 审核通过(秒杀中) | 商品正在参与秒杀活动 |
| 审核通过(非秒杀中) | 商品已通过审核,但当前不在秒杀时段内 |
| 待审核商品 | 等待平台审核 |
| 审核拒绝商品 | 平台审核不通过,商家可重新编辑后提交 |
“审核通过(秒杀中)”和“审核通过(非秒杀中)”的区分,是因为秒杀时段每天循环但商品参与秒杀有活动日期。比如商品参与“10号到15号下午2点”的秒杀,10号到15号下午2点期间是“秒杀中”,其他时间是“非秒杀中”。
审核拒绝的商品,商家可以重新编辑后再次提交审核。
2.4 秒杀商品的约束
LikeShop 对秒杀商品有几条明确的约束:
第一,秒杀活动里面的商品不参与分佣。如果平台开启了分销,秒杀商品不会产生分销佣金。
第二,进行中的活动需要暂停后才能移除商品。如果秒杀活动正在执行,不能直接移除已参加活动的商品。
第三,秒杀商品可以再参与其他营销活动。秒杀不排斥其他营销玩法,但要注意叠加规则。
三、配置链路:数据如何从后台写入数据库
3.1 秒杀活动配置的接口
秒杀活动的编辑接口为:
POST /adminapi/marketing.seckill/edit请求参数包括:
| 参数名 | 类型 | 说明 |
|---|---|---|
| id | string | 活动ID(编辑时必填) |
| name | string | 秒杀活动名称 |
| min_buy | int | 起购限制:0=不限制 |
| max_buy | int | 每单限制:0=不限制 |
| is_coupon | int | 是否能用优惠券:0=否,1=是 |
| goods | array | 参与活动的商品列表 |
min_buy和max_buy是秒杀的核心控制参数。min_buy控制单笔订单最少购买件数,max_buy控制单笔订单最多购买件数。这两个参数在订单创建时会被校验,防止用户一次性抢购过多库存。
3.2 秒杀商品的数据组织
秒杀商品在数据库中的组织方式遵循 LikeShop 营销模块的通用模式:活动主表 + 商品关联表。
活动主表存储秒杀活动的基础信息(名称、时段、限制参数),商品关联表存储参与活动的商品信息(商品ID、规格ID、秒杀价、秒杀库存)。这种设计的好处是:一个秒杀活动可以关联多个商品,商品也可以参与多个不同的秒杀活动。
多商户版的关键字段是shop_id。商家提交的秒杀商品记录中必须包含shop_id,平台端审核时通过shop_id识别商品归属的商户,用户在秒杀页面看到的商品也需要按商户维度展示。
四、执行链路:用户点击秒杀后发生了什么
4.1 秒杀请求的完整流程
用户在秒杀页面点击“立即抢购”后,系统的处理流程如下:
用户点击秒杀 ↓ ① 校验秒杀活动是否在有效时段内 ↓ ② 校验用户购买数量是否满足 min_buy / max_buy 限制 ↓ ③ Redis 预减库存(原子 DECR 操作) ↓ 库存不足 → 返回“已售罄” 库存充足 → 继续 ↓ ④ 生成秒杀订单,推入异步队列 ↓ ⑤ 立即返回“排队中”给用户 ↓ (异步消费者) ⑥ 写入数据库,扣减真实库存 ↓ ⑦ 更新订单状态4.2 秒杀时段校验
秒杀活动的时段是每天循环的,所以校验逻辑需要判断当前时间是否落在秒杀时段内,而不是判断具体日期。
// 伪代码示意$now=time();$currentTime=date('H:i:s',$now);$inSeckillTime=SeckillTimeModel::where('start_time','<=',$currentTime)->where('end_time','>=',$currentTime)->find();if(!$inSeckillTime){return['code'=>0,'msg'=>'当前不在秒杀时段内'];}4.3 购买数量校验
min_buy和max_buy的校验发生在订单创建阶段:
// 伪代码示意$num=$params['num'];if($activity['min_buy']>0&&$num<$activity['min_buy']){return['code'=>0,'msg'=>"最少购买{$activity['min_buy']}件"];}if($activity['max_buy']>0&&$num>$activity['max_buy']){return['code'=>0,'msg'=>"最多购买{$activity['max_buy']}件"];}4.4 库存扣减的多层防护
秒杀库存的扣减是防止超卖的核心环节。LikeShop 采用多层防护机制。
第一层:Redis 预减库存。利用 Redis 的原子性操作快速处理库存预扣,在内存中判断库存是否充足。Redis 的单线程模型天然保证了DECR操作的原子性,多个请求同时到达时不会出现超卖。
// 伪代码示意$stockKey="seckill:stock:{$activityId}:{$itemId}";$currentStock=Redis::decr($stockKey);if($currentStock<0){Redis::incr($stockKey);// 回滚return['code'=>0,'msg'=>'已售罄'];}第二层:数据库层原子扣减。异步消费者写入数据库时,通过WHERE stock >= num的条件做最终校验:
$affected=Db::name('seckill_goods')->where('id',$itemId)->where('seckill_stock','>=',$num)->setDec('seckill_stock',$num);if(!$affected){Redis::incr($stockKey);// 数据库库存不足,回滚 Redisreturnfalse;}第三层:订单超时回滚。用户下单后未在规定时间内支付,定时任务会扫描超时订单,将已扣减的库存恢复。
4.5 多商户版的库存隔离
多商户版中,每个商家的商品库存独立管理。Redis 预扣库存的 key 必须包含商户标识:
seckill:stock:{merchant_id}:{activity_id}:{goods_id}:{item_id}这样做的好处是:不同商家的秒杀库存互不影响;平台可以做全局的秒杀 QPS 限制;商家后台可以独立查看自己商品的秒杀库存状态。
五、高并发保障:Redis + 异步队列
5.1 为什么需要异步队列
秒杀场景下,瞬时涌入的订单创建请求如果全部同步写入数据库,数据库会直接崩溃。LikeShop 通过ThinkPHP Queue实现异步削峰。核心思路是将下单流程中“可以异步化”的步骤从主链路中剥离。
库存校验 → 下单触发 → 订单落库这三个步骤被解耦。前端用户的体验是“秒级响应”——Redis 快速判断库存是否充足并返回结果,真正的订单落库和后续操作交给消息队列异步处理。
5.2 异步队列的部署
队列消费者进程需要通过 Supervisor 守护:
php think queue:listen在多商户场景下,建议按商家维度拆分队列,或者在任务数据中携带商户标识,消费者按商户维度做限速处理。这样某个商家的秒杀活动流量过大时,不会阻塞其他商家的订单处理。
六、多商户秒杀与单商户秒杀的关键差异
| 维度 | 单商户版 | 多商户版 |
|---|---|---|
| 商品来源 | 商家自己发布 | 商家提交,平台审核 |
| 库存隔离 | 不需要 | 必须按商户维度隔离 |
| 审核机制 | 无 | 平台审核通过后才能上线 |
| 分佣 | 可能参与 | 秒杀商品不参与分佣 |
| 结算归属 | 统一结算 | 按商户独立结算 |
| 订单拆分 | 不需要 | 按店铺拆单 |
审核机制是多商户版最核心的差异。商家提交的秒杀商品必须经过平台审核,审核通过后商品才能参与秒杀活动。技术实现上,审核通过是触发库存预热的条件——如果商品还在待审核状态,不应该被加载到 Redis 中。
七、二开避坑清单
坑一:Redis 预扣没有回滚。如果 Redis 预扣成功后数据库写入失败,必须回滚 Redis 的库存扣减操作,否则会出现“Redis 显示无库存但数据库还有库存”的不一致。
坑二:多商户库存 key 没有商户标识。如果不同商家的秒杀商品共用一个 Redis key,会出现 A 商家的库存被 B 商家的用户抢走的情况。
坑三:秒杀时段校验用了日期判断。秒杀时段是每天循环的,校验时应该判断“当前时间是否在时段内”,而不是判断具体日期。
坑四:审核通过的商品没有触发库存预热。如果商品审核通过了但 Redis 中没有对应的库存数据,用户点击秒杀时会因为“库存不足”而失败。
坑五:异步队列消费者没有常驻。用php think queue:listen手动启动的消费者在终端关闭后就会停止。生产环境必须用 Supervisor 守护进程。
八、总结
LikeShop 多商户版秒杀模块的完整链路可以概括为两条线:
配置链路:平台端设置秒杀时段 → 商家端提交秒杀商品 → 平台审核 → 审核通过后商品上线。秒杀时段每天循环,商品参与秒杀有活动日期限制。
执行链路:用户点击秒杀 → 校验时段和购买限制 → Redis 预减库存 → 推入异步队列 → 消费者写入数据库。库存扣减采用 Redis 预扣 + 数据库原子扣减 + 定时任务回滚的三层防护。
多商户版需要额外守住三条底线:库存 key 按商户隔离、审核通过才触发预热、秒杀商品不参与分佣。把这三点处理好,秒杀模块就能在高并发下保持稳定。