十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

i茅台背后的预约式分发系统:高并发下的公平分配如何设计

i茅台背后的预约式分发系统:高并发下的公平分配如何设计 你身边大概率有人在讨论i茅台。有人把它当成每天的习惯性打卡有人只在“申购成功”通知弹出来的时候才愿意打开一次。这些讨论很容易把一款产品归结成两个词运气、中签率。但如果站在系统设计和软件开发的角度去观察这里其实有另一层内容值得拆。i茅台并不是传统电商里那种“把人拉进来、靠转化率驱动”的货架它把极度稀缺的商品放上数字化渠道让有限供给通过一套规则来完成分配。高峰期的访问压力、强实名限制、区域配额、异常流量对抗、结果公示、后期履约这些因素叠加在一起才构成了它真正的复杂度。我更愿意把这类产品定义为“预约式稀缺资源分发系统”。这不是从官方介绍里抄出来的定义而是基于业务形态做出的判断。一旦接受这个角度你会发现很多产品细节都能解释得通它没有像普通商城那样让人随时加购结算因为它要管理的不是库存周转率而是公平分配和全过程可追溯。这篇内容不讨论具体申购数据也不聊规则漏洞只从工程视角回答一个问题一个预约型系统到底难在哪里又应该如何从零开始设计和排查。1. i茅台真正在做的不是把茅台酒搬上线上货架很多传统品牌搭App思路是先做一个线上商城再把经销商体系里的商品搬上去。i茅台的产品逻辑不完全是这样。从公开形态看它更像是在做一个“官方直连 配额预约 线下履约”的组合。传统白酒销售链路通常是厂家生产后交给经销体系再通过终端门店触达消费者。品牌方对真实用户了解有限谁在买、区域热度如何、用户长期偏好是什么信息都隔着好几层。建立一个官方移动入口后这些数据开始直接回流实名身份、设备信息、预约习惯、支付行为、提货记录。对这些数据的洞察反过来会指导生产计划和配额投放。但真正让它和其他电商平台区分开的是分配方式。1.1 传统电商处理的是效率问题预约制处理的是分配问题常规电商商品供给充足核心优化目标通常是转化链路搜索、详情、加购、支付、物流每个环节提升一点点整体销售额就能上涨。i茅台这种场景不太一样。商品数量有限、单件价值高、用户数量远大于可分配数量核心矛盾就不是“怎么把东西卖出去”而是“给谁、不给谁、用什么规则让结果显得公平合理”。如果在大促期间直接做抢购结果很可能是外挂、脚本和黄牛系统占据优势普通用户每次进入都只看到“已售罄”。于是产品的交互形态变成了预约制用户不直接付款买货先提交申购意向到结果公布时间再确定是否被选中。这种设计把“手速竞争”转化为“规则竞争”让结果不完全依赖点击那一刻的速度。这同时也让技术系统的核心任务发生变化。系统不用在提交那一刻处理极致的库存扣减但必须保证一次提交可靠落库、参与资格可校验、抽签结果可复核、中签后的订单可流转可追踪。1.2 从一个渠道变成了一个可解释的分配入口消费侧用户看到的是每天能不能申购成功。供给侧的公司看到的是一套可以解释的配额投放过程。每天放量多少、每个省份或城市分到多少、给哪些门店或自提点分配多少这些都是配额问题。配额不只是一个数据库字段它决定了后续履约能不能闭环。如果一个用户中签了但系统在他所在区域找不到可提货的库存那就是最糟糕的用户体验。所以i茅台这类产品在后端设计上天然需要一个比普通商城更严谨的结构区域维度要能拆库存活动维度要能区分场次用户维度要能限制重复参与。这三者缺一不可。这里可以做一个简单总结判断一个系统是不是真正的预约型系统不要只看前端有没有“预约”按钮要看它是否有完整的申购单、抽签、配额、结果发布、支付和提货状态机。2. 稀缺商品的预约制和普通秒杀是完全两类系统很多人会把i茅台的瞬间峰值和双十一秒杀类比。但如果真要设计一套同类型系统直接抄秒杀方案是会出问题的。2.1 秒杀考的是抗压预约制考的是规则与稳定秒杀场景的特点是QPS极高但商品数量极少系统需要在极短时间内判断谁能拿到库存。因此Redis预扣库存、队列削峰、限流降级是常见手段核心目标是“不超卖、扛得住”。预约申购也有瞬时峰值。每天固定时间窗口开放参与时大量用户会在同一秒集中提交。但它的后续逻辑更重每条申购记录要保存下来与用户信息、活动场次、区域配额绑定等待时间点再做抽签。也就是说压力不在“交个易”而在“存一单、筛一批、抽一次、发一轮通知”。这要求数据库层存储必须可靠不能因为瞬时流量就把记录丢在内存里。任何丢失都可能演变成用户投诉因为用户已经实名提交系统需要保证“提交过就是提交过”。2.2 配额、区域、投放和透明性变成了硬约束普通商品在库存足够时可以由推荐系统决定用户看到什么。预约型商品不能这样做。用户参与前必须先确定他属于哪个配额池这通常由地域和活动场次决定。从工程层面看这意味着系统存在多个维度的约束条件一个用户同一天是否只能参与一次申购每个省份、城市是否有独立配额不同提货点是否承接不同数量每场活动的投放总量是否会动态调整未支付的中签单是否会把配额退回后续场次这些条件需要在提交申购时实时校验又需要在抽签时再次核对。如果只在前端校验很容易被绕过如果每次都在数据库里读全量数据性能又会成为瓶颈。常见做法是拆分两次校验提交时做一次轻量校验比如用户资格、活动开启状态、区域是否允许参与抽签前再做一次重量校验比如是否重复、配额是否有效、用户是否在合规名单内这样既保证响应速度又保证结果严谨。2.3 防异常流量要分层设计不是一个验证码能解决的事凡是高价值、低供给、强需求的商品都会吸引异常流量。这个斗智斗勇的过程不能只靠一套验证码完成。从防御端看至少有三层需要投入身份层。实名认证让批量注册成本提高也让每个账号背后有一个相对稳定的自然人。设备与网络层。同一设备频繁切换账号、模拟器特征、代理IP聚集、Wi-Fi环境异常这些都是风险信号。行为层。正常参与者在申购页面的停留时间、点击次数、操作路径会有一个分布范围。自动化脚本通常呈现出高度规律性容易被行为模型捕获。这些风控规则不能简单一刀切否则会误伤真实用户。更合理的思路是分层打分把用户分为可信、可疑、高风险几类不同风险等级对应不同处理策略。比如高风险账号允许进入申购流程但中签后的支付和核销环节需要更强验证。注意这里讲的是防御设计思路不是教你如何绕过风控。真正有运营价值的系统会把异常流量控制目标设定为“减少误伤、提高攻击成本”而不是追求把所有风险信号全部拦截。3. 一次申购背后至少需要五个模块稳定协作用户看到的交互链路其实很短注册、实名、提交申购、等结果、支付、提货。但支撑这条链路的服务要比页面所呈现的复杂得多。3.1 从用户点击到申购单落库链路并不短当用户完成一次申购点击后台至少要做以下几件事校验用户是否实名是否满足参与条件确认活动处于开放状态识别用户归属的区域或配额池检查该场次是否还有可申购的名额写入一条申购记录标识这条记录是否重复这一串操作中任何一步响应超时用户都可能重新点击于是后台必须处理同一个用户发来的两条相同请求。如果系统不做幂等保护就可能出现同一个人同一场申购了两条记录的情况。一个简单有效的方案是使用唯一约束用户、活动场次、申购日期三个字段联合唯一。无论用户点击多少次后面的插入请求都会被数据库拦截系统只保留第一次提交成功生成的申购单号。-- 常见幂等约束示例同一用户同一活动同一天只能有一条有效申购单 CREATE UNIQUE INDEX uk_user_activity_date ON lottery_order(user_id, activity_id, order_date);3.2 结果公布不只是数据库改一个状态到了结果公布环节系统要做的事也不只是把“待抽签”改成“中签”或“未中签”。首先抽签任务需要在某个时间点对已截止的申购单批量处理。这个任务要判断参与用户是否符合配额池过滤掉无效单再生成中签结果。其次结果需要通知到用户。通知渠道可能是App推送、短信或小程序订阅消息。通知过程允许失败所以还需要重试机制。中签和未中签用户看到的反馈应该完全不同。未中签用户可能需要一句“下次继续参与”的提示中签用户则需要明确的支付入口和有效期提醒。从系统设计看结果公布不是“一次性任务”而是一组异步任务的组合批量抽签任务执行结果写入中签表未中签状态批量更新中签用户进入待支付池消息通知写入队列等待发送未支付超时扫描任务开始对这批中签单计时3.3 状态机设计是预约单的“交通规则”如果把申购单看成一辆车状态机就是红绿灯和车道线。缺少状态机约束的系统很容易出现“支付成功但订单状态没更新”“中签后重复支付”这类混乱。一个典型的预约申购单状态流转可以表达为状态含义可流转到已提交用户申购成功待抽签、已取消待抽签等待抽签任务处理中签待支付、未中签中签待支付已获得购买资格已支付、超时取消、主动取消已支付用户完成支付待提货、退款中、已完成待提货可到指定地点提货或等待配送已完成已完成提货或收货完成终态每一步状态变化都对应一次业务动作也应在日志中留下操作人、操作时间和来源。用户咨询时客服或技术排查可以从状态变更记录中看出这张申购单在哪个环节断了。4. 抽签、幂等、超时释放最值得抠的三个支点预约型系统的开发中有三块细节最容易被低估也最容易在后期引发线上问题。4.1 公平抽签不能只依赖一行 random如果直接用系统当前时间作为随机数种子结果不可审计也很难向用户解释。真正的抽签系统需要做到“事后可复核”而不只是“随机产生幸运儿”。这里有一个通用思路from random import Random # 伪代码抽签结果生成逻辑示意 def run_lottery(activity_id, date, quota, candidates): candidates sorted(candidates) rng Random(f{activity_id}_{date}_daily_seed) winners rng.sample(candidates, min(quota, len(candidates))) # 持久化抽签种子、候选列表版本、中签结果 save_lottery_log(activity_id, date, winners, rng.getstate()) return winners这里的关键不是用哪个随机函数而是抽签前有确定的候选池快照随机种子有固定且可审计的来源种子、候选池版本、抽签时间和结果都需要持久化保存结果产生后不在运行时临时改动配额如果临时给某个地区增加配额应该重新执行一次校验和日志记录而不是在已生成结果上手工追加。原因很简单任何看起来“人为调整”的操作都会在后续引发信任问题。4.2 防重复提交幂等是所有网络场景的地基预约场景最大的特征是用户会焦虑焦虑会带来大量重复点击。移动端网络不稳定时用户也可能以为没提交成功于是在同一时间窗口反复提交。系统层面要尽可能让“重复请求”不在业务层产生副作用。做法包括前端按钮置灰提交后禁止再次点击后端接收请求时生成幂等键已存在则直接返回原结果数据库层建立唯一索引作为最终防线网关层对同一个用户同一场活动限流幂等不是哪个开发语言特有的概念而是一种全局设计意识。它要求每一个可能被重复调用的接口都返回第一次执行的结果而不是再执行一次。4.3 未支付释放用户放弃之后配额要能回流中签用户不支付是预约型业务必然存在的现象。有人只是随手参与有人中签后放弃有人错过支付时间。如果系统不处理这些超时状态配额会越积越多账面与实际可售数量不一致。常见做法是中签结果公布后给用户一段时间完成支付。超时未支付的中签单进入取消态配额回补到后续活动池。实现方案可以是延迟队列也可以是定时扫描任务。扫描任务的设计有几个注意点不要扫描整张表尽量按状态和截止时间字段缩小范围单次处理量要限流避免大量超时单同时触发配额的并发回补回补库存需要和支付关闭处于同一事务避免出现“支付关闭了但库存没回来”用户支付正处在临界点时系统要利用分布式锁或状态对比避免“关闭了又支付成功”实际项目中定时任务未必需要每隔几秒跑一次。更稳妥的方式是针对支付截止时间建立索引通过延迟队列触发二次确认把精确释放和兜底扫描结合起来。5. 消息延迟、支付成功但状态不变怎么按链路排查预约系统一旦进入运营阶段最常出现的问题不是“系统崩了”而是各种状态不一致。用户侧表现相似根因可能相差很远。5.1 不要一上来就怀疑数据库先分清现象发生在哪一层以下排查顺序更合理先看现象。是提交时无响应还是提交后查不到记录还是中签结果没收到还是支付后状态没变不同现象对应完全不同的模块。再看网络链路。客户端是否有重试服务网关是否限流是否存在连接超时或读超时。再看业务日志。申购单号是否生成状态机是否记录到对应变更异步任务是否异常退出。再看依赖服务。通知渠道延迟、支付回调延迟、缓存和数据库是否一致。最后再查历史数据。是否刚好在旧版本代码期间提交是否因为规则调整产生了脏数据。很多团队在状态不一致时第一反应是去改数据库字段这其实风险很高。正确流程是先用唯一标识把整个请求链路日志拉出来找到断点在哪。5.2 用 traceId 把一次申购从入口串到尾一次申购可能经历网关、业务服务、消息队列、数据库、支付回调等多个节点。如果每个节点各记各的日志事后很难还原。生产环境通常会引入链路追踪在请求开始时生成一个 traceId并在整个调用链中透传。排查问题时只需要拿着用户的手机号或申购单号反查出对应的 traceId就可以把日志聚合起来看。对于没有完整链路系统的项目也要在业务日志中约定一个固定字段比如申购单号。状态变化、请求入口、回调通知都必须带上这个字段。问题出现时通过申购单号搜索日志能快速缩小范围。5.3 状态不一致的根源通常是缺少补偿机制支付成功但订单状态未更新常见的根因是支付回调丢失。App或服务端在用户支付完成后没有能主动与支付渠道核对也没有定时补偿任务于是状态停在待支付。修复方式不是简单地“再手动改一次数据库”而是要在支付模块和订单模块之间建立对账机制。常见的做法包括支付回调接口做幂等重复通知不会导致重复发货服务端提供主动查单接口前端在支付结果页主动查询定时任务扫描“已支付但订单状态超时未更新”的记录调用支付渠道确认真实状态如果确认已支付则补偿更新订单状态这套机制的核心是“最终一致”。支付结果虽然可能延迟到达但通过主动查询和定时补偿系统最终会收敛到正确状态。6. 如果从零复刻一套预约申购系统建议这样落地判断一种剖析是否有价值还要看它能不能指导新的开发。若现在要做一个预约申购类业务从零开始我认为更应该坚持“最小闭环优先”的思路。6.1 先做最小闭环不要第一版就接入十几套风控很多团队一开始就规划了几十个微服务又是规则引擎又是设备指纹。实际上一个预约型系统的最小闭环并没有那么复杂用户注册并实名认证管理员创建活动配置开放时间和配额用户在时间内提交申购系统生成申购单定时任务执行抽签公布中签结果通知用户中签用户支付生成提货信息线下核销或物流发货状态更新为已完成先把这七步跑通再考虑防黄牛、防重复点击、区域复杂策略。否则大型风控系统还没上线业务流程就已经被拖住了。6.2 从放量到灰度参数和业务规则要一起打磨如果新业务准备上线我不建议直接在全部城市开放。更合理的方式是选择一个小范围先跑验证流程和后台操作是否顺畅小范围时人工介入处理特殊问题确认业务规则没有歧义逐步扩展配额同时观察数据库压力、通知成功率、支付转化率每次扩展前至少做一次压测避免瞬时峰值导致服务不可用参数方面可以这样设定初始值# 以下是通用初始值设计上线前需根据压测结果调整 限流阈值先按网关单机 100 QPS 起步 接口超时连接不超过 3 秒整体业务超时不超过 10 秒 重试策略指数退避初始 1 秒最大 10 秒 幂等键user_id activity_id order_date这些数字不是标准答案只是一个起点。真正合适的值取决于真实用户量、商品配额和团队可用资源。更关键的是压测时要模拟“集中提交”而不是均匀请求。6.3 用四类监控指标判断系统是否健康预约系统上线之后至少需要关注四类监控数据流量类。提交时段峰值QPS、入口成功率、限流拒绝量、服务响应耗时。业务类。每日有效申购单量、重复请求拦截量、中签结果生成耗时、结果通知成功率。状态类。待支付单量、超时未支付单量、支付成功但状态未更新单量、退款单量。体验类。首次启动耗时、页面提交成功率、结果查询成功率、支付页面跳转耗时。监控不只是“画一张大屏”。更实际的目标是让异常变得可发现某天未支付单量突然下降可能是因为活动关闭通知成功率下降可能是因为短信渠道欠费或推送证书过期。没有监控这些问题往往要等用户投诉到客服才能被发现。7. 回到出发点评价i茅台别只用一个申购率观察一个产品角度不同结论会完全不同。从普通用户角度看申购率低确实会让人沮丧。从渠道角度看配额怎么分、分给谁关系到市场秩序。从开发团队角度看真正的成绩不是某天放了多少量而是这套系统在高关注、高价值、强监管压力下能不能稳定运转、规则透明、处理公平。i茅台值得长期关注的原因不是因为每个人都应该从中获得一瓶酒而是它提供了一种稀缺商品的数字化分发样本。在这个样本里实物供应链、配额计划、用户数据、风控规则和线下履约被放在同一套系统里共同设计。无论你最终是否中签都可以从产品交互中感受到这种强约束不能随意下单、必须实名参与、结果要等公布、中签后要按时支付、提货有指定规则。这些约束对用户来说是门槛对系统来说是必要的控制边界。正是这些控制边界使得规则变得可解释、流程变得可追踪也让品牌能够在不失控的前提下一步步建立与用户的直接连接。如果你也是一个开发者下一次再遇到预约类产品建议不只盯着放量结果看。可以多留几个观察点提交时是否做了幂等处理抽签结果是否可审计支付超时后是否会自动释放状态不一致时是否有补偿机制。这些藏在页面之下的能力才是稀缺商品数字化真正的技术含量。
返回列表