Skip to main content

10小节:开发支持百万预约优惠券功能

作者:程序员马丁

在线博客:https://nageoffer.com

note

热门项目实战社群,收获国内众多知名公司面试青睐,近千名同学面试成功!助力你在校招或社招上拿个offer。

开发优惠券预约通知功能(一),元数据信息:

©版权所有 - 拿个offer-开源&项目实战星球专属学习项目,依据《中华人民共和国著作权法实施条例》《知识星球产权保护》,严禁未经本项目原作者明确书面授权擅自分享至 GitHub、Gitee 等任何开放平台。违者将面临法律追究。


内容摘要:在比对了多种优惠券预约提醒的技术架构方案后,最终选择了 RocketMQ 5.x 的任意延时消息机制来实现预约提醒。通过线程池并行通知用户的方式,提升了提醒的效率。在此过程中,我们使用了位图技术,将多个时间段的提醒信息高效地存储在一个字段中,从而实现了对海量提醒的精准管理和高效处理。

课程目录如下所示:

  • 业务背景
  • Git 分支
  • 数据库表设计
  • 创建优惠券预约提醒
  • 文末总结

业务背景

大家可以类比 12306 购票预约功能。当购买紧张列车的车票时,我们通常会提前设置预约提醒,以便在开票时及时购买。类似地,优惠券预约提醒也具有很强的时效性,同时需要支持海量用户的提醒需求。为了实现这一点,我们选择了 RocketMQ 5.x 的任意延时消息功能,并通过线程池来并行提醒用户。

在实现过程中,我们采用了位图(bitmap)思想,巧妙地利用单一字段实现了多个时间段的预约提醒功能。通过这种方式,我们不仅满足了海量提醒的需求,还确保了系统的高效和时效性。

整体实现较为复杂,涉及到二进制操作较多,如果大家不熟悉二进制 &、^、位移等操作,需要网上提前学习下。

Git 分支

20240918_dev_coupon-remind-v1_rocketmq-bitmap_youya

本章节预约提醒优惠券核心代码由优雅同学贡献,感谢优雅提供的优秀代码设计。

数据库表设计

1. 用户预约表 SQL

CREATE TABLE `t_coupon_template_remind` (
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`coupon_template_id` bigint(20) NOT NULL COMMENT '券ID',
`information` bigint(20) DEFAULT NULL COMMENT '存储信息',
`shop_number` bigint(20) DEFAULT NULL COMMENT '店铺编号',
`start_time` datetime DEFAULT NULL COMMENT '优惠券开抢时间',
PRIMARY KEY (`user_id`,`coupon_template_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户预约提醒信息存储表';

2. 是否分库分表?

考虑到主键由 coupon_template_iduser_id 组成,均为 bigint 类型,各占 8 字节,总计 16 字节。MySQL 的页大小为 16KB。

  • 索引页:每条记录由主键(8 字节 + 8 字节)加上指针(6 字节)组成,共计 22 字节。因此,索引页最多可存储:16,384 字节 ÷ 22 字节 ≈ 744 条记录。
  • 数据页:每条记录包含主键(16 字节)、information(8 字节)、shop_number(8 字节)和 start_time(8 字节),合计 40 字节。因此,数据页最多可存储:16,384 字节 ÷ 40 字节 ≈ 410 条记录。

在三层 B+ 树结构下,最大可存储的记录数为:744(第一层)× 744(第二层)× 410(叶子节点)≈ 226,492,160 条记录。

即大约 2.26 亿条记录。考虑到实际情况中页内可能存在的其他开销,这个数值可能会略少,但至少可以支持 2 亿条记录

假设用户量为 5000 万,那么平均每个用户可以预约 4 张券,完全满足需求。

通过设置定时任务,定期删除已过期的记录,可以有效控制数据量。因此,单表结构已经足够承载当前业务需求,无需进行分库分表

3. 为什么不设置主键 ID?

如果我们额外创建一个自增的 id 作为主键,那么除了主键索引外,我们仍然需要通过 (user_id, coupon_template_id) 唯一定位一条记录。这意味着我们还需要建立一个联合索引 (user_id, coupon_template_id) 来提高查询性能。因此,额外添加一个 id 作为主键并没有实际意义。

没有显示给出主键的话,MySQL 会找个唯一索引做主键,如果没有唯一索引,就给个隐藏主键。我们通过唯一索引字段作为主键,可以节省额外的主键字段大小。

由于 (user_id, coupon_template_id) 本身就是唯一的,所以直接将其作为主键既能保证唯一性,又能提升查询效率。

为什么选择 (user_id, coupon_template_id) 而不是 (coupon_template_id, user_id) 作为主键?

因为用户可能会查询自己预约了哪些提醒券,我们需要根据 user_id 来检索他预约的所有券。将 user_id 放在前面,可以符合索引的最左前缀匹配原则,显著提高查询性能。

4. 存储信息指的是什么?

一个用户可以有多个券,每个券的信息都是用位图(存储信息字段)存储的,可以存用户对该优惠券模板的所有预约消息,一个 Long 类型的字段就搞定了。

存储信息字段中,我们使用 5 分钟作为一个间隔,支持提前一小时提醒,也就是最多支持一共有 12 个值。并且,我们能支持 5 种通知方式,通过位图的设计思路放到一个字段里,详情看下文描述。

解锁付费内容,👉 戳