0 课程地址
https://coding.imooc.com/lesson/380.html#mid=28597
1 浓缩精华
1.1
2 个人关注
2.1
3 课程内容
这一章完整的介绍了优惠券系统的业务思想,包含三个功能微服务:模板微服务、分发微服务和结算微服务。之后对存储方面的设计进行了介绍,包含 MySQL 和 Redis 缓存的设计思想。最后,对系统的整体架构进行了介绍。优惠券系统的架构分为两类:SpringCloud 组件架构和功能微服务架构。
优惠券系统业务思想
模板微服务
先由运营人员创建优惠券模板,之后再去生成对应数量的优惠券,最后用户才可以去领取优惠券。这个模块(或者微服务)的核心功能都是围绕优惠券模板的。运营人员设定好条件(名称、logo、分类、产品线、数量、规则等等),后台异步创建优惠券模板。之所以是异步过程,是因为创建优惠券模板的过程是比较耗时的,HTTP接口不返回是一种不好的用户体验。
生成优惠券码需要考虑两个方面:
- 不可以重复
- 有一定的识别性
最终,我把优惠券码设定为18位,由三个部分组成:
- 前四位:产品线和类型
- 中间六位:日期随机
- 后八位:0 ~ 9 之间的随机数
业务思想如下图所示
模板创建的一个关键步骤是异步的生成对应的优惠券码(前面已经介绍了它是怎样构成的),并保存到 Redis 中。需要注意的地方:
- 提高异步线程池的效率,自定义线程池实现
- 静态单实例生成优惠券码
业务思想如下图所示
运营人员创建的优惠券模板不可能是一直有效的(模板一旦过期,它所对应的优惠券则不能再分发给用户。但是,已经分发给用户的,可以是不过期的),所以,需要有一个过期机制能够让过期的优惠券不返回给用户展示。我在这里设计了两种实现策略:
- 优惠券模板模块中实现一个定时任务,例如每个小时运行一次,定时清理过期的优惠券模板
- 其他模块从模板模块获取优惠券模板时,自己去判断是否已经过期。之所以需要这样做,是因为定时任务总会存在一个定时间隔的延迟,并不能保证实时的过期
分发微服务
优惠券分发模块主要涉及四个核心的功能点。
根据用户id和优惠券状态查找用户优惠券记录
-
首先,由于我们的系统暂时没有接入用户系统,所以,关于用户相关的创建、校验等功能是没有的,这些会在代码中进行简单的fake,或者叫做mock数据;这其实也很常见,我们在实际的企业级开发中,也会通过这样的方式去完成应用和对应用可用性的验证工作
-
第二,我这里把属于用户的优惠券状态(注意,这里所说的优惠券是用户相关的,需要与优惠券模板区分开)定义为三类。可用的和已使用的都是字面意思,过期的指的是超出了优惠券的有效使用期,但是仍未被使用的
-
第三,为了提升系统的响应速度,把用户的数据存储于Redis中,也就是与用户相关的优惠券信息都存储于Redis中;可以想象,在将来,展示用户数据的时候,将直接从Redis中读取
-
第四,第二条中说到优惠券存在过期的状态,那么,什么时候确定优惠券过期了呢?这里也会使用延迟处理的策略。也就是当用户查看自己优惠券的时候,判断是否存在过期的但是没有被标记的优惠券。如果存在,除了展示用户优惠券信息外,再做额外的过期处理
业务思想如下图所示
根据用户id查找当前可以领取的优惠券模板
-
第一,优惠券模板是一个独立的服务,所以,分发模块需要通过微服务调用去获取模板数据。但是访问任何一个微服务都存在不确定性,所以,这里要有熔断兜底的策略
-
第二,从模板服务中获取到的优惠券模板,并不一定都是可领取的,需要去比对优惠券模板的相关限制。例如,有一张优惠券模板A,限制用户只能领取一张可用。那么,如果之前用户已经领取过了,且状态仍是可用状态,则这次就不能再次领取了
业务思想如下图所示
用户领取优惠券
-
第一,优惠券模板是一个独立的服务,所以,分发模块需要通过微服务调用去获取模板数据。但是访问任何一个微服务都存在不确定性,所以,这里要有熔断兜底的策略
-
第二,从模板服务中获取到的优惠券模板,并不一定都是可领取的,需要去比对优惠券模板的相关限制。例如,有一张优惠券模板A,限制用户只能领取一张可用。那么,如果之前用户已经领取过了,且状态仍是可用状态,则这次就不能再次领取了
-
第三,由于每一张优惠券模板都要求它们所对应的优惠券要有优惠券码,且在生成的时候,直接放入到Redis中。所以,这里需要尝试从Redis中获取优惠券码
-
第四,通过了验证,即优惠券模板是可以领取的,且成功获取到了优惠券码,就可以将优惠券写入MySQL和Redis了
业务思想如下图所示
结算(核销)优惠券
-
第一,无论是结算还是核销,都需要对前端/客户端传递的参数值进行校验,判断当前用户想要使用的优惠券是否是合法的,合法的标准是属于当前用户且优惠券的状态是可用
-
第二,由于我们的分发微服务直接面向用户,而结算这样的功能实际只与优惠券的相关,更细致的说,是只与优惠券模板定义的规则相关。所以,结算功能不放在分发微服务中,而是由优惠券系统中的第三个功能微服务负责,即结算微服务
-
第三,需要知道,结算和核销是两个不同的概念。结算是计算利用优惠券可以优惠的金额,但并不是使用。这种场景发生在我们付款之前,付款之前,优惠券并未使用,但是,也会显示使用优惠券之后优惠的金额和实际需要结算的金额。而核销则是使用优惠券。所以,对于核销这种情况,需要把数据回写到数据库中
业务思想如下图所示
结算微服务
结算微服务只提供一个功能:根据优惠券类型结算优惠券
-
第一,我们在设计优惠券的时候,会对优惠券设置不同的分类,例如:满减类、折扣类,大家也可以自行扩展更多的分类
-
第二,由于优惠券种类的不同,自然会有不同的结算方式,或者说结算的算法。例如,满减券是根据满多少金额减去多少金额,而折扣券是直接打一定的折扣等等。另外,更复杂的情况是优惠券之间可以组合。例如满减和折扣组合,先去满减,再去打一定的折扣。需要注意,由于优惠券种类比较多,如果枚举出所有的组合,将会有巨大的工作量。所以,我在课程中,给出了一个组合优惠券的结算过程,其他的组合方式,大家可以按照我的实现方式自行修改,这个过程也并不会很复杂
业务思想如下图所示
存储设计
MySQL 表设计
系统中一共有两张 MySQL 表:
-
优惠券模板表:优惠券模板是与用户无关的,是对一类优惠券的描述。运营人员通过设定模板,来描述优惠券的各种信息。
-
用户优惠券表:优惠券模板是用来描述优惠券的,而优惠券表则是记录用户用户优惠券信息。这张表比较简单,除了主键之外,只有5个字段。
Redis 缓存设计
对于缓存,也是有两类,且都是使用Redis来实现。
- 优惠券码缓存
- 使用Redis实现,KV类型的缓存
- Key是需要有意义的,即最好能够根据Key来识别它对应的是什么数据。且需要注意,Redis这类基础工具往往是通用的,不要与其他的Key有冲突
- 由于优惠券码需要一直保持在系统中,等待分发(即等待用户的领取),所以,并不设置过期时间。
总结下来,为了保证优惠券码的Key不冲突,以前缀+主键的形式构成;且使用list类型(当然,使用set也是可以的)来保存优惠券码。
- 用户优惠券信息缓存
- 使用Redis实现,KV类型的缓存
- Key是需要有意义的,即最好能够根据Key来识别它对应的是什么数据。且需要注意,Redis这类基础工具往往是通用的,不要与其他的Key有冲突
- 由于优惠券分为3类,为了更加高效的检索,我这里的实现也会使用到三个缓存去实现。且由于每一类优惠券都可能是很多个,这里我选择使用Redis的hash类型
- 由于用户数据量比较大,且在MySQL中保存有完整的用户信息。所以,不在Redis中长时间保留用户优惠券信息。需要设置一个过期时间
用户优惠券信息缓存的key是前缀+用户id的形式;value是hash类型,hash的key是优惠券id,hash的value是优惠券信息。
架构设计
SpringCloud微服务组件架构
这里主要是两个组件:Eureka和Zuul。客户端的请求入口是Zuul,也就是整个系统的网关服务。网关服务的最核心功能是能够根据请求做分发。把不同的请求分发到对应的微服务上去。Eureka
Server是整个系统的注册中心,是SpringCloud服务治理的基础。不论是网关还是功能微服务,都需要把自己注册到Eureka
Server上。各自在需要系统元信息的时候,再去询问Eureka Server去主动获取。
功能微服务架构设计
结算服务是比较独立的。目前只是我们的优惠券分发服务在做结算时会使用到。但是,对于结算,可以设计的更加通用,不只是优惠券的结算,还可以扩展成商品的结算等等。所以,在实现上,我会把结算服务单独的作为一个微服务。模板服务和结算服务不依赖于其他的服务,而分发服务则会依赖它们两个。实现上,需要考虑调用方式和熔断降级策略。
4 代码演练
4.1