目录
优惠卷原型分析
推广方式就是用户手动去领还是使用兑换码去兑换
优惠卷规则可以使用优惠卷类型去判断
-
优惠类型:天机学堂支持的类型有 1:满减,2:每满减,3:折扣,4:无门槛
-
优惠券状态:包括 1:待发放,2:未开始 3:进行中,4:已结束,5:暂停
使用返回 可以存0 1 适用范围 在另外一张表中存储
其中的字段包含:
-
优惠券名称:一个普通字符串
-
使用范围:这里有两种选择:全部课程、指定课程分类,也就是不限定课程、限定课程,可以用布尔类型来表示。不过一旦选定了课程分类,就需要指定真正限定的分类。
此处是允许多选的,也就是说一个优惠券可以限定多个课程分类。而一个分类也可能被不同的券作为限定范围。因此优惠券与限定的分类是多对多关系。需要一张中间表来保存关系。这个以后再说。
-
优惠券类型:包含满减、每满减、满折扣、无门槛四种,例如:
-
满100减15
-
每满100减10
-
满200打8折,不超过50
-
直减20
-
可以看出来,虽然规则不同,但都可以用以下几部分来表示:
-
优惠的门槛:比如满100的100
-
优惠值:比如减15的15、打8折的8
-
优惠上限:比如不超过50
因此,我们完全要表示完整优惠策略就需要四个字段:优惠类型、优惠门槛、优惠值、优惠上限
-
推广方式:手动领取和指定发放
-
发放数量
-
每人限领数量
与新增页面重复的就不再赘述了,这里多出的一些字段有:
-
已领取数量
-
已使用数量
-
领用期限:也就是优惠券开始发放、结束发放的时间
-
使用期限:用户领取券后的使用期限,有两种方式:
-
固定时间段:需要指定开始时间、结束时间
-
固体天数:指定天数,从用户领取之日起计算
-
优惠券状态
-- 导出 表 tj_promotion.coupon 结构
CREATE TABLE IF NOT EXISTS `coupon` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '优惠券id',
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '0' COMMENT '优惠券名称,可以和活动名称保持一致',
`type` tinyint NOT NULL DEFAULT '1' COMMENT '优惠券类型,1:普通券。目前就一种,保留字段',
`discount_type` tinyint NOT NULL COMMENT '折扣类型,1:满减,2:每满减,3:折扣,4:无门槛',
`specific` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否限定作用范围,false:不限定,true:限定。默认false',
`discount_value` int NOT NULL DEFAULT '1' COMMENT '折扣值,如果是满减则存满减金额,如果是折扣,则存折扣率,8折就是存80',
`threshold_amount` int NOT NULL DEFAULT '0' COMMENT '使用门槛,0:表示无门槛,其他值:最低消费金额',
`max_discount_amount` int NOT NULL DEFAULT '0' COMMENT '最高优惠金额,满减最大,0:表示没有限制,不为0,则表示该券有金额的限制',
`obtain_way` tinyint NOT NULL DEFAULT '0' COMMENT '获取方式:1:手动领取,2:兑换码',
`issue_begin_time` datetime DEFAULT NULL COMMENT '开始发放时间',
`issue_end_time` datetime DEFAULT NULL COMMENT '结束发放时间',
`term_days` int NOT NULL DEFAULT '0' COMMENT '优惠券有效期天数,0:表示有效期是指定有效期的',
`term_begin_time` datetime DEFAULT NULL COMMENT '优惠券有效期开始时间',
`term_end_time` datetime DEFAULT NULL COMMENT '优惠券有效期结束时间',
`status` tinyint DEFAULT '1' COMMENT '优惠券配置状态,1:待发放,2:未开始 3:进行中,4:已结束,5:暂停',
`total_num` int NOT NULL DEFAULT '0' COMMENT '总数量,不超过5000',
`issue_num` int NOT NULL DEFAULT '0' COMMENT '已发行数量,用于判断是否超发',
`used_num` int NOT NULL DEFAULT '0' COMMENT '已使用数量',
`user_limit` int NOT NULL DEFAULT '1' COMMENT '每个人限领的数量,默认1',
`ext_param` json DEFAULT NULL COMMENT '拓展参数字段,保留字段',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`creater` bigint NOT NULL COMMENT '创建人',
`updater` bigint NOT NULL COMMENT '更新人',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1630563495906942979 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='优惠券的规则信息';
兑换码
兑换码的作用是让用户拿着这个码来兑换一张优惠券。因此一定与两个实体有关:
-
优惠券
-
用户
也就是说,我们需要知道将来是谁来兑换的券,可以兑换哪张券。
新增优惠卷
重点是这里面的stream流的用法
兑换码生成算法
兑换码是用来兑换优惠卷的,32BIT的自增id 权重与32位加权作为秘钥,多种权重值,4位新鲜值来判断选择作为权重进行加权,加权得出来的数就是签名
要求如下:
-
可读性好:兑换码是要给用户使用的,用户需要输入兑换码,因此可读性必须好。我们的要求:
-
长度不超过10个字符
-
只能是24个大写字母和8个数字:ABCDEFGHJKLMNPQRSTUVWXYZ23456789
-
-
数据量大:优惠活动比较频繁,必须有充足的兑换码,最好有10亿以上的量
-
唯一性:10亿兑换码都必须唯一,不能重复,否则会出现兑换混乱的情况
-
不可重兑:兑换码必须便于校验兑换状态,避免重复兑换
-
防止爆刷:兑换码的规律性不能很明显,不能轻易被人猜测到其它兑换码
-
高效:兑换码生成、验证的算法必须保证效率,避免对数据库带来较大的压力
要满足唯一性,很多同学会想到以下技术:
-
UUID
-
Snowflake
-
自增id
我们的兑换码要求是24个大写字母和8个数字。而以上算法最终生成的结果都是数值类型,并不符合我们的需求!
有没有什么办法,可以把数字转为我们要求的格式呢?
Base32
- Base32编码是使用32个可打印字符(字母A-Z和数字2-7)对任意字节数据进行编码的方案,编码后的字符串不用区分大小写并排除了容易混淆的字符,可以方便地由人类使用并由计算机处理。
- Base32主要用于编码二进制数据,但是Base32也能够编码诸如ASCII之类的二进制文本。
- Base32将任意字符串按照字节进行切分,并将每个字节对应的二进制值(不足8比特高位补0)串联起来,按照5比特一组进行切分,并将每组二进制值转换成十进制来对应32个可打印字符中的一个。
那自增id算法符合我们的需求呢?
自增id从1增加到Integer的最大值,可以达到40亿以上个数字,而占用的字节仅仅4个字节,也就是32个bit位,距离50个bit位的限制还有很大的剩余,符合要求!
综上,我们可以利用自增id作为兑换码,但是要利用Base32加密,转为我们要求的格式。此时就符合了我们的几个要求了:
-
可读性好:可以转为要求的字母和数字的格式,长度还不超过10个字符
-
数据量大:可以应对40亿以上的数据规模
-
唯一性:自增id,绝对唯一
重兑校验算法
那重兑问题该如何判断呢?此处有两种方案:
-
基于数据库:我们在设计数据库时有一个字段就是标示兑换码状态,每次兑换时可以到数据库查询状态,避免重兑。
-
优点:简单
-
缺点:对数据库压力大
-
-
基于BitMap:兑换或没兑换就是两个状态,对应0和1,而兑换码使用的是自增id.我们如果每一个自增id对应一个bit位,用每一个bit位的状态表示兑换状态,是不是完美解决问题。而这种算法恰好就是BitMap的底层实现,而且Redis中的BitMap刚好能支持2^32个bit位。
-
优点:简答、高效、性能好
-
缺点:依赖于Redis现在,就剩下防止爆刷了。我们的兑换码规律性不能太明显,否则很容易被人猜测到其它兑换码。但是,如果我们使用了自增id,那规律简直太明显了,岂不是很容易被人猜到其它兑换码?!
-
-
所以,我们采用自增id的同时,还需要利用某种校验算法对id做加密验证,避免他人找出规律,猜测到其它兑换码,甚至伪造、篡改兑换码。
那该采用哪种校验算法呢?
防刷校验算法
非常可惜,没有一种现成的算法能满足我们的需求,我们必须自己设计一种算法来实现这个功能。
不过大家不用害怕,我们可以模拟其它验签的常用算法。比如大家熟悉的JWT技术。我们知道JWT分为三部分组成:
-
Header:记录算法
-
Payload:记录用户信息
-
Verify Signature:验签,用于验证整个token
JWT中的的Header和Payload采用的是Base64算法,与我们Base32类似,几乎算是明文传输,难道不怕其他人伪造、篡改token吗?
为了解决这个问题,JWT中才有了第三部分,验证签名。这个签名是有一个秘钥,结合Header、Payload,利用MD5或者RSA算法生成的。因此:
-
只要秘钥不泄露,其他人就无法伪造签名,也就无法伪造token。
-
有人篡改了token,验签时会根据header和payload再次计算签名。数据被篡改,计算的到的签名肯定不一致,就是无效token
因此,我们也可以模拟这种思路:
-
首先准备一个秘钥
-
然后利用秘钥对自增id做加密,生成签名
-
将签名、自增id利用Base32转码后生成兑换码
只要秘钥不泄露,就没有人能伪造兑换码。只要兑换码被篡改,就会导致验签不通过
异步生成兑换码
开启异步调用需要两个注解:启动类上@EnaleAsync 方法上@Async
异步用的是线程池里面的线程 ,2.0.9之后用的是threadpoolTaskExecutor中的注解,但是默认的线程池线程队列长度是int的最大值,会有OOM风险,所以最好重写
@Slf4j
@Configuration
public class PromotionConfig {
@Bean
public Executor generateExchangeCodeExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 1.核心线程池大小
executor.setCorePoolSize(2);
// 2.最大线程池大小
executor.setMaxPoolSize(5);
// 3.队列大小
executor.setQueueCapacity(200);
// 4.线程名称
executor.setThreadNamePrefix("exchange-code-handler-");
// 5.拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
然后在注解后面指明用的哪个线程池
面试中文那个地方用到线程池?
异步生成兑换码,延迟队列中拉取到任务后,保存数据库任务的执行(提交学习记录到数据库)
mybatisplus中对应数据库的三种主要主键策略
auto 自增,此时是以数据库的自增id为主,这个时候数据库主键必须是自增的
input 这个时候是以手动输入为主
雪花算法
redis incr和incrBy的使用
用incr和incrBy在接口里做了下埋点统计每天请求总数,这两个命令还是挺好用的,先说下这俩命令吧
注:redis后台服务是串行的单线程执行,不存在并发,即多线程调用Incr/incrby方法,在redis服务器上仍然是串行的单线程执行,不存在并发,所以这俩命令都是原子自增、线程安全的。
Redis Incr 命令将 key 中储存的数字值增一。
如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。
Redis Incrby 命令将 key 中储存的数字加上指定的增量值。
如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCRBY 命令。
keyi
使用redis-incr实现流量控制
这里我们将在java中使用redis-incr的特性来构建一个1分钟内只允许 请求100次的控制代码,key代表在redis内存放的被控制的键值。
面试
面试官:你们优惠券支持兑换码的方式是吧,哪兑换码是如何生成的呢?(请设计一个优惠券兑换码生成方案,可以支持20亿以上的唯一兑换码,兑换码长度不超过10,只能包含字母数字,并且要保证生成和校验算法的高效)
不需要数据库交互,效率很高