第10节:不超卖库存规则实现
-
今天主要学习了扣减库存
performRaffle(100006, "xiaofuge")
│
├─ 📦 阶段一:责任链抽奖(不变)
│ └─ ... 同之前,随机到 awardId=102 → logicModel="rule_default" → 进入规则树
│
├─ 🌲 阶段二:规则树抽奖
│ │
│ ├─ raffleLogicTree("xiaofuge", 100006, 102)
│ │ │
│ │ ├─ queryStrategyAwardRuleModel → ruleModels = "tree_lock"
│ │ ├─ queryRuleTreeVOByTreeId("tree_lock") → 获得完整规则树
│ │ ├─ openLogicTree → new DecisionTreeEngine
│ │ │
│ │ └─ treeEngine.process("xiaofuge", 100006, 102)
│ │ │
│ │ ├─ 🔄 第1轮:node = rule_lock
│ │ │ ├─ logicTreeNode = RuleLockLogicTreeNode
│ │ │ ├─ ruleValue = "1"(来自 rule_tree_node 表的 rule_value 字段) ← 🆕 新增:把 ruleValue 传给节点
│ │ │ ├─ long raffleCount = Long.parseLong("1") ← 🆕 从 ruleValue 读取限定次数,不再写死
│ │ │ ├─ userRaffleCount(10) >= raffleCount(1) → true
│ │ │ ├─ 返回 ALLOW, strategyAwardVO=null
│ │ │ └─ nextNode("ALLOW", ...) → "rule_stock"
│ │ │
│ │ ├─ 🔄 第2轮:node = rule_stock ← 🆕🆕 重点变化
│ │ │ ├─ logicTreeNode = RuleStockLogicTreeNode
│ │ │ ├─ ruleValue = null(rule_stock 节点未配 rule_value)
│ │ │ ├─ 🆕 步骤①:原子扣减 Redis 缓存库存
│ │ │ │ ├─ strategyDispatch.subtractionAwardStock(100006, 102)
│ │ │ │ │ └─ cacheKey = "strategy_award_count_key_100006_102"
│ │ │ │ │ └─ redisService.decr(cacheKey)
│ │ │ │ │ ├─ 库存从 97 → 96(decr 后 surplus=96)
│ │ │ │ │ ├─ surplus >= 0 → 没有超卖
│ │ │ │ │ ├─ lockKey = "strategy_award_count_key_100006_102_96"
│ │ │ │ │ ├─ redisService.setNx(lockKey) → 给这个"槽位"加锁
│ │ │ │ │ └─ return true(扣减成功)
│ │ │ │ │
│ │ │ │ └─ 如果 surplus < 0:
│ │ │ │ ├─ redisService.setValue(cacheKey, 0) → 恢复到 0
│ │ │ │ └─ return false(库存不足)
│ │ │ │
│ │ │ ├─ 🆕 步骤②:扣减成功(status=true)
│ │ │ │ ├─ 组装 StrategyAwardStockKeyVO{strategyId=100006, awardId=102}
│ │ │ │ ├─ awardStockConsumeSendQueue → 放入 Redisson 延迟队列
│ │ │ │ │ └─ delayedQueue.offer(vo, 3, TimeUnit.SECONDS)
│ │ │ │ │ └─ 3 秒后这条消息才可被消费
│ │ │ │ └─ 返回 TAKE_OVER, strategyAwardVO={awardId=102, awardRuleValue=null}
│ │ │ │
│ │ │ └─ 如果扣减失败(status=false):
│ │ │ └─ 返回 ALLOW, strategyAwardVO=null(库存不足,放行让下一节点兜底)
│ │ │
│ │ └─ 🔄 第3轮:node = rule_luck_award ← 🆕 不再写死 101
│ │ └─ ruleValue = "1,100"(来自数据库 rule_tree_node 表)
│ │ ├─ split(":") → ["1,100"](只有一段)
│ │ ├─ luckAwardId = Integer.valueOf("1,100") → 抛异常!(格式不对)
│ │ │ ↑
│ │ │ ⚠️ 这里有个坑:分隔符是冒号 ":",但数据存的是逗号 "1,100"
│ │ │ big-market 数据库里的值是 "101:1,100" 这样的格式
│ │ │ 101 = 兜底奖品ID, 1,100 = 积分范围
│ │ └─ ...
-
新增内容拆解
1. ILogicTreeNode.logic() 新增 ruleValue 参数
之前:logic(userId, strategyId, awardId)
现在:logic(userId, strategyId, awardId, ruleValue) ← 🆕 多了一个参数
每个规则节点不再写死数值,而是从 rule_tree_node.rule_value 读取配置:
rule_lock的rule_value = "1"→ 表示"抽奖 1 次后解锁"rule_luck_award的rule_value = "101:1,100"→ 表示"兜底奖品 101,积分范围 1~100"
2. rule_lock:从永远放行 → 根据配置判断
之前:
rule_lock.logic() → 永远返回 ALLOW(等于没作用)
现在:
rule_lock.logic(userId, strategyId, awardId, "1")
│
├─ raffleCount = Long.parseLong(ruleValue) → 1(需要抽奖 1 次后解锁)
├─ userRaffleCount(10) >= raffleCount(1) → true → 返回 ALLOW(放行)
│
└─ 如果 userRaffleCount < raffleCount → 返回 TAKE_OVER(拦截,不让抽)
└─ 拦截后走 rule_lock → rule_luck_award 的连线,给兜底奖品
3. rule_stock:从空壳 → Redis 原子扣减 + 延迟队列异步落库
这是本次最大的变化,分 3 个步骤:
rule_stock.logic(userId, strategyId, awardId, ruleValue)
│
├─ 🔴 步骤①:Redis 原子扣减
│ ├─ strategyDispatch.subtractionAwardStock(100006, 102)
│ │ ├─ cacheKey = "strategy_award_count_key_100006_102"
│ │ └─ redisService.decr(cacheKey)
│ │ ├─ 库存 97 → 96(decr 后 surplus=96)
│ │ ├─ surplus >= 0 → 没超卖 ✅
│ │ ├─ setNx("strategy_award_count_key_100006_102_96") → 对槽位 96 加锁
│ │ │ └─ 作用:即使将来手动恢复库存,已卖出的槽位也无法再被扣减
│ │ └─ return true
│ │
│ ├─ 扣减成功 → 执行步骤②③
│ │
│ ├─ 🟡 步骤②:放入延迟队列(异步刷库)
│ │ ├─ VO = StrategyAwardStockKeyVO{strategyId=100006, awardId=102}
│ │ ├─ delayedQueue.offer(VO, 3, TimeUnit.SECONDS)
│ │ │ └─ 放入 Redisson 延迟队列,3 秒后可被消费
│ │ │
│ │ └─ 返回 TAKE_OVER, strategyAwardVO={awardId=102, awardRuleValue=null}
│ │
│ └─ 扣减失败(stock < 0):
│ ├─ redisService.setValue(cacheKey, 0) → 恢复为 0
│ └─ 返回 ALLOW, strategyAwardVO=null → 放行到 rule_luck_award 兜底
│
└─ 🟢 步骤③:定时任务消费队列 → 更新数据库(异步)
│
└─ UpdateAwardStockJob.exec()(每 5 秒执行一次)
├─ raffleStock.takeQueueValue()
│ └─ blockingQueue.poll() → StrategyAwardStockKeyVO{100006, 102}
├─ raffleStock.updateStrategyAwardStock(100006, 102)
│ └─ UPDATE strategy_award
│ SET award_count_surplus = award_count_surplus - 1
│ WHERE strategy_id=100006 AND award_id=102 AND award_count_surplus > 0
└─ 数据库库存从 97 → 96
4. rule_luck_award:从写死 → 读数据库配置
之前:
return {awardId=101, awardRuleValue="1,100"} ← 写死
现在:
ruleValue = "101:1,100"(来自 rule_tree_node 表)
│
├─ split(":") → ["101", "1,100"]
├─ luckAwardId = 101
├─ awardRuleValue = "1,100"
└─ return {awardId=101, awardRuleValue="1,100"} ← 从配置读取
5. assembleLotteryStrategy:新增库存缓存预热
之前:只做概率装配
现在:先缓存库存到 Redis,再做概率装配
assembleLotteryStrategy(100006)
│
├─ 🆕 步骤①:缓存库存
│ └─ for each award:
│ ├─ cacheKey = "strategy_award_count_key_100006_102"
│ ├─ redisService.setAtomicLong(cacheKey, awardCount)
│ │ └─ 把数据库的 award_count 写入 Redis(如 97)
│ └─ 如果 key 已存在 → 跳过(避免重复装配时覆盖实时数据)
│
└─ 步骤②:概率装配(不变)
└─ 构建概率查找表存入 Redis
为什么这样设计?(关键点)
| 问题 | 方案 |
|---|---|
| 高并发下直接 UPDATE 数据库有行锁竞争 | Redis decr 原子操作,无锁竞争 |
| Redis 扣了但数据库还没更新 → 不一致 | 最终一致性:Redis 是实时的,数据库慢 3~5 秒追上 |
| Redis 挂了库存数据丢失 | 装配时从数据库恢复缓存(isExists 判断) |
| 手动恢复库存后超卖 | 每个扣减后的槽位用 setNx 加锁,恢复的库存占新槽位,老槽位已被锁 |
| 数据库 UPDATE 太频繁 | 延迟队列缓冲 3 秒 + 定时任务每 5 秒批量消费 |
完整调用链路(一句话总结)
assembleLotteryStrategy 时把库存写入 Redis
→ 抽奖走到 rule_stock 节点
→ Redis decr 原子扣库存
→ 扣减成功 → 写入延迟队列(3 秒后可用)
→ 返回奖品
→ UpdateAwardStockJob(每 5 秒)从队列取 → UPDATE 数据库
-
问题:
1.为什么有了setValue 还要写setAtomicLong?
setValue 存的是通用对象(什么都能放),setAtomicLong 存的是数字(只能放 long)。存数字用 setAtomicLong,后面才能用 decr/incr 做原子加减。存通用对象用 setValue,后面用 getValue 读。
2.
请你跟我描述一下这个 public Boolean subtractionAwardStock(String cacheKey) {
//返回的是剩余库存值
long surplus = redisService.decr(cacheKey);
if (surplus < 0) {
redisService.setValue(cacheKey,0);
return false;
}
String lockKey= cacheKey+Constants.UNDERLINE+surplus;
Boolean lock=redisService.setNx(lockKey);
if (!lock){
log.info(“策略奖品枷锁失败{}”,lockKey);
}
return lock;
}我不知道他的功能是什么
答:
subtractionAwardStock("strategy_award_count_key_100006_102")
│
├─ long surplus = redisService.decr(cacheKey)
│ └─ Redis 中 "strategy_award_count_key_100006_102" 的值:97 → 96
│ └─ surplus = 96
│
├─ if (surplus < 0) → 96 < 0? → false → 跳过
│
├─ String lockKey = "strategy_award_count_key_100006_102_96"
│ └─ 用 decr 后的值(96)拼出一个唯一的"槽位锁 key"
│
├─ Boolean lock = redisService.setNx(lockKey)
│ └─ setNx = "如果这个 key 不存在,就写入,返回 true;如果已存在,什么都不做,返回 false"
│ └─ 第一次执行:"strategy_award_count_key_100006_102_96" 不存在 → 写入成功 → lock = true
│
├─ if (!lock) → false → 跳过日志
│
└─ return true ✅(扣减成功)
decr 后 surplus=96 → setNx("..._96") → 这个槽位被占用了
decr 后 surplus=95 → setNx("..._95") → 这个槽位被占用了
...
decr 后 surplus=0 → setNx("..._0") → 这个槽位被占用了
请求1:decr → 3→2 → setNx("count_key_2") → true ✅ 卖出第1个
请求2:decr → 2→1 → setNx("count_key_1") → true ✅ 卖出第2个
请求3:decr → 1→0 → setNx("count_key_0") → true ✅ 卖出第3个
请求4:decr → 0→-1 → surplus < 0 → return false ❌ 卖完了
Redis 里现在有这些 key:
count_key = 0
count_key_2、count_key_1、count_key_0 ← 这 3 个是 setNx 留下的"已卖出凭证"
运营把数据库 award_count_surplus 从 0 改回 3
重新装配 → Redis "count_key" = 3
请求5:decr → 3→2 → 成功 ✅ → 卖出第4个(实际只有3个库存!超卖了!)
请求6:decr → 2→1 → 成功 ✅ → 卖出第5个
请求7:decr → 1→0 → 成功 ✅ → 卖出第6个
请求5:decr → 3→2 → setNx("count_key_2")
⚠️ "count_key_2" 已经存在!(第1个请求卖 2→1 时留下来的)
setNx 返回 false → 返回 false ❌ 不放行!
请求6:decr → 2→1 → setNx("count_key_1")
⚠️ "count_key_1" 也已经被占用了!
setNx 返回 false ❌
请求7:decr → 1→0 → setNx("count_key_0")
⚠️ "count_key_0" 也已经被占用了!
setNx 返回 false ❌

浙公网安备 33010602011771号