优惠券系统的业务思想,包含三个功能微服务:模板微服务、分发微服务和结算微服务。之后对存储方面的设计进行了介绍,包含 MySQL 和 Redis 缓存的设计思想。最后,对系统的整体架构进行了介绍。优惠券系统的架构分为两类:SpringCloud 组件架构和功能微服务架构。

一、优惠券系统业务思想

1.1 模板微服务

先由运营人员创建优惠券模板,之后再去生成对应数量的优惠券,最后用户才可以去领取优惠券。这个模块(或者微服务)的核心功能都是围绕优惠券模板的。运营人员设定好条件(名称、logo、分类、产品线、数量、规则等等),后台异步创建优惠券模板。之所以是异步过程,是因为创建优惠券模板的过程是比较耗时的,HTTP接口不返回是一种不好的用户体验。
生成优惠券码需要考虑两个方面:

  • 不可以重复
  • 有一定的识别性

最终,我把优惠券码设定为18位,由三个部分组成:

  • 前四位:产品线和类型
  • 中间六位:日期随机
  • 后八位:0 ~ 9 之间的随机数

业务思想如下图所示
优惠券系统业务分析 - 图1

模板创建的一个关键步骤是异步的生成对应的优惠券码(前面已经介绍了它是怎样构成的),并保存到 Redis 中。需要注意的地方:

  • 提高异步线程池的效率,自定义线程池实现
  • 静态单实例生成优惠券码

业务思想如下图所示
优惠券系统业务分析 - 图2
运营人员创建的优惠券模板不可能是一直有效的(模板一旦过期,它所对应的优惠券则不能再分发给用户。但是,已经分发给用户的,可以是不过期的),所以,需要有一个过期机制能够让过期的优惠券不返回给用户展示。我在这里设计了两种实现策略:

  • 优惠券模板模块中实现一个定时任务,例如每个小时运行一次,定时清理过期的优惠券模板
  • 其他模块从模板模块获取优惠券模板时,自己去判断是否已经过期。之所以需要这样做,是因为定时任务总会存在一个定时间隔的延迟,并不能保证实时的过期

    1.2 分发微服务

    优惠券分发模块主要涉及四个核心的功能点。

    1.2.1 根据用户id和优惠券状态查找用户优惠券记录

  • 首先,由于我们的系统暂时没有接入用户系统,所以,关于用户相关的创建、校验等功能是没有的,这些会在代码中进行简单的fake,或者叫做mock数据;这其实也很常见,我们在实际的企业级开发中,也会通过这样的方式去完成应用和对应用可用性的验证工作

  • 第二,我这里把属于用户的优惠券状态(注意,这里所说的优惠券是用户相关的,需要与优惠券模板区分开)定义为三类。可用的和已使用的都是字面意思,过期的指的是超出了优惠券的有效使用期,但是仍未被使用的
  • 第三,为了提升系统的响应速度,把用户的数据存储于Redis中,也就是与用户相关的优惠券信息都存储于Redis中;可以想象,在将来,展示用户数据的时候,将直接从Redis中读取
  • 第四,第二条中说到优惠券存在过期的状态,那么,什么时候确定优惠券过期了呢?这里也会使用延迟处理的策略。也就是当用户查看自己优惠券的时候,判断是否存在过期的但是没有被标记的优惠券。如果存在,除了展示用户优惠券信息外,再做额外的过期处理

业务思想如下图所示
优惠券系统业务分析 - 图3

1.2.2 根据用户id查找当前可以领取的优惠券模板

  • 第一,优惠券模板是一个独立的服务,所以,分发模块需要通过微服务调用去获取模板数据。但是访问任何一个微服务都存在不确定性,所以,这里要有熔断兜底的策略
  • 第二,从模板服务中获取到的优惠券模板,并不一定都是可领取的,需要去比对优惠券模板的相关限制。例如,有一张优惠券模板A,限制用户只能领取一张可用。那么,如果之前用户已经领取过了,且状态仍是可用状态,则这次就不能再次领取了

业务思想如下图所示
优惠券系统业务分析 - 图4

1.2.3 用户领取优惠券

  • 第一,优惠券模板是一个独立的服务,所以,分发模块需要通过微服务调用去获取模板数据。但是访问任何一个微服务都存在不确定性,所以,这里要有熔断兜底的策略
  • 第二,从模板服务中获取到的优惠券模板,并不一定都是可领取的,需要去比对优惠券模板的相关限制。例如,有一张优惠券模板A,限制用户只能领取一张可用。那么,如果之前用户已经领取过了,且状态仍是可用状态,则这次就不能再次领取了
  • 第三,由于每一张优惠券模板都要求它们所对应的优惠券要有优惠券码,且在生成的时候,直接放入到Redis中。所以,这里需要尝试从Redis中获取优惠券码
  • 第四,通过了验证,即优惠券模板是可以领取的,且成功获取到了优惠券码,就可以将优惠券写入MySQL和Redis了

业务思想如下图所示
优惠券系统业务分析 - 图5

1.2.4 结算(核销)优惠券

  • 第一,无论是结算还是核销,都需要对前端/客户端传递的参数值进行校验,判断当前用户想要使用的优惠券是否是合法的,合法的标准是属于当前用户且优惠券的状态是可用
  • 第二,由于我们的分发微服务直接面向用户,而结算这样的功能实际只与优惠券的相关,更细致的说,是只与优惠券模板定义的规则相关。所以,结算功能不放在分发微服务中,而是由优惠券系统中的第三个功能微服务负责,即结算微服务
  • 第三,需要知道,结算和核销是两个不同的概念。结算是计算利用优惠券可以优惠的金额,但并不是使用。这种场景发生在我们付款之前,付款之前,优惠券并未使用,但是,也会显示使用优惠券之后优惠的金额和实际需要结算的金额。而核销则是使用优惠券。所以,对于核销这种情况,需要把数据回写到数据库中


这里可以理解结算微服务是专门用来做计算。】**
业务思想如下图所示
优惠券系统业务分析 - 图6

1.3 结算微服务

结算微服务只提供一个功能:根据优惠券类型结算优惠券:结算仅仅与商品和优惠券模板信息有关

  • 第一,我们在设计优惠券的时候,会对优惠券设置不同的分类,例如:满减类、折扣类,大家也可以自行扩展更多的分类
  • 第二,由于优惠券种类的不同,自然会有不同的结算方式,或者说结算的算法。例如,满减券是根据满多少金额减去多少金额,而折扣券是直接打一定的折扣等等。另外,更复杂的情况是优惠券之间可以组合。例如满减和折扣组合,先去满减,再去打一定的折扣。需要注意,由于优惠券种类比较多,如果枚举出所有的组合,将会有巨大的工作量。所以,我在课程中,给出了一个组合优惠券的结算过程,其他的组合方式,大家可以按照我的实现方式自行修改,这个过程也并不会很复杂

业务思想如下图所示
优惠券系统业务分析 - 图7

1.4 业务系统总结

整个流转过程的描述:
首先由运营人员去创建一个优惠券模版,根据模版信息中的优惠券的数量,异步生成对应数量的优惠券码放到redis缓存当中,因为生成优惠券码需要一定的时间,如果http请求不反回内容,让用户一直等待显然不是一种很好的体验,这些优惠券码不会重复,并且按照一定规则生成,并且优惠券模版是有过期时间的,需要创建定时任务去清理过期的模版,但是由于定时任务清除,中间会有一段时间间隔,可能有些模版已经过期但是并没有被清除掉,需要调用模版微服务的调用方去判断一下,当前的这个微服务模版是否过期。

前面的过程与使用的用户无关,之后用户通过分发微服务,可以去领取优惠券,首先根据我们要领取的优惠券的模版,通过微服务调用模版微服务,确定这个模版是否有效,如果有效,再看这个模版的规则,比如每个用户只能领取一张,比对完规则之后确认可以领取之后,再去redis中获取优惠券码,在满足优惠券模版可以领取优惠券和成功获取优惠券码的前提下,就去在优惠券表中创建一条记录,根据这个用户的id和优惠券的状态可以找到他所拥有的优惠券列表,还可以根据用户id去获取他所能领取的优惠券模版列表。之后我们可以对优惠券进行结算和核销,结算和核销都需要调用结算微服务系统,结算是指计算我们使用优惠券之后的商品价格,核销是指,我们完成订单,之后对优惠券进行核销,也就是更改优惠券变成已使用状态,放到kafka当中,最后回写到mysql.

结算微服务的作用是提供商品的价格和优惠券模版的计算规则,得出最终的价格,他只于优惠券模版和商品的价格有关,与优惠券的分发和用户都无关,所以单独作为一个微服务系统。


二、存储设计

2.1 MySQL 表设计

系统中一共有两张 MySQL 表:

  • 优惠券模板表:优惠券模板是与用户无关的,是对一类优惠券的描述。运营人员通过设定模板,来描述优惠券的各种信息。
    优惠券系统业务分析 - 图8

    image.png

  • 用户优惠券表:优惠券模板是用来描述优惠券的,而优惠券表则是记录用户用户优惠券信息。这张表比较简单,除了主键之外,只有5个字段。
    优惠券系统业务分析 - 图10

    image.png

2.2 Redis 缓存设计

对于缓存,也是有两类,且都是使用Redis来实现。
image.png

  • 优惠券码缓存
    • 使用Redis实现,KV类型的缓存
    • Key是需要有意义的,即最好能够根据Key来识别它对应的是什么数据。且需要注意,Redis这类基础工具往往是通用的,不要与其他的Key有冲突
    • 由于优惠券码需要一直保持在系统中,等待分发(即等待用户的领取),所以,并不设置过期时间。

总结下来,为了保证优惠券码的Key不冲突,以前缀+主键的形式构成;且使用list类型(当然,使用set也是可以的)来保存优惠券码。

  • 用户优惠券信息缓存

image.png

  • 使用Redis实现,KV类型的缓存
  • Key是需要有意义的,即最好能够根据Key来识别它对应的是什么数据。且需要注意,Redis这类基础工具往往是通用的,不要与其他的Key有冲突
  • 由于优惠券分为3类,为了更加高效的检索,我这里的实现也会使用到三个缓存去实现。且由于每一类优惠券都可能是很多个,这里我选择使用Redis的hash类型
  • 由于用户数据量比较大,且在MySQL中保存有完整的用户信息。所以,不在Redis中长时间保留用户优惠券信息。需要设置一个过期时间

用户优惠券信息缓存的key是前缀+用户id的形式;value是hash类型,hash的key是优惠券id,hash的value是优惠券信息。


三、架构设计

3.1 SpringCloud微服务组件架构

这里主要是两个组件:Eureka和Zuul。客户端的请求入口是Zuul,也就是整个系统的网关服务。网关服务的最核心功能是能够根据请求做分发。把不同的请求分发到对应的微服务上去。Eureka Server是整个系统的注册中心,是SpringCloud服务治理的基础。不论是网关还是功能微服务,都需要把自己注册到Eureka Server上。各自在需要系统元信息的时候,再去询问Eureka Server去主动获取。
优惠券系统业务分析 - 图14

3.2 功能微服务架构设计

结算服务是比较独立的。目前只是我们的优惠券分发服务在做结算时会使用到。但是,对于结算,可以设计的更加通用,不只是优惠券的结算,还可以扩展成商品的结算等等。所以,在实现上,我会把结算服务单独的作为一个微服务。模板服务和结算服务不依赖于其他的服务,而分发服务则会依赖它们两个。实现上,需要考虑调用方式和熔断降级策略。
优惠券系统业务分析 - 图15

四、微服务调用组件

image.png

image.png
Ribbon使用轮询的方式

image.png

image.png

image.png

image.png

image.png

五、Eureka自我保护机制

image.png

image.png

六、课程总结

image.png

image.png

image.png

image.png

image.png