优惠券表结构设计,设计一个优惠券系统
高价值的内容是做好企业营销推广的前提,如果你对企业内容营销感兴趣的话,不妨看看MarketUP近期整理的《2023内容营销获客实战白皮书》,希望能给大家有一些实质性的帮助,预计发布400份!送完下架,赶快领取!感兴趣的朋友可以点击链接即可下载阅读:《2023内容营销获客实战白皮书》
背景
部门为一个租房房源平台,为各个商家提供房源发布&C端曝光获客的功能,现在要构建一个优惠券系统,用于各个节假日节点进行商家营销活动。形式主要以商家在B端参与活动,对房源绑定优惠券,将租赁价格进行优惠,来在C端吸引用户进行租房。
1. 业务梳理
在清楚了大致的业务背景后,下面来进行整体的业务流程梳理,大致如下图所示。
首先,平台建立好活动,在商家B端将可报名的活动展示出来,商家通过报名对应优惠力度的活动,来建立对应优惠的优惠券。
然后,通过将房源与对应的优惠券建立绑定关系,来对房源数据打上优惠券标识。这样一来在C端展示房源时,就可以进行对应的优惠房源筛选,以及让用户在房源上进行领取某个类别的优惠券。
最后,用户在C端领取优惠券后,可联系商家进行实地房源考察,如果双方达成协议,即可在线上签约。而在签约时即可使用对应优惠券,实现相应的价格优惠。
至此,就是整个系统的完整正向流程了。
2. 技术设计
下面来对每个环节的进行相应的技术设计。
2.1 建立活动
2.1.1 数据表
活动信息需要如下数据项
下面就是活动信息数据表的具体设计
CREATETABLE`t_activity`( `activeId`bigint(20) NOTNULLCOMMENT’活动ID’, `title`varchar(256) NOTNULLCOMMENT’活动名称’, `applyStartTime`timestampNULLDEFAULTNULLCOMMENT’报名开始时间’, `applyEndTime`timestampNULLDEFAULTNULLCOMMENT’报名停止时间’, `activityStartTime`timestampNULLDEFAULTNULLCOMMENT’活动开始时间’, `activityEndTime`timestampNULLDEFAULTNULLCOMMENT’活动结束时间’, `cityIds`varchar(256) NOTNULLCOMMENT’覆盖城市,多个逗号分隔’, `couponType`tinyint(4) NOTNULLDEFAULT’0’COMMENT’优惠类型,1 直减;2 折扣;3免费住N天;4免押金;5特价房’, `lowerLimit`intNOTNULLDEFAULT0COMMENT’优惠数值下限’, `upperLimit`intNOTNULLDEFAULT0COMMENT’优惠数值上限’, `description`textCOMMENT’活动描述’, `cubeType`smallint(6) NOTNULLDEFAULT’1001’COMMENT’活动类型’, `foreignId`bigint(20) NOTNULLDEFAULT’0’COMMENT’外部ID’, `status`tinyint(4) NOTNULLDEFAULT’0’COMMENT’活动状态’, `createTime`timestampNULLDEFAULTNULLCOMMENT’创建时间’, `updateTime`timestampNULLDEFAULTNULLCOMMENT’更新时间’, `recordStatus`tinyint(4) NOTNULLDEFAULT’0’COMMENT’数据状态’, PRIMARY KEY(`activeId`) ) ENGINE=InnoDBDEFAULTCHARSET=utf8 COMMENT=’活动信息表’;
2.1.2 数据读取
在商家端可以通过status字段和cityIds字段来进行可报名活动的展示。
C端读取的展示代码里可以使用设计模式中的代理模式来加一层缓存,在进行中的活动会将数据推入到cache层中。
2.1.3 活动状态流转
通过crontab定时任务,每分钟进行时间段的检查,来更新对应的status字段,完成活动状态的流转。
2.1.4 数据读取
可以使用设计模式中的代理模式来加一层缓存,非强实时的查询走cache,cache中不存在或需实时数据的再走db。
2.2 商家报名建券
2.2.1 数据表
CREATETABLE`t_couponmeta`( `couponMetaId`bigint(20) NOTNULLAUTO_INCREMENT COMMENT’券id’, `appId`int(11) NOTNULLDEFAULT’1’COMMENT’区分建立来源’, `activeId`bigint(20) NOTNULLDEFAULT’0’COMMENT’活动ID’, `companyId`bigint(20) NOTNULLCOMMENT’公司编号’, `cityId`int(11) NOTNULLCOMMENT’城市id’, `companyName`varchar(255) DEFAULTNULLCOMMENT’公司名称’, `companyShortName`varchar(255) DEFAULTNULLCOMMENT’公司简称’, `couponType`tinyint(4) NOTNULLCOMMENT’优惠券类型’, `title`varchar(256) NOTNULLCOMMENT’优惠券名称’, `directDiscount`int(11) NOTNULLDEFAULT’0’COMMENT’直减券优惠力度’, `discount`int(11) NOTNULLDEFAULT’0’COMMENT’折扣力度’, `freeLive`int(11) NOTNULLDEFAULT’0’COMMENT’免费住n天券’, `threshold`varchar(256) NOTNULLCOMMENT’使用门槛’, `deduction`tinyint(4) NOTNULLDEFAULT’1’COMMENT’抵扣说明 1首月抵扣,2 平摊到月’, `totalAmount`int(11) NOTNULLDEFAULT’0’COMMENT’券总数’, `applyAmount`int(11) NOTNULLDEFAULT’0’COMMENT’已领取总数’, `activityStartTime`timestampNULLDEFAULTNULLCOMMENT’活动开始时间’, `activityEndTime`timestampNULLDEFAULTNULLCOMMENT’活动结束时间’, `startTime`timestampNULLDEFAULTNULLCOMMENT’券使用开始时间’, `expireTime`timestampNULLDEFAULTNULLCOMMENT’券使用结束时间’, `status`int(11) NOTNULLDEFAULT’10’COMMENT’10:新建未启用,20:已启用,30:过期, 40 已结束 50 已中止’, `expireType`tinyint(4) NOTNULLDEFAULT’1’COMMENT’类型:1固定有效期类型,2浮动有效期类型’, `validPeriod`tinyint(4) NOTNULLDEFAULT’0’COMMENT’浮动有效期(单位:天)’, `tenantRange`tinyint(1) NOTNULLDEFAULT’1’COMMENT’租客范围枚举值’, `customScope`varchar(256) NOTNULLDEFAULT”COMMENT’自定义租客范围’, `comment`varchar(50) DEFAULTNULLCOMMENT’备注’, `cubeType`smallint(6) NOTNULLDEFAULT’1001’COMMENT’活动类型’, `updateTime`timestampNULLDEFAULTNULLCOMMENT’更新时间’, `createTime`timestampNULLDEFAULTNULLCOMMENT’创建时间’, `recordStatus`tinyint(4) DEFAULT’0’COMMENT’状态 0默认 -1删除’, PRIMARY KEY(`couponMetaId`) ) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT=’优惠券表’;
2.2.2 数据读取
同样可以使用设计模式中的代理模式来加一层缓存,对于C端大量读取优惠券信息的场景,需要让读请求尽量都在cache层处理完成,降低db的压力。
2.2.3 状态流转
优惠券的状态流转如图所示
大部分状态取决于活动的状态(除中止和过期),所以在活动状态的crontab任务中,当发现活动状态发生变更后,会将下面的优惠券状态变更任务以MQ发送出来,异步来修改优惠券的状态。而优惠券状态的变更又会涉及联动数据的更新,举例来说:
可以想到,在每个状态流转的时候,都有很多操作来做。为了保证业务逻辑清晰,状态流转我采用了状态模式来进行实现,类图如下所示:
而状态更新后,C端房源索引数据/b端基础房源数据/缓存中的数据,这些数据的更新使用了观察者模式监听状态的变更,在观察者中通过发送mq异步来进行数据更新,类图如下所示:
通过将需要的观察者注册到CouponStateMachine中,在进行实际的doChangeStatus操作后,notify所有的观察者即可保证联动数据的正确性。
2.3 绑定优惠券
2.3.1 数据表
将商家b端绑定了的优惠券全量(新建/启用中/未过期)数据存于MySQL数据表中,表结构比较简单,如下所示:
CREATETABLE`t_bindcoupon`( `id`bigint(20) NOTNULLAUTO_INCREMENT COMMENT’id’, `couponMetaId`int(11) NOTNULLCOMMENT’券id’, `companyId`bigint(20) NOTNULLCOMMENT’公司编号’, `activityStatus`tinyint(4) NOTNULLCOMMENT’状态 0 准备中 1 活动中 2 活动结束 券未失效 3活动结束券失效’, `houseId`bigint(20) NOTNULLDEFAULT’0’COMMENT’房源id’, `recordStatus`tinyint(4) NOTNULLCOMMENT’数据状态 0 有效,-1 失效’, `createTime`timestampNULLDEFAULTNULLCOMMENT’创建时间’, `updateTime`timestampNULLDEFAULTNULLCOMMENT’更新时间’, PRIMARY KEY(`id`), KEY`idx_companyId_couponMetaId`(`companyId`,`couponMetaId`), KEY`idx_planeId`(`planeId`), KEY`idx_houseid_activitystatus`(`houseId`,`activityStatus`) ) ENGINE=InnoDBAUTO_INCREMENT=17379DEFAULTCHARSET=utf8 COMMENT=’优惠券绑定范围表’
该表主要是记录房源和优惠券之间的绑定关系,并记录当前绑定的状态,该状态用于限制同一个房源不可以绑太多的有效优惠券,限制商家为了增加c端曝光,胡乱操作。
操作房源绑定优惠券的时候,会使用分布式锁,避免并发绑定操作导致超限
2.3.2 数据展示
数据的展示依赖于C端的索引数据,索引数据中存储的都是在生效状态的优惠券信息,字段结合业务场景用于筛选和展示,具体如下:
- 优惠券ID(多值)
- 活动ID(多值)
- 参加的活动类型(多值)
通过索引筛选条件检索出房源数据后,即可获取房源的完整数据来获取该房源绑定了哪些优惠券,进行优惠券数据的展示。
2.3.3 状态流转
对于绑定关系的状态实时更新有两个触发动作,根据情况去更新数据的status和recordStatus两个字段:
依托于2.2.3中的优惠券状态的MQ消息 db绑定数据发生变化时(改/增/减),同样也会发送MQ,异步更新索引中的数据。
2.4 C端用户领券
C端用户领券是一个比较重要的地方,核心要求就是绝对不能多领,尽可能避免少领。具体设计流程图如下:
流程大概分为三步:
- 请求校验
- redis库存扣减
- 领取记录和更新库存任务以事务形式写入MySQL
下面根据图中所示逐步进行说明
2.4.1 数据读取
由于C端浏览券详情&领券可能会是一个并发量较高的操作,所以尽可能都从缓存中读数据,包括以下数据:
- 活动信息
- 优惠券信息
- 优惠券库存
当活动开始前五分钟会禁止编辑活动信息和优惠券信息,活动开始时将上述数据推入缓存中,不设置过期时间,待活动状态转为结束时再清理数据。
同时在web服务集群中,对这1 2数据项做了一层短时间的本地缓存,减少请求redis集群的网络开销。
2.4.2 校验
首先,服务端收到用户领券的请求后,会在redis中校验是否存在领取记录cache。这里可以使用布隆过滤器来实现,key为优惠券id,若用户id存在其中则直接返回。
然后,会进行优惠券状态校验,优惠券数据在活动开始时即推入了redis中,所以直接用redis中的数据校验即可。
最后,会校验是否存在该优惠券的存库数据,为了提高容错性和可用性,若不存在则发送一个初始化库存任务的MQ,然后直接返回,由MQ异步来重新初始化库存数据,避免缓存击穿的问题。
2.4.3 库存扣减
直接操作db,以使用乐观锁形式扣减库存,MySQL的库存更新操作会成并发热点,请求都会在行锁的争抢中阻塞,支持的并发量有限,并且会给db带来压力。由于redis可以保证操作的原子性,并且数据在内存中,适合高并发场景,所以通过redis来完成库存的扣减。
将db的库存更新操作通过db消息任务表,进行异步化,串行化,避免阻塞的同时也降低锁的争抢。
但是扣减库存以redis为准的话,就分为几种情况:
- redis扣减成功,但是db领取记录和更新库存任务写入失败,执行回滚,incr库存数量。
- redis扣减失败(没库存/redis宕机),不会执行db操作。
- redis扣减成功,db事务执行时,服务运行机器重启或宕机,没有回滚库存,产生少领情况。
- redis扣减成功后redis主库宕机,DB写入数据成功,但扣减数据未同步到从库,使用从库进行扣减时产生超领现象。
1 2 属于正常情况,3 4 属于异常情况。
对于情况3, 可以在redis中库存扣减光时,触发异步任务来对比库存数据,若还有可领取库存,则更新redis的库存信息,达到避免少领的情况。
对于情况4,由于redis无法保证主从强一致,在数据操作丢失的情况下,就有可能会产生超领情况。
我的想法是,有以下几种方式:
- 若更新库存操作已经存在超领的情况,将用户领取优惠券的数据进行删除或冻结,避免带来损失。
- 监控剩余库存量每变动5%,就执行异步任务将优惠券状态进行冻结,让C端无法领券。然后将当前消息事务表中db的扣减任务都处理完成后,进行redis和db的数据校验同步,同步完成后将优惠券解冻,恢复正常领取。
- 由于redis属于AP,若要保证数据的强一致性则牺牲可用性,改为使用CP的存储。
- 优惠券核销时检查核销数量是否超过总数量,若达到阈值,则提示优惠券不可用。
- 根据优惠券发放总数量,生成一批优惠券id,同时存入db与redis队列中,通过pop id生成优惠券领取记录,根据id是否相同来限制重复领取,从而达到防止超领的情况。
第一种方式,用户领券成功到更新db库存任务的执行,在这中间时间窗口很小的情况下,可以尽可能的避免超领并使用的情况。
第二种方式,也不能完全解决超量的问题,只能以牺牲很少时间的领取功能,矫正领取请求非激增的情况下数据不一致的情况。
第三种方式不做赘述。
第四种方式,被动的进行数量的校验,保证使用的优惠券不会超过发放的总数量,个人认为是比较柔和的影响范围最小的处理方式了,只不过可能会引起客诉,(为什么领了还是不能用!!!)
第五种方式,db中根据优惠券发放总数量生成一批领取记录,但是领取用户id以及领取时间空着。将这批记录的id存入redis队列中,扣库存的时候pop该队列,获取id以后,通过乐观锁方式更新对应id的数据行(where id = x and userId = 0),若更新失败则领取失败。这样可以有效的防止第四种情况产生,需要考虑的是若发放数量过多,而实际领取很少,还需活动结束后清理占用的数据表空间。事前需要预热插入db中的数据,事后需要清理没有绑定userId的数据。
在我的业务场景下,考量过后主要选用了第四种方式,第二种方式选择了每天凌晨四点进行一次,第一种没有删除而仅仅是进行了报警。
若有更好的方式,希望大神能够指点一下
2.5 优惠券核销
以微服务形式,提供用户优惠券获取接口,以及状态变更接口给调用方使用,状态流转分为未使用、锁定、已使用三种状态。
当客户下了签约订单时,将对应使用的优惠券进行锁定。若订单完成则调用接口将状态流转为已使用,若订单取消则回滚优惠券的状态为未使用。
未来可优化方向
- 用户的优惠券数据分库分表,进行数据水平拆分,提升db读写能力。
- redis集群以分片形式部署,提升可用性及容量。
- 集群拆分,每个集群仅处理部分优惠券请求,通过网关打散请求到不同的pod中。
- 引入jd-hotkey组件,热key实时同步到集群本地缓存中,减少访问分布式缓存。
- 引入canal组件,通过binlog同步db更新的信息,更新缓存并进行数据联动更新。
所以
至此,一个完整的优惠券系统就构建完毕了。
遵循读多写少用缓存,写多读少用队列的原则。
对于展现的活动数据,代码通过代理模式尽可能的通过缓存进行读取。使用了多级缓存的同时,为了避免产生缓存击穿的场景,对于活动中的数据都采用了主动推数据到redis中的方式。
对于活动->优惠券->房源的联动数据的写操作,代码通过状态模式+观察者模式实现,以及MQ控制并发量的异步更新。
对于库存扣减采用了redis,依托于它的原子性,但是redis不保证集群内部数据强一致性。为了避免超领带来的损失问题,在核销优惠券时进行了数量阈值校验。
对于热点的db库存更新则采用了db事务消息表,通过事务保证领取记录插入成功的同时一定会落入更新库存任务,从而异步串行的进行库存更新。
以上就是今天分享的全部内容,希望能够对广大企业营销人员有一些营销方面的启发。作为国内知名的营销自动化平台,MarketUP通过完善的企业营销自动化系统,帮助企业实现有效和有意义的企业营销工作,更好地满足您每个目标受众端到端的需求,为您的渠道带来更多理想的潜在客户并留存孵化。如果您对我们的营销自动化产品或功能感兴趣,欢迎点击【这里】进行演示申请,我们将有专业的团队为您提供服务。
本文由MarketUP营销自动化博客发布,不代表MarketUP立场,转载联系作者并注明出处:https://www.marketup.cn/marketupblog/yxzx/14425.html