当前位置:网站首页>设计一个抢红包系统
设计一个抢红包系统
2022-07-06 15:51:00 【OoZzzy】
1.需求分析
常见的红包系统,由用户指定金额、红包总数来完成红包的创建,然后通过某个入口将红包下发至目标用户,用户看到红包后,点击红包,随机获取红包,最后,用户可以查看自己抢到的红包。整个业务流程不复杂,难点在于抢红包
这个行为可能有很高的并发
。所以,系统设计的优化点主要关注在抢红包
这个行为上。
- 发红包:用户设置红包总金额、总数量
- 抢红包:用户从总红包中随机获得一定金额
抢红包必须保证高可用,不然用户会很愤怒。其次,必须保证系统数据一致性不能超发,不然抢到红包的用户收不到钱,用户会很愤怒。最后一点,系统可能会有很高的并发。
2.表结构设计
红包活动表
CREATE TABLE `t_redpack_activity`
(
`id` bigint(20) NOT NULL COMMENT '主键',
`total_amount` decimal(10, 2) NOT NULL DEFAULT '0.00' COMMENT '总金额',
`surplus_amount` decimal(10, 2) NOT NULL DEFAULT '0.00' COMMENT '剩余金额',
`total` bigint(20) NOT NULL DEFAULT '0' COMMENT '红包总数',
`surplus_total` bigint(20) NOT NULL DEFAULT '0' COMMENT '红包剩余总数',
`user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '用户编号',
`version` bigint(20) NOT NULL DEFAULT '0' COMMENT '版本号',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
红包表
CREATE TABLE `t_redpack`
(
`id` bigint(20) NOT NULL COMMENT '主键',
`activity_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '红包活动ID',
`amount` decimal(10, 2) NOT NULL DEFAULT '0.00' COMMENT '金额',
`status` TINYINT(4) NOT NULL DEFAULT 0 COMMENT '红包状态 1可用 2不可用',
`version` bigint(20) NOT NULL DEFAULT '0' COMMENT '版本号',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
明细表
CREATE TABLE `t_redpack_detail`
(
`id` bigint(20) NOT NULL COMMENT '主键',
`amount` decimal(10, 2) NOT NULL DEFAULT '0.00' COMMENT '金额',
`user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '用户编号',
`redpack_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '红包编号',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
活动表,就是你发了多少个红包,并且需要维护剩余金额。明细表是用户抢到的红包明细。红包表是每一个具体的红包信息。为什么需要三个表呢?事实上如果没有红包表也是可以的。但我们的方案预先分配红包需要使用一张表来记录红包的信息,所以设计的时候才有此表。
3.基于分布式锁的实现
基于分布式锁的实现最为简单粗暴,整个抢红包接口以activityId作为key进行加锁,保证同一批红包抢行为都是串行执行。分布式锁的实现是由spring-integration-redis工程提供,核心类是RedisLockRegistry。锁通过Redis的lua脚本实现,且实现了阻塞式本地可重入。
4.基于乐观锁的实现
第二种方式,为红包活动表增加乐观锁版本控制,当多个线程同时更新同一活动表时,只有一个 clien 会成功。其它失败的 client 进行循环重试,设置一个最大循环次数即可。此种方案可以实现并发情况下的处理,但是冲突很大。因为每次只有一个人会成功,其他 client 需要进行重试,即使重试也只能保证一次只有一个人成功,因此 TPS 很低。当设置的失败重试次数小于发放的红包数时,可能导致最后有人没抢到红包,实际上还有剩余红包。
5.基于悲观锁的实现
由于红包活动表增加乐观锁冲突很大,所以可以考虑使用使用悲观锁:select * from t_redpack_activity where id = #{id} for update,注意悲观锁必须在事务中才能使用。此时,所有的抢红包行为变成了串行。此种情况下,悲观锁的效率远大于乐观锁。
6.预先分配红包,基于乐观锁的实现
可以看到,如果我们将乐观锁的维度加在红包明细上,那么冲突又会降低。因为之前红包明细是用户抢到后才创建的,那么现在需要预先分配红包,即创建红包活动时即生成 N 个红包,通过状态来控制可用/不可用。这样,当多个 client 抢红包时,获取该活动下所有可用的红包明细,随机返回其中一条然后再去更新,更新成功则代表用户抢到了该红包,失败则代表出现了冲突,可以循环进行重试。如此,冲突便被降低了。
7.基于 Redis 队列的实现
和上一个方案类似,不过,用户发放红包时会创建相应数量的红包,并且加入到 Redis 队列中。抢红包时会将其弹出。Redis队列很好的契合了我们的需求,每次弹出都不会出现重复的元素,用完即销毁。缺陷:抢红包时一旦从队列弹出,此时系统崩溃,恢复后此队列中的红包明细信息已丢失,需要人工补偿。
8.基于 Redis 队列,异步入库
这种方案的是抢到红包后不操作数据库,而是保存持久化信息到Redis中,然后返回成功。通过另外一个线程UserRedpackPersistConsumer,拉取持久化信息进行入库。需要注意的是,此时的拉取动作如果使用普通的pop仍然会出现crash point的问题,所以考虑到可用性,此处使用Redis的BRPOPLPUSH操作,弹出元素后加入备份到另外一个队列,保证此处崩溃后可以通过备份队列自动恢复。崩溃恢复线程CrashRecoveryThread通过定时拉取备份信息,去 DB 中查证是否持久化成功,如果成功则清除此元素,否则进行补偿并清除此元素。如果在操作数据库的过程中出现异常会记录错误日志redpack.persist.log,此日志使用单独的文件和格式,方便进行补偿(一般不会触发)。
Redis则需要做高可用。
边栏推荐
- Ajout, suppression et modification d'un tableau json par JS
- Word2vec (skip gram and cbow) - pytorch
- Without CD, I'll teach you a trick to restore the factory settings of win10 system
- Is the more additives in food, the less safe it is?
- (DART) usage supplement
- Unified Focal loss: Generalising Dice and cross entropy-based losses to handle class imbalanced medi
- Graphite document: four countermeasures to solve the problem of enterprise document information security
- 前置机是什么意思?主要作用是什么?与堡垒机有什么区别?
- 企業不想換掉用了十年的老系統
- A few suggestions for making rust library more beautiful! Have you learned?
猜你喜欢
Case recommendation: An Qing works with partners to ensure that the "smart court" is more efficient
11 preparations for Web3 and Decentralization for traditional enterprises
每日刷题记录 (十五)
MySQL implementation of field segmentation from one line to multiple lines of example code
koa2对Json数组增删改查
How to choose indoor LED display? These five considerations must be taken into account
DockerMySQL无法被宿主机访问的问题解决
B 站弹幕 protobuf 协议还原分析
Today's sleep quality record 78 points
Restoration analysis of protobuf protocol of bullet screen in station B
随机推荐
让 Rust 库更优美的几个建议!你学会了吗?
Detailed explanation of regular expression (regexp) in MySQL
If the request URL contains jsessionid, the solution
PDF批量拆分、合并、书签提取、书签写入小工具
The worse the AI performance, the higher the bonus? Doctor of New York University offered a reward for the task of making the big model perform poorly
CRMEB 商城系统如何助力营销?
Case recommendation: An Qing works with partners to ensure that the "smart court" is more efficient
Cover fake big empty talk in robot material sorting
Without CD, I'll teach you a trick to restore the factory settings of win10 system
Coscon'22 community convening order is coming! Open the world, invite all communities to embrace open source and open a new world~
Can async i/o be implemented by UDF operator and then called by SQL API? At present, it seems that only datastre can be seen
mysql拆分字符串作为查询条件的示例代码
Hard core observation 545 50 years ago, Apollo 15 made a feather landing experiment on the moon
Summary of three methods for MySQL to view table structure
企业不想换掉用了十年的老系统
COSCon'22 社区召集令来啦!Open the World,邀请所有社区一起拥抱开源,打开新世界~
请问async i/o可以由udf算子实现然后用sql api调用吗?目前好像只看到Datastre
每日刷题记录 (十五)
Up to 5million per person per year! Choose people instead of projects, focus on basic scientific research, and scientists dominate the "new cornerstone" funded by Tencent to start the application
How does crmeb mall system help marketing?