10小节:开发支持百万预约优惠券功能
作者:程序员马丁
热门项目实战社群,收获国内众多知名公司面试青睐,近千名同学面试成功!助力你在校招或社招上拿个offer。
开发优惠券预约通知功能(一),元数据信息:
- 什么是牛券oneCoupon:https://t.zsxq.com/pAWgS
- 代码仓库:https://gitcode.net/nageoffer/onecoupon —— 申请项目权限参考上述牛券项目链接
- 章节难度:★★★★☆ - 很难
- 视频地址:文档先行视频次之
©版权所有 - 拿个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_id
和 user_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 种通知方式,通过位图的设计思路放到一个字段里,详情看下文描述。