优惠券相信大家在各大电商网站购物的时候都用过,有的时候领了一张券会想法设法的挑选商品来达到优惠券的使用金额,哪怕买了预期之外的商品也要把这张券用掉,褥羊毛的心态大部分人都有。
从产品的角度思考类似优惠券这种营销活动正是抓住了用户褥羊毛的心态,促进用户消费来增加商品的销量,从而赚取更多的利益。现在很多类电商产品在你注册之后会时不时的给你发一张券,当你有需求时还是很乐意去 app 上面买东西的。
最近一段时间也是接触了营销优惠券相关的业务,我所处的情况比较复杂 ,公司正经历 C 端业务由 PHP 切到 Java 的过程,优惠券相关业务以前是 PHP 做的,需要全部切成 Java,除了需要保证功能可用还要完成数据转化,当用户由老版本升级到新版本,整个过程是无感知的。接下来我主要从优惠券需求分析、优惠券业务现状、需求迁移设计、数据迁移与需求优化几个方面谈谈最近的感受与收获。
一、优惠券需求分析
1.1 优惠券模型
一个完整的优惠券信息可以理解为一套规则的组合,主要包括以下几点:基本信息、优惠券类型、优惠券业务类型、有效期、使用范围等。可以结合自己的业务场景调整。
- 基本信息:优惠券的基本信息主要有优惠券的名字,发放数量,每人限制领取数量,规则描述等
- 优惠券类型:满减券、折扣券等
- 优惠券业务类型:这个业务类型可以根据产品的定位考虑是否需要,比如我们是医疗相关的产品,会售卖一些服务与商城商品,因此我们就有商城与服务两种业务类型的券
- 有效期:有效期一般有两种,固定时间段与设置固定有效天数
- 适用范围:全部商品可用、指定商品分类可用、指定商品可用等
1.2 优惠券活动
上面梳理的优惠券模型只是一个模板,并不具备活动信息,因此一个优惠券模板并不具备发放资格,一般我们会搞一个专题活动页,点进去是一张或者多张优惠券,活动一般有开始与结束时间、活动状态、图片、链接等信息。活动创建完成后对优惠券与活动进行关联,优惠券发放更灵活,无论从产品还是开发角度都更容易理解。
1.3 发券方式
一个用户获取优惠券的方式有很多种:
- 专题活动页面手动领取
- 注册或者进入 APP 主页系统以弹窗形式自动发放
- 精准营销,以任务的形式给某些用户群体发放,通过短信或者 push 推送告知用户
- 分享领券,比如我们常用的外卖软件,分享后自己与他人皆可领券
- 后台手动给指定用户发放(后门)
1.4 下单流程
下面主要从开发的角度来分析,用户选购商品在下单时需要获取所有可用优惠券(达到满减金额、指定商品等条件),默认选中对用户最优惠的,如果业务复杂这里还需要考虑多优惠券互斥情况。
用户领取的优惠券一般有未使用,已使用,已过期几个状态,当下单使用了一张优惠券时,这张优惠券的状态应该怎么设置,因为用户下单并不意味着会支付,订单可能会被用户取消,也可能是因为超时未支付取消,也可能被核销。
这个可以根据产品需求处理,比如用户下单的优惠券,在取消订单时不退回,那可以直接修改成已使用状态。如果取消订单需要退还用户优惠券,那就不能改成已使用状态,可以考虑用一个中间态来表示下单未支付的情况-冻结,这个冻结只是中间随着订单状态转化而变化的状态,灵活程度更高。
二、优惠券业务现状
迁移前发券的方式主要有三种
- 新用户注册发放一个新人礼包,这个礼包里有多个券,由系统统一发放
- 商城领券中心,有多张券,需要用户手动领取
- 后台给指定用户发券
老的设计一个优惠券并不是一个模板,而是具备了活动属性,比如状态、开始与结束领取时间等,商城里的领券中心也不是一个可以专门配置的专题活动,而是获取指定商城里正在进行中的优惠券列表,导致优惠券需求直接被做死了,我觉得现在有的运营可能都不清楚这个券是怎么配置的。
优惠券礼包是一个活动,活动本身具备了发放数量、状态、活动开始与结束时间等数据,而优惠券本身也具有这些数据,在校验上要以哪种数据为准,是个很难抉择的问题,设计的有点糟糕,而且礼包只支持添加优惠券,比如以后会在礼包里添加红包等其他优惠,会很难适配。
要保证新老版本数据迁移对用户无影响,在重新设计前,我们通过抓包,定位 PHP 接口的方式来梳理需求,因为很多需求都是运营直接找开发做的,导致没有需求文档,这也是为什么优惠券会设计的如此糟糕的原因,找产品梳理需求,他们也是一脸懵逼,后面只能通过看 PHP 代码整理逻辑。
线下产品推广非常给力,每天新用户增量大概在 1w 左右,一个新人礼包里配了 7 张券,这样计算起来,优惠券领取记录每天会有 7w 左右的增量。查 DB count 的时候有 220w+ 的数据,而且按照这个增量,突破 500w 也只是时间问题,后来一个多月的时间就从 220w 涨到了 550w,迁移数据的时候需要把分库分表方案也规划进去。
后台运营中心有优惠券统计需求,比如发放数量、使用数量这些简单的统计,因为没有数据部门,这些统计都是以 SQL 的方式做的,因为数据量比较大,统计的维度又比较多,很多统计都不会命中数据库索引,就导致有的统计接口非常慢,最慢的一个甚至需要 4s 左右。
三、需求迁移设计
3.1 迁移目标
优惠券及礼包活动由 PHP 逻辑到 Java 的重写切换,数据从 PHP 表到 Java 表的转化迁移,新老版本用户无感知,数据完全统一。
3.2 迁移前准备
刚开始接触的时候,自己的想法很简单,想简单熟悉下业务逻辑后直接撸代码,完全没有往设计方面上想,后来被石南开导了一番后,意识到了架构设计与业务整理的重要性。充分考虑每个问题,按照计划推进每个阶段该做的事。
接下来的两周一直在看 PHP 代码,整理业务逻辑,梳理表结构,对这个过程进行归档,这个过程很漫长,也很枯燥,但是却是很必要的。两周时间下来几乎所有的逻辑都梳理清楚了,接下来也是最难的一部分,根据原有的逻辑,重新设计表结构。
自己刚做营销业务不久,对营销活动的概念理解还不是很透彻,前期总认为优惠券、礼包与活动概念不搭边,因此设计出来的表结构也很直白,也不能适配我们底层的表结构与代码架构,对于底层优惠券模型的设计也有点糟糕,后来参考了其他知名电商网站的优惠券设计后,和石南重新设计了表结构。
我们在以前业务的基础上把优惠券模型单独抽成优惠券模型表(模型表在预留出查询字段后,可以按照业务字段分组以 JSON 的形式存储,这样扩展性会非常高),为了适配以前优惠券的活动属性,在优惠券的基础上又套用了一层活动信息,这只是我们为了适配原有业务设计的,不建议大家也这样做,因为时间紧张,后期优化我们会考虑把活动属性剥离出来。
和产品确认基本需求后,优惠券统计需求直接干掉了,因为这些需求都是以前运营找开发加的。具体的表设计这里就不进行细节性的介绍了,参考上面的两张图,相信也能给大家一些思路。
3.3 开发细节
我们营销底层有一套规则校验引擎,比如像用户领取优惠券,下单获取可用优惠券,这中间一系列的规则条件校验都可以走规则引擎。关于规则引擎我简单的介绍一下,大家可以根据实际情况设计。
- 首先要有规则,规则字段(规则集合)由前端拼接完成后存储到 DB 里
- 从 DB 里取出规则字符串转成对象集合
- 传入规则对象集合,封装用户规则条件,走规则校验器统一校验
大致的流程就是上面几个步骤,这个架构设计被石南扔到了 GitHub 上,没有思路的话大家可以参考营销平台架构。核心类图如下
很多营销活动一般都会有库存概念,不同类型的奖励库存扣减方式可能是不同的,对于库存扣减也可以进行抽象,根据活动类型进行路由分派走不同的处理流程,这块设计比较简单就不进行细节介绍了。
四、数据迁移
业务逻辑梳理清晰后,编写代码并不是一件难事。后期比较头大的是数据迁移,因为之前 PHP 与我们的表结构不一样,甚至差别很大,优惠券领取记录表已经由之前的 220w 增长到了 550w+,下面来总结下整个过程我们是如何做的。
4.1 迁移方案
因为表结构差别大,有些业务逻辑我们进行了优化,并不能完美兼容之前的数据,或者说要兼容以前的数据需要复杂的逻辑转化,因为数据量比较大,转化过程过于复杂会影响迁移的效率,一些优先级较低平常不怎么用到的需求我们和产品及运营确认后直接干掉了。
迁移开发方案采取双数据源的方式,通过读取 PHP 表数据,进行字段与逻辑转化后批量插入 Java 数据表。
4.2 迁移代码开发
完成数据迁移主要考虑两个方面:全量数据与增量数据。
- 全量数据迁移:全量数据迁移并不是一下读取数据库里所有数据,试想一下如果一次读取 550w+ 的数据,可能会造成什么样的后果。全量数据同步我们进行了分段,比如 550w+ 的数据,我们可以分成 6 次进行,每次同步 100w,下一次同步时跳过上次同步的 100w,这样对数据库与服务器的压力会小很多
- 增量数据迁移:增量数据也是考虑两个方面:增量修改与增量添加,怎么判断是修改了数据还是新增了数据,可以通过主键 ID 与上次同步时间戳来判断。根据时间戳去数据库拉取增量,然后根据传入的主键 ID(全量同步最大的主键 ID) 判断是增量更新还是增量添加,然后走不同的逻辑,这个过程也可以参考数据增量考虑是否需要分段同步
有的文章分析 mybatis 的批量操作并不是批量操作的数据越多效率就越高,如果数据过多会导致 mybatis SQL 字符串很大,当批量插入的时候效率反而会下降。我们选择每次从数据库读取 50 条数据,效率还可以,希望能给大家一个参考。
4.3 数据同步效率预估
除了优惠券领取数量数据比较多,其他相关的表数据大概也有 100w 左右,因为之前没有搞过数据同步,对同步效率没有概念,不知道一分钟能跑多少数据。
在迁移代码开发好后,导入了 100w 的测试数据,在本地测试了几次,如果过程没有复杂的查询,1 - 2s 左右就能跑完 1w 的数据,这还是在测试服务器上跑出来的结果,正式环境会比这还要快。后来我们接了分库分表,正式环境 6 - 7s 能跑完 1w 的数据,550w+ 的数据,一个小时不到全量就跑完了。
在没测试之前还以为很慢,项目环境跑了一遍看结果吓了一跳,采用双数据源的方式同步在数据量不是特别大的情况下还是可行的。
4.4 分库分表
我们接的是 sharding-jdbc,只需要做一些简单的配置,就可以实现分库分表,用起来很方便,侵入型也比较小。因为之前没有用过这个框架,在同步数据的时候发现有的版本不支持批量插入,这也是当时没有想到的问题,后来升级版本就可以了。
虽然用了 sharding-jdbc 但是内部原理并不清楚,后续把分库分表也规划到学习计划中去。
五、需求优化
5.1 线上慢查询优化
项目上线后发现有的接口的 RT 很大,远远超出了能够接受的范围。主要通过下面三个步骤来解决问题
- 添加缓存
- 删除不必要的业务代码
- 检查索引
一般接口 RT 比较大的接口都比较复杂,要查询的数据也比较多,对于这些数据考虑是否可以添加缓存,减少查询 DB 的次数,另外一个方面就是重新梳理业务,检查是否有更简单的方案来实现,一个表添加一个索引的开销是比较大的,因此最后一步才是考虑添加索引。
5.2 后续需求优化
优惠券现在有一个严重的问题:承载了活动属性,因为时间紧迫,我们在迁移过程中还保留了原先的设置,这样就导致有的优惠券需求被做死了,想要扩展非常难,关于这点已经和产品沟通过,后面优化的时候希望去掉活动属性,走统一的优惠券模版,优惠券活动的形式进行配置。
六、总结
- 遇到的问题总会有解决办法,切勿急躁
- 需求分析,架构设计非常重要,写代码前花半天时间写下文档,画画流程图,先把思路理清楚再写代码
- 项目规划,按计划推进每个过程中要做的事
- 项目总结,做完一个项目经历了什么,留下了什么
感谢项目组的信任,在一开始接触这个业务的时候还有些担心不能完成,最后还是一步步挺过来了,最终顺利上线!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。