每日学习-03
问题一:优惠券秒杀中为什么使用 Redis Lua 脚本进行校验和库存预扣问题二:你所说的 Lua 脚本“回退”是指什么什么情况下需要回退问题三:如果 Lua 脚本已经执行成功但后续订单消息发送到 Kafka 失败应该怎么处理问题四:只依靠 Lua 脚本和消息队列就能保证库存不超卖和一人一单吗问题五:如果绕过 Redis 入口直接在数据库中更新库存数据库层面如何防止超卖问题一:优惠券秒杀中为什么使用 Redis Lua 脚本进行校验和库存预扣我分两点说原因保证操作原子性防止超卖校验库存、判断用户是否已抢购、预扣库存这三步如果分成多条Redis命令执行并发下会出现数据错乱。Lua脚本会把所有逻辑打包成一段代码Redis执行Lua时是单线程串行执行中间不会插入其他客户端命令整套流程要么全部执行成功要么全部不生效不会出现扣多库存的问题。减少网络IO损耗提升秒杀并发性能如果分开多条命令每次和Redis交互都要走一次网络往返Lua脚本只需要一次网络请求就能完成全部校验扣减逻辑大幅降低网络耗时扛住秒杀高并发流量。问题二:你所说的 Lua 脚本“回退”是指什么什么情况下需要回退Lua 脚本的回退指什么Redis 执行 Lua 脚本时具备原子性脚本里任意一步判断不满足比如库存不足、用户已经抢过券就不会执行库存扣减相当于全部操作自动回滚不会出现只执行一半逻辑的情况脚本执行中途报错已执行的所有 Redis 操作也会全部撤销这就是回退。需要回退的场景① 校验不通过比如优惠券库存为 0、同一个用户重复抢购不允许扣库存直接回退 ② 脚本执行过程中出现异常防止只扣库存但没生成下单记录 ③ 后续业务失败需要补偿比如消息发送失败、数据库创建订单报错要重新执行 Lua 脚本把预扣的库存加回 Redis做主动回退补偿。问题三:如果 Lua 脚本已经执行成功但后续订单消息发送到 Kafka 失败应该怎么处理这种情况要分两步处理避免 Redis 库存扣了但数据库没下单导致库存不一致本地先做事务保障发送 Kafka 消息前不直接执行 Lua 预扣库存可以用本地事务表记录抢购记录Lua 预扣库存和写入本地抢购记录放同一本地事务只有入库成功才发消息。如果发消息失败事务回滚Redis 库存同步加回。消息层面兜底机制① 使用 Kafka 事务消息Lua 扣库存和发订单消息绑定原子执行要么库存扣减 消息投递同时成功要么一起回滚 ② 开启生产者确认机制消息发送失败捕获异常手动执行补偿 Lua 脚本把 Redis 里预扣的库存加回去 ③ 后台定时补偿任务定期扫描 Redis 预扣库存但数据库无对应订单的记录自动归还库存做最终一致性兜底。问题四:只依靠 Lua 脚本和消息队列就能保证库存不超卖和一人一单吗考官只靠 Lua 脚本和 Kafka 消息队列不能完全保证库存不超卖、一人一单只能拦截大部分并发问题还有漏洞Redis 层面风险 如果 Redis 集群宕机、主从切换Lua 预扣库存的数据丢失会出现重复发放优惠券、超卖Lua 只能管控 Redis 缓存无法约束绕过 Redis 直接操作数据库的请求。消息队列丢失 / 重复消费问题 Kafka 消息丢失会导致预扣的库存没生成订单库存永久少扣消息重复消费会重复创建订单破坏一人一单规则。最终兜底必须依赖数据库 数据库要做两层兜底 一是优惠券订单表给用户 优惠券加唯一索引从物理层面保证一人一单 二是更新库存时用乐观锁或者update stock stock -1 where stock 0语句防止超卖。 简单说RedisKafka 是高并发前置拦截数据库才是最终一致性的底线保障。问题五:如果绕过 Redis 入口直接在数据库中更新库存数据库层面如何防止超卖就算绕过Redis直接操作数据库靠下面两种方案就能防止超卖悲观锁方案查询优惠券库存时用select ... for update加行锁锁住这条库存记录同一时间只允许一个事务修改库存更新完成释放锁并发下不会多扣缺点是高并发下锁竞争激烈性能差。乐观锁推荐秒杀场景常用第一种给库存表加version版本字段更新语句写update coupon set stockstock-1,versionversion1 where id? and version? and stock0只有版本匹配、库存大于0才更新成功更新失败说明被其他线程抢先扣减直接返回抢购失败。 第二种极简乐观锁不加version直接用update coupon set stock stock - 1 where id? and stock 0数据库在执行这条更新时是原子操作并发时不会出现超卖开发更简单。补充兜底订单表给「用户id优惠券id」建立唯一索引从底层保证一人一单避免重复下单。