抢购预约
2026-01-31 13:40:16 0 举报
抢购
作者其他创作
大纲/内容
抢购
抢购活动步骤
1.抢购活动创建<b>在促销系统中创建</b>
2.促销系统创建时会选择是否抢购
3.勾选则调用<b>抢购系统</b>将抢购活动信息写入到<b>促销库</b>
redis
注意<b>存储活动信息和库存</b>
C端抢购<b>使用redis扣减存储防止超卖</b>
mysql
预约
预约活动步骤
步骤1.在ERP创建,调用商品接口,<b>打预约标签(可以理解为商品属性中存在预约字段)-----<font color="#ec7270">创建时应该是添加了预约活动的预约开始/结束 抢购开始/结束时间</font></b>
2.创建过程中调用规则中心服务,查看SKU在该时间段,是否存在<b><font color="#e74f4c"> 预约活动冲突</font></b>,<b>没有则创建预约活动</b>
3.预约活动信息保持存在<b>数据库和redis中</b>
Redis 做业务聚合应用缓存
抢购的查询维度(以SKU 为核心 key)聚合为结构化的 value(如 Hash/JSON),存储到 Redis 中
4.用户在C端预约/活动状态改变
1.更改redis缓存
2.异步写入到mysql
mysql中存储的活动规则对应sku的关系,查询时 需要的sku对于活动的关系
理解
mysql应该时存储了活动规则,SKU,活动规则 - SKU 关联关系,如果在商品页面详情调用时,需要的是「聚合后的数据」--<b>多表联查 + 业务聚合---存在性能问题</b>
3.存在问题redis和mysql的数据不一致性问题
解决方案
使用旁路worker(<b>个人理解类似定时任务</b>),对比redis和mysql的数据一致性,存在则以<b>redis数据为准 进行修复 问题解析</b>
思考:<b>为什么不使用常规的更新 MySQL 成功---通过 MQ / 异步线程,事后异步去更新 Redis</b>
1.性能原因,该redis(微秒/毫秒级别),异步更新mysql,<b>如果同步改 MySQL,高并发下 MySQL 的写操作会出现锁等待、事务排队</b>
2.抢购在高并发情况下如果先同步mysql,可能存在雪崩问题
3.写操作改了 MySQL,Redis 还是旧数据,而 C 端查询只读 Redis,导致用户操作成功后,页面长时间显示旧数据(比如预约成功了,但剩余名额没减、状态还是 “未预约”)。<br>这种情况会引发用户重复操作(反复点击预约 / 抢购)
预约活动改造
开始一个sku只能对应一个活动信息,为适应一个sku可以存在多个不同区域的活动,进行优化
原方法:sku
sku 为keyde String结构缓存
用于预约资格:key 也是String结构缓存
解决方案
1.原有结构不变,<b>在原有key后面添加区域字段,代表区域活动,</b>
快速,简单,扩展性差
2.改变缓存结构使用Hash,key是sku,<b> filed对应各个规则(活动区域,方便以后扩展),value是对应的活动信息,</b>
1.用于预约资格String结构,也需要修改为Hash结构,开发量大
2.优点在于扩展性强
业务流程
商品详细页会先调用商品信息接口,<b>查看商品是否存在预约标识,如果存在调用预约信息接口</b>
预约存在四个阶段,内部使用状态机轮转
预约开始
预约结束
抢购开始
抢购结束
前后端操作流程
<b>预约开始</b>:用户跳转到商品详细也,商品详细页会带着用户信息,调用预约系统,查看用户是否预约(<b>校验是否存在预约资格</b>)
已经预约
无预约:显示<b>预约按钮,</b>点击预约按钮调用预约接口
1.对用户防重,调用风控RPC
2.用户预约资格写入redis缓存(<b>这一步完成就算人的预约成功</b>)
3.如果商品抢购活动存在优惠和促销,也要调用对应RPC
4.用户预约列表redis缓存写入,
5.发送mq去数据库增加用户缓存
预约完成:会在商品详细页返回预约人数
redis存储,String类型的热点key
热点优化:利用本地guava统计一个时间段内key次数,如果超过某个最大值,则不立即写入redis,现在jvm中将缓存存起来,达到一点数量后再一起写入redis,减少key操作redis次数
比如本地使用ConcurrentHashMap<String, AtomicInteger>
思考:<b>如果热点消退时,本地只攒了 20 次增量(未达 50 的攒批阈值),或服务做轻量重启(JVM 内存清空),这 20 次增量会直接丢失,无法落地 Redis</b>
<b>定时兜底</b>:是否可以使用用单线程定时任务,遍历本地攒批缓存,将所有未刷写的增量全部incrby刷入 Redis,刷完后重置计数器
<b>服务关闭兜底</b>:监听 JVM 的关闭钩子(ShutdownHook),服务停止 / 重启时,触发兜底刷写,将本地所有暂存增量刷入 Redis,避免进程退出导致数丢
预约完成:会提醒用户再什么时间进行抢购,在抢购前15分钟会发站内信息提醒用户,预约系统的worker(定时任务)实时扫描预约活动库,当活动快到抢购期,扫描<b>用户预约资格库,将对应用户信息取出,发送mq到消息中心发送消息</b>
注意:
热门商品预约活动人数可能达到百万,所以要先判断活动对应的<b>预约人数,超过十万回去扫描redis缓存去发送抢购提醒信息</b>
redis缓存list结构,key活动组,value用户信息,<b>百万信息会形成大key,所以需要拆分key为活动组_0/1000这种,变成小key</b>
等待抢购:如果抢购时间到了,商品详细页按钮会变成立即抢购
普通模式:在等待期到抢购期,商品会在<b>抢购时调用获取预约信息和是否预约接口,根据结果判断是否显示立即抢购按钮</b>
茅台模式:商详底部按钮会在<b>抢购开始前ms级别内自动变为立即抢购按钮</b>,<b>不会调用预约</b>的是否预约顺约信息授口,在抢购开始前就会调用预约验资用户,会返回用户是否有资格和时间校验,<b>提前进入有资格的用户直接进入抢购,减少爆品活动,在同一时刻调用预约验资的流量</b>
预约活动状态机更新策略:状态机使用的是一个内存级别的延迟队列,所以活动特别多的话,<b>活动状态的流转 肯定会有一定的延迟,</b>这就会导致页面上的按钮不准确,。所以在每次商详获取预约信息 获取 状态时,<b>会用系统当前时间 去实时算一遍当时的时间 属于哪个时期</b>,不直接使用缓存中的状态,这样就会最准确
理解:<b>实际就是状态机状态机 + 实时计算(获取活动的开始实际,结束时间,抢购开始时间,结束时间,然后根据当前时间服务器的UTC时间)计算当前的预约状</b>态
思考:状态机结构
点击抢购按钮:
1.直接转到结算页面
2.预约后直接将商品加到购物车,用户进入购物车,<b>会调用促销系统 获取抢购信息,然后跳转到结算页面</b>
结算页支付
1.调用抢购系统,然后调用库存系统,进行库存预占,等待支付
支付失败/没有支付:使用mq调用回滚接口,将扣减库存还回去,<b>防止超卖</b>
说明:抢购系统内部不会超卖,<b>原因:抢购系统redis存储的信息: 抢购活动,以及抢购库存</b>
<font color="#e74f4c">只有再极端情况下<b>redis集群某个节点主从切换才会出现超卖</b>(原因:Redis 主从「异步复制」+ 主从切换的「写操作丢失」<br></font> 主从切换时的超卖,只发生在 「主节点执行写操作成功,但未同步到从节点」的瞬间,主节点突然故障 ,哨兵触发主从切换,原从节点升级为新主节点 ——新主节点因为没同步到之前的扣减操作,库存数据会「回滚」到扣减前的状态)
2.在抢购的库存都扣完了之后,会<b>异步发mq去通知促销系统 下线页面上的抢购活动,</b>如果这时用户取消了订单,抢购库存回退了,也不会再改动页面上抢购活动的状态,<b>所以这里如果有大量的订单取消,可能大量导致少卖</b>
解决方案:使用<b>延迟的线程池</b>,在第一次 抢购库存扣完的消息过来时,首先也会去查询下库存剩余量,如果真没了,会记录一个幂等信息,然后会写入延迟的线程池,根据抢购结束时间 计算一个时间段去 调用促销系统下线页面抢购入口
<span style="font-weight:normal;">个人理解</span>:库存扣完≠最终无库存可卖,尤其是抢购刚结束 / 库存刚扣完的短时间内,会有大量用户取消订单(比如手滑抢多了、后悔了),导致库存回滚,但此时页面入口已经下线,回滚的库存无法被其他用户抢购,直接造成少卖(库存浪费)。
个人理解步骤
1.库存扣完触发后,先二次查询 Redis 实际库存,做首次有效性校验
设计目的:<b>防止误触发</b>。<br>抢购是高并发场景,可能因 Redis 原子扣减的并发时序问题(比如多个请求同时扣减最后 1 个库存),导致「临时触发多次库存扣完信号」,或出现扣减后因网络 / 操作问题库存实际未到 0的情况,二次查询能过滤这些无效触发,保证只有真・库存为 0 时才走后续流程。
2.首次校验确认库存真的为 0 后<b>,记录幂等信息,防止重复创建延迟任务</b>
设计目的:<b>解决高并发下「多次触发库存扣完」导致的重复创建延迟任务问题</b>。<br>比如抢购最后 1 个库存时,10 个并发请求同时扣减,都触发了「库存扣完」信号,如果不做幂等,会创建 10 个相同的延迟下线任务,最终多次通知促销系统下线,造成业务混乱(比如重复下线、接口报错)。<br>幂等规则:<b>一个活动 ID,只允许创建 1 个延迟下线任务</b>,后续再触发「库存扣完」信号,只要查到幂等标识已存在,直接跳过所有逻辑
3<b>.根据抢购结束时间计算延迟时间段,将下线任务提交至延迟线程池</b>
4.延迟时间 T 到后,延迟线程池执行最终逻辑:再次校验库存 + 决定是否通知下线
5.(兜底)若延迟任务执行后通知下线,促销系统接收到通知后,更新页面抢购活动状态
思考<b>:内存级延迟线程池核心可靠性短板—— 服务挂掉(重启 / 宕机 / 容器重启)后,线程池里所有未执行的延迟下线任务会直接丢失,没有任何恢复的可能</b>
解决方案:内存延迟线程池 + Redis 持久化兜底是否可行,活<b>动创建延迟下线任务时,同时做两件事:① 提交到内存延迟线程池;② 把任务元数据持久化到 Redis;服务挂掉重启后,启动时扫描 Redis 中未执行的任务,重新提交到延迟线程池 / 直接执</b>
抢购项目改造
使用redis操作替代lua解决大ley和热点key问题
下单时结算页,携带订单号,抢购限制类型,最少抢购数量等请求下单服务,<b>抢购内部对所有限制进行校验</b>
大概流程:活动信息存储各种限制条件库存中枢,活动期限等,然后校验所有库存是否充足,在依次进行扣减库存,只有有一种库存不满足就不会进行真正扣减
实现方式:扣减库存使用lua脚本操作,保证了不加锁情况下复杂操作的原子性
缺点
lua脚本对应的所有操作要在redis的同一个分片上
不能实现复杂规则的叠加,和新业务规则扩展
解决方案:
1.<b>HashTag 强制管理key在同一个集群节点上</b>,活动信息key和限制key都加同一个hashTag
举例
动态变量
activityId:活动唯一 ID(如 1001、2002
promoType:促销类型(如 1 = 满减、2 = 折扣)
promoId:促销方案唯一 ID(比 activityId 更细,一个活动可能对应多个 promoId)
pin:用户唯一标识(如 user123、pin456)
day:日期维度(如 20260131、20260201)
用户活动期间购买限制 key:{Q_activityId_promoType}_pin
{Q_activityId_promoType},所有的用户信息都存储在这个hash结构中
优点:<b>hashTag 聚节点:强关联 key 加相同 {xxx},保证同 Redis 节点,解决集群跨节点痛点</b>
存在问题:
<b><font color="#e74f4c">大key问题</font></b>:比如活动信息 key:<b>{Q_activityId_promoType},所有的用户信息都存储在这个hash结构中</b>,如果购买让人数多,那么就会出行大key
个人理解这里的大key
field 的 Hash key 总内存也会达到几十到上百 MB
field 数远超 10w,可能达到百万
热点key:lua脚本只能在同一个分片上,用户一次操作所有key都达到一个redis分片上,如果是爆品会形成热点key,分片容易挂
热点key解决:
<b>1.拆分key</b>,比如将key:{Q_activityId_promoType}_pin拆分成64个小key,:{Q_activityId_promoType}_pin_0/64
快速,保证稳定性,选用了
2.直接出去lua脚本使用redis分布式锁,保证事务,将之前hash结构的大key按照用户维度,订单维度拆分成小key
比如将{Q_activityId_promoType}_pin,拆分成{Q_activityId_promoType_zhangsan},{Q_activityId_promoType_lisi},用户维度路由,分散到库存到各个分片
思考:这样是不是key就太多了,reids内存开销是不是就会增大很多
实现:
1.将各个维度库存分桶存储,并记录分桶存是否已经发完,这样每次可以先查询片库存再扣减
分桶:解决局部热点
先查后扣:提示扣减成功率
2.jvm级别的本地缓存,前置存储各个维度是否已经扣完库存,可以快速失败返回,减少redis交互
优化后下单流程
1.结算页将用户抢购促销信息传递到抢购系统
2.抢购下单服务再本地guava缓存校验活动和促销状态是否结束
如果这些信息过期会从redis中更新
3.根据订单号使用redis分布式锁,setnxex操作,根据活动信息进行责任链匹配,执行具体限制的库存扣减
库存有hash变成了string结构,并且分桶避免热点key
stock_0,stock_1,stock_2这三个key的库存总和才是总库存,所以要有一个库存分片key<font color="#e74f4c">,例如use:0,1,2扣减时按照顺序轮询可用分片</font>
扣减使用的是incrBy,累计增加到库存key分配的最大值,则进行扣减下一个库存key,直到所有库存key都扣完还不够,就扣减失败
使用incrBy操作,如果redis异常,我们不知道key是否扣减成功,可能出现少卖场景
1.如果异常判断直接返回操作失败,没有累加成功,<b>但是异常比如是因为超时引起的时间,实际累加成功,就会出现少买</b>
解决方案:添加安全key incrby和sentnxex一起操作
redis事务操作
lua脚本操作
pipeline
只能一定程度减少库存少卖,不能避免
如果incrby原子操作累计超过库存,返回失败并回滚库存,如果redis回滚是超时异常,<b><font color="#e74f4c">如果回滚操作失败,那么会出现超卖,注意一般操作失败都会重试一次</font></b>
<font color="#e74f4c">只有再极端情况下<b>redis集群某个节点主从切换才会出现超卖</b>(原因:Redis 主从「异步复制」+ 主从切换的「写操作丢失」<br></font> 主从切换时的超卖,只发生在 「主节点执行写操作成功,但未同步到从节点」的瞬间,主节点突然故障 ,哨兵触发主从切换,原从节点升级为新主节点 ——新主节点因为没同步到之前的扣减操作,库存数据会「回滚」到扣减前的状态)
效果:OPS从24w降到5w,TP999从900ms降到200ms
0 条评论
下一页