【业务场景】详谈电商优惠系统的设计与实现
目录
1.优惠活动
1.1.难点
1.2.实体
1.3.如何实现开闭原则
1.4.伪代码实例
2.优惠券
2.1.流程
2.2.难点
2.3.实体
2.4.如何大批量给用户发券
2.4.如何防止用户重复领券
2.5.优惠券如何过期
2.6.分布式事务问题
1.优惠活动
1.1.难点
优惠活动的优惠规则有很多,不可能每上一个优惠活动都要根据其优惠规则的不同而去修改代码,所以优惠活动的核心难点在于如何在在开闭原则的基础上适配各种优惠规则。
开闭原则:对外开放扩展,对内关闭修改,即通过不修改已有内部代码的方式,通过扩展外部(新的类)来实现新的功能。
开闭原则是面向对象的终极追求,尤其适合处理多情况的业务。
1.2.实体
优惠规则=条件+优惠
条件有多种,优惠也有多种。
1.3.如何实现开闭原则
抽象一下上面的整个过程:
发起下单请求,触发优惠动作,针对不同的请求相同的一个节点(优惠活动)会呈现出不同的状态。
这是什么?多态。
实现多态当然是抽象父类+实现子类的方法来实现对不同请求的不同响应。
总结:
面对不同的请求做出不同的响应,且响应是可动态扩展的,就要考虑用上多态。
开闭原则的实现方法:
优惠活动实体=优惠+折扣,条件+折扣和优惠活动是组合关系,条件、折扣从外部传入。
优惠和条件两个实体分别抽象出父类来实现多态。
优惠:
优惠有三种:
-
立减
-
满减
-
折扣
类图上展示的就是这三种。
条件:
优惠条件三种:
-
某些种类
-
达到某个总量(价格或者数量)
-
以上两者兼有
整个优惠活动流程:
在折扣中组合条件即可。
1.4.伪代码实例
条件基类:
public abstract class BaseCouponLimitation {
public abstract boolean pass();
}
优惠基类:
public abstract class BaseCouponDiscount {
//条件
List<BaseCouponLimitation> couponLimitations=new ArrayList<>();
public void setCouponLimitations(List<BaseCouponLimitation> couponLimitations){
this.couponLimitations=couponLimitations;
}
public void addCouponLimitation(BaseCouponLimitation couponLimitation){
this.couponLimitations.add(couponLimitation);
}
public abstract void compute();
}
用一个List来实现支持多条件下的优惠。
总量条件:
/**
* 满多少
*/
public class AmountCouponLimitation extends BaseCouponLimitation{
@Override
public boolean pass() {
//做个判断,满多少才生效
return true;
}
}
立减优惠:
public class PriceCouponDiscount extends BaseCouponDiscount{
{
addCouponLimitation(new AmountCouponLimitation());
}
@Override
public void compute() {
couponLimitations.forEach(couponLimitation->{
if(couponLimitation.pass()){
//减多少钱
}
});
}
}
使用:
public class test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
BaseCouponDiscount couponDiscount=new PriceCouponDiscount();
//进行优惠
couponDiscount.compute();
}
}
2.优惠券
2.1.流程
优惠券的流程:
整个流程里面需要注意的点:
-
活动页面长什么样子
-
发券后需要进行消息通知,如短信、微信公众号等
-
退还,根据具体的业务来决定什么时候要退还优惠券,什么时候不退还优惠券。常见的策略如下
-
用户下单未支付,取消订单,优惠券可退还
-
商家侧在订单未完成,发起退款,优惠券可退还
-
下单已完成后,申请退款,优惠券不可退还
-
这是常见的活动界面,在做活动页的时候可做参考:
2.2.难点
-
如何大批量给用户发券
-
如何限制券的使用条件
-
如何防止用户重复领券(超发)
-
优惠券如何过期
-
分布式事务问题
2.3.实体
优惠券应该由什么组成?
优惠卷由于不能被伪造,所以需要有唯一性,也就是:
券码+一些模板信息(如活动名、规则、时效期等)
一些详细信息,我们可以把它抽象成优惠券的模板,一类优惠券的这部分信息是相同的,不同的只有券码。
也就是说:
一张优惠券=券码+模板
券码:
券码可以这样去生成:
当然这只是参考方案,可以根据需要用自己的生成方案。
模板:
模板信息应该包含:
-
基本信息
-
名称、发放数量、是否可叠加、每人限领张数、是否可以和其它促销同时使用、使用规则
-
-
优惠类型
-
满减、直减、折扣
-
-
使用范围
-
指定用户类型,新用户?老用户?指定哪些区域、哪些商品
-
-
有效期
-
有效时间
-
优惠券模板实体:
create table template
(
id int,
name varchar(50),
rule_id int comment '规则id,外键',
total_count int '总数量',
assign_count int '已发数量',
used_count int ’已使用数量‘
)
规则实体:
create table rule
(
rule_id comment '规则id',
name varchar(50) ,
type int comment ’优惠券类型,0-满减,1-折扣‘,
rule_content blob comment '规则内容'
)
规则内容,是个json,用来记录规则的细节:
{
threshold: 5.01 // 使用门槛
amount: 5 // 优惠金额
use_range: 3 // 使用范围,0—全场,1—商家,2—类别,3—商品
commodity_id: 10 // 商品 id
receive_count: 1 // 每个用户可以领取的数量
is_mutex: true // 是否互斥,true 表示互斥,false 表示不互斥
receive_started_at: 2020-11-1 00:08:00 // 领取开始时间
receive_ended_at: 2020-11-6 00:08:00 // 领取结束时间
use_started_at: 2020-11-1 00:00:00 // 使用开始时间
use_ended_at: 2020-11-11 11:59:59 // 使用结束时间
}
2.4.如何大批量给用户发券
优惠券的领取有点类似于秒杀场景,多用户去抢,所以首先就应该将优惠券放在redis中。其次如果领取的时候才生成,太吃性能了,应该先生成在redis中,再让用户去抢。
抢到后再异步落库。
2.4.如何防止用户重复领券
这种发券时的架构,不难发现会存在超领问题。用户领取优惠券后由于是异步写,在没落库的时候,又去领取就没地方进行校验,会导致一个用户领取多张。
这时候就会想到,分布式锁行不行?
答案是不太行,首先是用到了Kafka进行异步,锁不住是一个问题。其次用redis本来就是想用来拉高吞吐量以应对高并发,如果用锁把redis操作和mq、数据库操作锁在一起,明显就将redis的吞吐量拉到和mq、数据库一个量级了。没起到引入redis的初衷,所以肯定不行。
怎么办?
在redis中维护一个用户的id集合,用来记录哪些用户是已经有该优惠券了的,利用这个redis的集合来进行超发的校验,mq的异步落库就不会影响到重复领券的校验。
2.5.优惠券如何过期
两种过期方式:
-
主动过期,用定时任务来扫,不是实时性的,这是定时任务的通病。
-
被动过期,使用的时候(打开优惠券列表)再校验优惠券是否可用。
2.6.分布式事务问题
一次下单+优惠会调用到很多服务,要注意处理分布式事务:
分布式事务可以异步作者另一篇文章:
详谈分布式事务_sharding proxy分布式事务-CSDN博客
分布式事务_bugman csdn-CSDN博客