黑马点评-优惠券秒杀-02_voucher_table_design

发布时间:2026/5/29 1:49:05

黑马点评-优惠券秒杀-02_voucher_table_design 黑马点评优惠券秒杀二为什么有了优惠券表还要再拆秒杀券表本文继续整理黑马点评 Redis 实战篇第 3 章「优惠券秒杀」。上一篇讲了全局唯一订单 ID。这一篇先不急着进入下单而是把秒杀券的数据模型讲清楚。因为如果不先分清tb_voucher、tb_seckill_voucher、tb_voucher_order分别负责什么后面看库存、一人一单、下单记录时会很容易混。1. 本文解决什么问题这篇主要解决几个学习时很容易冒出来的问题1. 普通券到底有没有库存 2. 为什么有了 tb_voucher还要有 tb_seckill_voucher 3. Voucher 类里明明有 stock为什么又说 tb_voucher 不存库存 4. 新增秒杀券时为什么要先 save(voucher)再保存 SeckillVoucher 5. voucherId 能不能直接让前端传先给结论tb_voucher存优惠券本体信息tb_seckill_voucher存秒杀专属信息tb_voucher_order存用户购买记录。秒杀券本质上是“优惠券本体 秒杀扩展信息”所以新增秒杀券时需要先保存优惠券本体再用生成出来的 voucherId 保存秒杀扩展。2. 为什么要拆三张表优惠券秒杀里至少有三类信息1. 优惠券本身是什么。 2. 这张券参与秒杀时有什么规则。 3. 哪个用户买了哪张券。这三类信息不是一回事。如果强行塞进一张表会变成普通券也带库存字段 普通券也带秒杀开始时间 普通券也带秒杀结束时间 订单记录也和券本体混在一起所以更合理的拆法是tb_voucher优惠券本体 tb_seckill_voucher秒杀扩展信息 tb_voucher_order用户下单记录3. tb_voucher优惠券本体表Voucher实体对应的是TableName(tb_voucher)publicclassVoucherimplementsSerializable{TableId(valueid,typeIdType.AUTO)privateLongid;privateLongshopId;privateStringtitle;privateStringsubTitle;privateStringrules;privateLongpayValue;privateLongactualValue;privateIntegertype;privateIntegerstatus;}它主要描述这张券属于哪个店铺 这张券叫什么 使用规则是什么 支付金额是多少 抵扣金额是多少 券的类型和状态是什么这些都是一张优惠券的基础信息。所以tb_voucher可以理解为优惠券身份证表不管是普通券还是秒杀券它首先都是一张优惠券。4. Voucher 里为什么也有 stock、beginTime、endTime这是一个很容易卡住的点。Voucher类中确实有TableField(existfalse)privateIntegerstock;TableField(existfalse)privateLocalDateTimebeginTime;TableField(existfalse)privateLocalDateTimeendTime;很多人看到这里会误以为tb_voucher 表里也有库存、开始时间、结束时间。其实不是。关键在这个注解TableField(existfalse)它的意思是这个字段不是数据库表中的真实列。那为什么还要放在Voucher类里因为新增秒杀券时前端会一次性提交两类数据1. 优惠券基础信息标题、规则、金额、店铺 id 2. 秒杀信息库存、开始时间、结束时间为了接收这个请求项目把秒杀字段临时挂在Voucher对象上。所以这里要分清Java 对象里有字段 不等于 数据库表里一定有这个列5. tb_seckill_voucher秒杀扩展表SeckillVoucher对应的是秒杀优惠券表TableName(tb_seckill_voucher)publicclassSeckillVoucherimplementsSerializable{TableId(valuevoucher_id,typeIdType.INPUT)privateLongvoucherId;privateIntegerstock;privateLocalDateTimebeginTime;privateLocalDateTimeendTime;}这张表不是“另一张优惠券表”。它更准确地说是秒杀扩展信息表它只保存秒杀业务需要的字段voucher_id关联哪一张优惠券 stock秒杀库存 begin_time秒杀开始时间 end_time秒杀结束时间这里最重要的是TableId(valuevoucher_id,typeIdType.INPUT)privateLongvoucherId;voucherId不是重新生成一张券的 ID而是关联tb_voucher.id。也就是说tb_voucher.id 1 tb_seckill_voucher.voucher_id 1表示id 为 1 的这张优惠券参加了秒杀活动。6. tb_voucher_order订单事实表VoucherOrder对应订单表TableName(tb_voucher_order)publicclassVoucherOrderimplementsSerializable{TableId(valueid,typeIdType.INPUT)privateLongid;privateLonguserId;privateLongvoucherId;privateIntegerpayType;privateIntegerstatus;}它记录的是谁买了哪张券比如id 订单 ID user_id 用户 ID voucher_id 优惠券 ID后面的一人一单判断本质上就是查这张表selectcount(*)fromtb_voucher_orderwhereuser_id?andvoucher_id?如果结果大于 0就说明这个用户已经买过这张券。7. 新增普通券只写 tb_voucher新增普通券的接口类似PostMappingpublicResultaddVoucher(RequestBodyVouchervoucher){voucherService.save(voucher);returnResult.ok(voucher.getId());}普通券只需要保存基础信息。所以只执行voucherService.save(voucher);这句是 MyBatis-Plus 的通用保存方法可以粗略理解为insertintotb_voucher(...)values(...)8. 新增秒杀券为什么要写两张表新增秒杀券的代码OverrideTransactionalpublicvoidaddSeckillVoucher(Vouchervoucher){// 保存优惠券save(voucher);// 保存秒杀信息SeckillVoucherseckillVouchernewSeckillVoucher();seckillVoucher.setVoucherId(voucher.getId());seckillVoucher.setStock(voucher.getStock());seckillVoucher.setBeginTime(voucher.getBeginTime());seckillVoucher.setEndTime(voucher.getEndTime());seckillVoucherService.save(seckillVoucher);// 保存秒杀库存到 RedisstringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEYvoucher.getId(),voucher.getStock().toString());}这个方法做了三件事1. 保存优惠券本体到 tb_voucher。 2. 保存秒杀信息到 tb_seckill_voucher。 3. 把秒杀库存预热到 Redis。9. 为什么必须先 save(voucher)这里最容易问voucherId 不是能前端传进来吗技术上前端当然可以传。但业务上不应该让前端决定新优惠券的主键 ID。Voucher的主键是TableId(valueid,typeIdType.AUTO)privateLongid;IdType.AUTO表示这个 ID 由数据库自增生成。所以新增秒杀券的正确顺序是1. 前端传来优惠券业务信息。 2. 后端先保存 tb_voucher。 3. 数据库生成真实 voucher.id。 4. 后端再用这个 id 写 tb_seckill_voucher.voucher_id。如果信任前端传来的voucherId会有几个问题1. 前端可能传一个已经存在的 ID导致主键冲突。 2. 前端传的 ID 不一定真实存在。 3. 两张表之间的关联关系会变得不可信。所以这里不是“前端不能传”而是新增数据时主键应该由后端和数据库产生不能由外部请求随便决定。10. 为什么新增秒杀券时还要写 Redis最后这句stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEYvoucher.getId(),voucher.getStock().toString());它会写入seckill:stock:{voucherId} - stock比如seckill:stock:1 - 100这一步是为后面的秒杀优化做准备。因为秒杀时如果每个请求都去 MySQL 查库存数据库压力会很大。后面会把资格判断前移到 RedisRedis 先判断库存和一人一单 通过后再异步下单所以新增秒杀券时顺手把库存写入 Redis是后续优化链路的一部分。11. 新增秒杀券的数据流转图管理端提交秒杀券信息VoucherController /voucher/seckilladdSeckillVoucher(voucher)save(voucher) 写入 tb_voucher数据库生成 voucher.id创建 SeckillVouchersetVoucherId(voucher.getId)写入 tb_seckill_voucher写入 Redis: seckill:stock:{id}12. 易错点1. 普通券不是完全不能有数量概念真实业务里普通券也可能有限量。但在这个项目的建模里库存和抢购时间主要属于秒杀业务所以放在tb_seckill_voucher。2.Voucher.stock不是tb_voucher.stock因为它标了TableField(existfalse)它只是 Java 对象中用于接收参数的字段。3.tb_seckill_voucher不是新的券本体表它只是给已有优惠券补充秒杀字段。4. 前端传主键不可信新增数据时主键应该由数据库或后端 ID 生成器负责。13. 面试怎么回答如果面试官问为什么优惠券和秒杀券要拆表可以回答因为优惠券基础信息和秒杀活动信息不是同一类数据。普通券只需要标题、规则、金额等基础字段而秒杀券额外需要库存、开始时间、结束时间。把秒杀字段拆到tb_seckill_voucher中可以避免普通券携带无意义字段也让秒杀业务扩展更清晰。如果面试官问新增秒杀券的流程是什么可以回答后端先保存优惠券基础信息到tb_voucher拿到数据库生成的优惠券 ID然后创建SeckillVoucher把这个 ID 作为voucher_id再保存库存和秒杀时间到tb_seckill_voucher最后把秒杀库存以seckill:stock:{voucherId}为 key 写入 Redis为后续 Redis 秒杀资格判断做准备。如果面试官问为什么不能让前端传 voucherId可以回答新增优惠券时主键 ID 应该由数据库或后端生成不能信任前端传入。否则可能出现主键冲突、伪造关联或关联不存在数据的问题。正确做法是先保存券本体拿到真实 ID再写秒杀扩展表。14. 总结这一节的核心是把三张表分清tb_voucher优惠券本体 tb_seckill_voucher秒杀规则和库存 tb_voucher_order用户购买记录新增普通券只写tb_voucher。新增秒杀券要写tb_voucher tb_seckill_voucher Redis 秒杀库存理解完这一步后面再看秒杀下单时就能知道判断时间和库存要查 SeckillVoucher 保存订单要写 VoucherOrder 一人一单要查 VoucherOrder Redis 优化要基于 seckill:stock:{voucherId}这样第三章的数据模型就立住了。

相关新闻