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