项目经验123

DDD+RPC架构

DDD分层架构介绍

DDD(Domain-Driven Design 领域驱动设计),目的是对软件所涉及到的领域进行建模,以应对系统规模过大时引起的软件复杂性的问题。开发团队和领域专家一起通过 通用语言(Ubiquitous Language)去理解和消化领域知识,从领域知识中提取和划分为一个一个的子领域(核心子域,通用子域,支撑子域),并在子领域上建立模型,再重复以上步骤,这样周而复始,构建出一套符合当前领域的模型

依靠领域驱动设计的设计思想,通过事件风暴建立领域模型,合理划分领域逻辑和物理边界,建立领域对象及服务矩阵和服务架构图,定义符合DDD分层架构思想的代码结构模型,保证业务模型与代码模型的一致性

通过上述设计思想、方法和过程,指导团队按照DDD设计思想完成微服务设计和开发

  • 拒绝泥球小单体、拒绝污染功能与服务、拒绝一加功能排期一个月
  • 架构出高可用极易符合互联网高速迭代的应用服务
  • 物料化、组装化、可编排的服务,提高人效

服务架构调用关系

项目经验123_第1张图片

接口层{interfaces}

  • 接口服务位于用户接口层,用于处理用户发送的Restful请求和解析用户输入的配置文件等,并将信息传递给应用层

应用层{application}

  • 应用服务位于应用层。用来表述应用和用户行为,负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果的拼装
  • 应用层的服务包括应用服务和领域事件相关服务
  • 应用服务可对微服务内的领域服务以及微服务外的应用服务进行组合和编排,或者对基础层如文件、缓存等数据直接操作形成应用服务,对外提供粗粒度的服务
  • 领域事件服务包括两类:领域事件的发布和订阅。通过事件总线和消息队列实现异步数据传输,实现微服务之间的解耦

领域层{domain}

  • 领域服务位于领域层,为完成领域中跨实体或值对象的操作转换而封装的服务,领域服务以与实体和值对象相同的方式参与实施过程
  • 领域服务对同一个实体的一个或多个方法进行组合和封装,或对多个不同实体的操作进行组合或编排,对外暴露成领域服务。领域服务封装了核心的业务逻辑。实体自身的行为在实体类内部实现,向上封装成领域服务暴露
  • 为隐藏领域层的业务逻辑实现,所有领域方法和服务等均须通过领域服务对外暴露
  • 为实现微服务内聚合之间的解耦,原则上禁止跨聚合的领域服务调用和跨聚合的数据相互关联

基础层{infrastructure}

  • 基础服务位于基础层。为各层提供资源服务(如数据库、缓存等),实现各层的解耦,降低外部资源变化对业务逻辑的影响

  • 基础服务主要为仓储服务,通过依赖反转的方式为各层提供基础资源服务,领域服务和应用服务调用仓储服务接口,利用仓储实现持久化数据对象或直接访问基础资源

  • DDD结构它是一种充血模型结构,所有的服务实现都以领域为核心应用层定义接口,领域层实现接口且定义数据仓储,基础层实现数据仓储中关于DAO和Redis的操作

  • 同时几方又有互相的依赖那么这样的结构再开发独立领域提供 http 接口时候,并不会有什么问题体现出来

引入RPC的问题

  • 但如果这个时候需要引入 RPC 框架,就会暴露问题了,因为使用 RPC 框架的时候,需要对外提供描述接口信息的 Jar 让外部调用方引入才可以通过反射调用到具体的方法提供者
  • 那么这个时候,RPC 需要暴露出来,而 DDD 的系统结构又比较耦合,怎么进行模块化的分离就成了问题点

DDD+RPC 模块分离系统搭建

项目经验123_第2张图片

引入RPC的解决方案

如果按照模块化拆分,那么会需要做一些处理,包括:

  1. 应用层,不再给领域层定义接口,而是自行处理对领域层接口的包装。否则领域层既引入了应用层的Jar,应用层也引入了领域层的Jar,就会出现循环依赖的问题
  2. 基础层中的数据仓储的定义也需要从领域层剥离,否则也会出现循环依赖的问题
  3. RPC 层定义接口描述,包括:入参Req、出参Res、DTO对象,接口信息,这些内容定义出来的Jar给接口层使用,也给外部调用方使用

项目经验123_第3张图片

那么,这样拆分以后就可以按照模块化的结构进行创建系统结构了,每一层按照各自的职责完成各自的功能,同时又不会破坏DDD中领域充血模型的实现

设计模式

策略模式

ID生成策略领域开发

关于 ID 的生成因为有三种不同 ID 用于在不同的场景下;

  • 订单号:唯一、大量、订单创建时使用、分库分表
  • 活动号:唯一、少量、活动创建时使用、单库单表
  • 策略号:唯一、少量、活动创建时使用、单库单表

通过策略模式的使用,来开发策略ID的服务提供。之所以使用策略模式,是因为外部的调用方会需要根据不同的场景来选择出适合的ID生成策略

  • IdContext,ID生成上下文,也就是从这里提供策略配置服务。
  • IIdGenerator,定义生成ID的策略接口。RandomNumeric、ShortCode、SnowFlake,是三种生成ID的策略

模板模式处理抽奖流程

模版模式的核心点:由抽象类定义抽象方法执行策略,即父类规定了好一系列的执行标准,这些标准的串联成一整套业务流程

在于把抽奖流程标准化,需要考虑的一条思路线包括:

  1. 根据入参策略ID获取抽奖策略配置
  2. 校验和处理抽奖策略的数据初始化到内存
  3. 获取那些被排除掉的抽奖列表,这些奖品可能是已经奖品库存为空,或者因为风控策略不能给这个用户薅羊毛的奖品
  4. 执行抽奖算法
  5. 包装中奖结果

以上这些步骤就是需要在抽奖执行类的方法中需要处理的内容,如果是在一个类的一个方法中,顺序开发这些内容也是可以实现的

但这样的代码实现过程是不易于维护的,也不太方便在各个流程节点扩展其他功能,也会使一个类的代码越来越庞大,因此对于这种可以制定标准流程的功能逻辑,通常使用模板方法模式是非常合适的

工厂搭建发奖领域

简单工厂模式

运用简单工厂设计模式,搭建发奖领域服务。介绍:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行

  • 关于 award 发奖领域中主要的核心实现在于 service 中的两块功能逻辑实现,分别是:goods 商品处理factory 工厂

  • goods:包装适配各类奖品的发放逻辑,(1:文字描述、2:兑换码、3:优惠券、4:实物奖品)

  • factory:工厂模式通过调用方提供发奖类型,返回对应的发奖服务。通过这样由具体的子类决定返回结果,并做相应的业务处理。从而不至于让领域层包装太多的频繁变化的业务属性,因为如果你的核心功能域是在做业务逻辑封装,就会就会变得非常庞大且混乱

  • 把四种奖品的发奖,放到一个统一的配置文件类 Map 中,便于通过 AwardType 获取相应的对象,减少 if...else 的使用。

状态变更(状态模式)

  • 状态模式:类的行为是基于它的状态改变的,这种类型的设计模式属于行为型模式它描述的是一个行为下的多种状态变更

  • 比如一个网站的页面,在你登录与不登录下展示的内容是略有差异的(不登录不能展示个人信息),而这种登录与不登录就是我们通过改变状态,而让整个行为发生了变化

项目经验123_第4张图片

  • 流程节点中包括了各个状态到下一个状态扭转的关联条件。比如 : 审核通过才能到活动中,而不能从编辑中直接到活动中,而这些状态的转变就是我们要完成的场景处理
  • 大部分程序员基本都开发过类似的业务场景,需要对活动或者一些配置需要审核后才能对外发布,而这个审核的过程往往会随着系统的重要程度而设立多级控制,来保证一个活动可以安全上线,避免造成误操作引起资损

activity 活动领域层包括:deploy、partake、stateflow

stateflow 状态流转运用的状态模式,主要包括抽象出状态抽象类AbstractState 和对应的 event 包下的状态处理,最终使用 StateHandlerImpl 来提供对外的接口服务

//1.定义抽象类
*在整个接口中提供了各项状态流转服务的接口,例如;活动提审、审核通过、审核拒绝、撤审撤销等7个方法。
*在这些方法中所有的入参都是一样的,activityId(活动ID)currentStatus(当前状态),只有他们的具体实现是不同的

//2.实现类(提审状态)
*提审状态中的流程,比如:待审核状态不可重复提审、非关闭活动不可开启、待审核活动不可执行活动中变更,而:审核通过、审核拒绝、撤销审核、活动关闭,都可以操作
*通过这样的设计模式结构,优化掉原本需要在各个流程节点中的转换使用 ifelse 的场景,这样操作以后也可以更加方便你进行扩展。当然其实这里还可以使用如工作流的方式进行处理    
    
//3.状态流转配置抽象类
*在状态流转配置中,定义好各个流转操作
    
//4.实现状态处理服务
*在状态流转服务中,通过在 状态组 stateGroup 获取对应的状态处理服务和操作变更状态	    
    

抽奖策略领域模块开发

抽奖算法实现

  • 策略模式是一种行为模式,也是替代大量if else的利器

  • 它所能帮你解决的是场景,一般是具有同类可替代的行为逻辑算法场景,可以使用策略模式进行行为包装供给外部使用

    • 不同类型的交易方式(信用卡、支付宝、微信)

    • 生成唯一ID策略(UUID、DB自增、DB+Redis、雪花算法、Leaf算法)等

两种抽奖算法描述,场景A20%、B30%、C50%

  • 总体概率:如果A奖品抽空后,B和C奖品的概率按照 3:5 均分,相当于B奖品中奖概率由 0.3 升为 0.375

  • 单项概率:如果A奖品抽空后,B和C保持目前中奖概率,用户抽奖扔有20%中为A,因A库存抽空则结果展示为未中奖。为了运营成本,通常这种情况的使用的比较多

算法描述:分别把A、B、C对应的概率值转换成阶梯范围值,A=(0~0.2」、B=(0.2-0.5」、C=(0.5-1.0」,当使用随机数方法生成一个随机数后,与阶梯范围值进行循环比对找到对应的区域,匹配到中奖结果

单项概率(算法)

// 1. 获取策略聚合信息
// 2. 获取抽奖策略,检查该策略是否初始化,解析并初始化中奖概率到散列表(非单项概率,不必存入缓存,因为这部分抽奖算法需要实时处理中奖概率)
程序启动时初始化概率元祖,在初始化完成后使用过程中不允许修改元祖数据,元祖数据作用在于讲百分比内(0.20.30.5)的数据,转换为一整条数组上分区数据,
通过数据拆分为整条后,再根据0-100中各个区间的奖品信息,使用斐波那契散列计算出索引位置,把奖品数据存放到元祖中
     * 斐波那契散列增量,逻辑:黄金分割点:(5 - 1) / 2 = 0.6180339887Math.pow(2, 32) * 0.6180339887 = 0x61c88647
     * 数组初始化长度 128,保证数据填充时不发生碰撞的最小初始化值
     * 1.0.2 转换为 20
     * 2. 20 对应的斐波那契值哈希值:(20 * HASH_INCREMENT + HASH_INCREMENT)= -1549107828 HASH_INCREMENT = 0x61c88647
     * 3. 再通过哈希值计算索引位置:hashCode & (rateTuple.length - 1) = 12
     * 4. 那么tup[14] = 0.2 中奖概率对应的奖品
     * 5. 当后续通过随机数获取到1-100的值后,可以直接定位到对应的奖品信息,通过这样的方式把轮训算奖的时间复杂度从O(n) 降低到 0(1)    
// 3. 获取不在抽奖范围内的列表,包括:奖品库存为空、风控策略、临时调整等(子类实现)
// 4. 执行抽奖算法(子类实现)
   //单项概率
    *获取策略对应的元祖
    *获取随机索引
    *如果中奖ID命中排除奖品列表,则返回NULL,否则返回奖品
    //总体概率
    *首先要从总的中奖列表中排除掉那些被排除掉的奖品,这些奖品会涉及到概率的值重新计算
	*如果排除后剩下的奖品列表小于等于1,则可以直接返回对应信息
    *获取随机索引循环与奖品列表中的值进行比对(index累加当前奖品的概率值)

总体概率(算法)

项目经验123_第5张图片

  • 首先要从总的中奖列表中排除掉那些被排除掉的奖品,这些奖品会涉及到概率的值重新计算
  • 如果排除后剩下的奖品列表小于等于1,则可以直接返回对应信息
  • 接下来就使用随机数工具生产一个100内的随值与奖品列表中的值进行循环比对,算法时间复杂度O(n)

扫描方式注册抽奖策略方案

  • 当前项目是通过手动编码方式注册抽奖策略,这种方法在初期会比较方便,但如果抽奖策略多了,每次新增抽奖策略都需要同步修改抽奖策略注册的处理
  • 修改为 枚举+注解+扫描方式完成抽奖策略的注入

改造方案

修改为 枚举 + 注解 + 扫描方式注入:

1、创建枚举类 lottery-common/src/main/java/cn/itedus/lottery/common/Constants.java

2、创建注解 lottery-domain/src/main/java/cn/itedus/lottery/domain/strategy/annotation/Strategy.java

/**
 * 抽奖策略模型注解
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Strategy {
	 /**
     * 抽奖策略模型枚举
     */
    Constants.StrategyMode strategyMode();
}

3、给抽奖策略的实现类增加注解

@Component("entiretyRateRandomDrawAlgorithm")
@Strategy(strategyMode = Constants.StrategyMode.ENTIRETY)
public class EntiretyRateRandomDrawAlgorithm extends BaseAlgorithm {
}

@Component("singleRateRandomDrawAlgorithm")
@Strategy(strategyMode = Constants.StrategyMode.SINGLE)
public class SingleRateRandomDrawAlgorithm extends BaseAlgorithm {
}

4、修改策略注册方法:

/**
 * 抽奖统一配置信息类
 */
public class DrawConfig {

    @Resource
    private List<IDrawAlgorithm> algorithmList = new ArrayList<>();

    /**
     * 抽奖策略组
     */
    protected static Map<Integer, IDrawAlgorithm> drawAlgorithmGroup = new ConcurrentHashMap<>();

    @PostConstruct
    public void init() {
        algorithmList.forEach(algorithm -> {
            Strategy strategy = AnnotationUtils.findAnnotation(algorithm.getClass(), Strategy.class);
            if (null != strategy) {
                drawAlgorithmGroup.put(strategy.strategyMode().getCode(), algorithm);
            }
        });
    }
}

注意事项

public class DrawConfig implements ApplicationContextAware {
    private ApplicationContext applicationContext;
    protected static Map<Integer, IDrawAlgorithm> drawAlgorithmMap = new ConcurrentHashMap<>();

    @PostConstruct
    public void init() {
        Map<String, Object> strategyModeMap = applicationContext.getBeansWithAnnotation(
            StrategyMode.class);
        strategyModeMap.entrySet().forEach(r->{
            StrategyMode strategyMode = AnnotationUtils.findAnnotation(r.getValue().getClass(),StrategyMode.class);
            if(r.getValue() instanceof IDrawAlgorithm){
                drawAlgorithmMap.put(strategyMode.strategyMode().getId(), (IDrawAlgorithm)r.getValue());
            }
        });
    }
    /**
     * 注入 ApplicationContext
     * @author 徐明龙 XuMingLong 2021-12-09
     * @param applicationContext
     * @return void
     */
    @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

/*
applicationContext 不能从通过其他类获取,必须直接注入到当前类,否则@PostConstruct的方法执行时,其他类不一定已经完成了ApplicationContext的注入

例如使用以下类的getApplicationContext()方法获取applicationContext,获得的是空的对象,因为ApplicationContextAware 的setApplicationContext方法在同一个类里可以保证在@PostConstruct之前调用,但在不同的类里,不能保证
*/

/**
 * Spring 工具类
 */
@Component
public class SpringUtils implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringUtils.applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext(){
        return applicationContext;
    }

}

实现和使用分库分表

描述:开发一个基于 HashMap 核心设计原理,使用哈希散列+扰动函数的方式,把数据散列到多个库表中的组件

前言

1.开发日志

  • 新增数据库路由组件开发工程 db-router-spring-boot-starter 这是一个自研的分库分表组件。主要用到的技术点包括:散列算法、数据源切换、AOP切面、SpringBoot Starter 开发等
  • 完善分库中表信息,user_take_activity、user_take_activity_count、user_strategy_export_001~004,用于测试验证数据库路由组件
  • 基于Mybatis拦截器对数据库路由分表使用方式进行优化,减少用户在使用过程中需要对数据库语句进行硬编码处理

2.需求分析

数据库路由 需要做什么技术点?

为什么要用分库分表,其实就是由于业务体量较大,数据增长较快,所以需要把用户数据拆分到不同的库表中去,减轻数据库压力

分库分表操作主要有垂直拆分和水平拆分:

  • 垂直拆分:指按照业务将表进行分类,分布到不同的数据库上,这样也就将数据的压力分担到不同的库上面。最终一个数据库由很多表的构成,每个表对应着不同的业务,也就是专库专用
  • 水平拆分:如果垂直拆分后遇到单机瓶颈,可以使用水平拆分。相对于垂直拆分的区别是:垂直拆分是把不同的表拆到不同的数据库中,而本章节需要实现的水平拆分,是把同一个表拆到不同的数据库中。如:user_001、user_002

项目经验123_第6张图片

数据库路由设计包括的技术知识点

  • AOP 切面拦截的使用,这是因为需要给使用数据库路由的方法做上标记,便于处理分库分表逻辑
  • 数据源的切换操作,既然有分库那么就会涉及在多个数据源间进行链接切换,以便把数据分配给不同的数据库
  • 数据库表寻址操作,一条数据分配到哪个数据库,哪张表,都需要进行索引计算。在方法调用的过程中最终通过 ThreadLocal 记录
  • 数据散列的操作,让数据均匀的分配到不同的库表中去,不能分库分表后,让数据都集中在某个库的某个表,这样就失去了分库分表的意义

综上,可以看到在数据库和表的数据结构下完成数据存放,我需要用到的技术包括:AOP数据源切换散列算法哈希寻址ThreadLocal以及SpringBoot的Starter开发方式等技术。而像哈希散列寻址数据存放,其实这样的技术与 HashMap 有太多相似之处

3.技术调研

HashMap、ThreadLocal,两个功能则用了哈希索引、散列算法以及在数据膨胀时候的拉链寻址和开放寻址

1.ThreadLocal

项目经验123_第7张图片

@Test
public void test_idx() {
    int hashCode = 0;
    for (int i = 0; i < 16; i++) {
        hashCode = i * 0x61c88647 + 0x61c88647;
        int idx = hashCode & 15;
        System.out.println("斐波那契散列:" + idx + " 普通散列:" + (String.valueOf(i).hashCode() & 15));
    }
} 

斐波那契散列:7 普通散列:0
斐波那契散列:14 普通散列:1
斐波那契散列:5 普通散列:2
斐波那契散列:12 普通散列:3
斐波那契散列:3 普通散列:4
斐波那契散列:10 普通散列:5
斐波那契散列:1 普通散列:6
斐波那契散列:8 普通散列:7
斐波那契散列:15 普通散列:8
斐波那契散列:6 普通散列:9
斐波那契散列:13 普通散列:15
斐波那契散列:4 普通散列:0
斐波那契散列:11 普通散列:1
斐波那契散列:2 普通散列:2
斐波那契散列:9 普通散列:3
斐波那契散列:0 普通散列:4
  • 数据结构:散列表的数组结构
  • 散列算法:斐波那契(Fibonacci)散列法
  • 寻址方式:Fibonacci 散列法可以让数据更加分散,在发生数据碰撞时进行开放寻址,从碰撞节点向后寻找位置进行存放元素。公式:f(k) = ((k * 2654435769) >> X) << Y对于常见的32位整数而言,也就是 f(k) = (k * 2654435769) >> 28 ,黄金分割点:(√5 - 1) / 2 = 0.6180339887 1.618:1 == 1:0.618
  • 学到什么:可以参考寻址方式和散列算法,但这种数据结构与要设计实现作用到数据库上的结构相差较大,不过 ThreadLocal 可以用于存放和传递数据索引信息

2.HashMap

项目经验123_第8张图片

public static int disturbHashIdx(String key, int size) {
    return (size - 1) & (key.hashCode() ^ (key.hashCode() >>> 16));
}
  • 数据结构:哈希桶数组 + 链表 + 红黑树
  • 散列算法:扰动函数、哈希索引,可以让数据更加散列的分布
  • 寻址方式:通过拉链寻址的方式解决数据碰撞,数据存放时会进行索引地址,遇到碰撞产生数据链表,在一定容量超过8个元素进行扩容或者树化。
  • 学到什么:可以把散列算法、寻址方式都运用到数据库路由的设计实现中,还有整个数组+链表的方式其实库+表的方式也有类似之处

技术实现

-----
//1.定义注解
*@Retention(RetentionPolicy.RUNTIME)
*@Target({ElementType.TYPE, ElementType.METHOD})
*String key() default "";
*它的使用方式是通过方法配置注解,就可以被指定的 AOP 切面进行拦截,拦截后进行相应的数据库路由计算和判断,并切换到相应的操作数据源上

//2.解析路由配置
*dbCount分库的数量 tbCount分表的数量 defalut默认数据库 list:db01,db02(分库数据库的信息)   
//2.1 数据源配置提取
*实现EnvironmentAware接口的setEnvironment方法提取配置信息并存放到dataSourceMap中方便后续使用    

//3.数据源切换
*在结合 SpringBoot 开发的 Starter 中,需要提供一个 DataSource 的实例化对象,那么这个对象就放在 DataSourceAutoConfig 来实现,并且这里提供的数据源是支持动态切换数据源    
*从配置信息中读取数据源信息,进行实例化创建
*数据源创建完成后放到 DynamicDataSource 中,它是一个继承了 AbstractRoutingDataSource 的实现类,这个类里可以存放和读取相应的具体调用的数据源信息

//4.切面拦截
*获取分库分表字段(dbKey),若为空且数据路由配置中也空则抛出异常
*提取了库表乘积的数量,把它当成 HashMap 一样的长度进行使用和HashMap一样的扰动函数逻辑,让数据分散的更加散列
*当计算完总长度上的一个索引位置后,还需要把这个位置折算到库表中,看看总体长度的索引因为落到哪个库哪个表
*最后是把这个计算的索引信息存放到 ThreadLocal 中,用于传递在方法调用过程中可以提取到索引信息    
    
//5.Mybatis拦截器处理分表
*基于 Mybatis 拦截器进行处理,通过拦截 SQL 语句动态修改添加分表信息,再设置回 Mybatis 执行 SQL 中
*再完善一些分库分表路由的操作,比如配置默认的分库分表字段以及单字段入参时默认取此字段作为路由字段
*实现 Interceptor 接口的 intercept 方法,获取StatementHandler、通过自定义注解判断是否进行分表操作、获取SQL并替换SQL表名 USER 为 USER_03、最后通过反射修改SQL语句
*此处会用到正则表达式拦截出匹配的sql,(from|into|update)[\\s]{1,}(\\w{1,})

//验证分库
*打包 db-router-spring-boot-starter
*引入 pom 文件
//1.在需要使用数据库路由的DAO方法上加入注解
*@DBRouter(key = "uId") key 是入参对象中的属性,用于提取作为分库分表路由字段使用
*如果一个表只分库不分表,则它的 sql 语句并不会有什么差异
*如果需要分表,那么则需要在表名后面加入 user_take_activity_${tbIdx} 同时入参对象需要继承 DBRouterBase 这样才可以拿到 tbIdx 分表信息 (这部分内容我们在后续开发中会有体现)

//验证分表
*@DBRouterStrategy(splitTable = true) 配置分表信息,配置后会通过数据库路由组件把sql语句添加上分表字段,比如表 user 修改为 user_003
*@DBRouter(key = "uId") 设置路由字段
*@DBRouter 未配置情况下走默认字段,routerKey: uId    

事物问题

  • 问题:如果一个场景需要在同一个事务下,连续操作不同的DAO操作,那么就会涉及到在 DAO 上使用注解 @DBRouter(key = “uId”) 反复切换路由的操作。虽然都是一个数据源,但这样切换后,事务就没法处理了
  • 解决:这里选择了一个较低的成本的解决方案,把数据源的切换放在事务处理前,而事务操作也通过编程式编码进行处理
//1.拆解路由算法策略,单独提供路由方法,无论是切面中还是硬编码,都通过这个方法进行计算路由

//2.配置事物处理对象
*创建路由策略对象,便于切面和硬编码注入使用
*创建事务对象,用于编程式事务引入(PROPAGATION_REQUIRED)    

*dbRouter.doRouter(partake.getuId()); 是编程式处理分库分表,如果在不需要使用事务的场景下,直接使用注解配置到DAO方法上即可
*transactionTemplate.execute 是编程式事务,用的就是路由中间件提供的事务对象,通过这样的方式也可以更加方便的处理细节的回滚,而不需要抛异常处理    
    

在应用层编排抽奖过程

  • 分别在两个分库的表 lottery_01.user_take_activity、lottery_02.user_take_activity 中添加 state【活动单使用状态 0未使用、1已使用】 状态字段,这个状态字段用于写入中奖信息到 user_strategy_export_000~003 表中时候,两个表可以做一个幂等性的事务
  • 同时还需要加入 strategy_id 策略ID字段,用于处理领取了活动单但执行抽奖失败时,可以继续获取到此抽奖单继续执行抽奖,而不需要重新领取活动。其实领取活动就像是一种活动镜像信息,可以在控制幂等反复使用

在 lottery-application 模块下新增 process 包用于流程编排,其实它也是 service 服务包是对领域功能的封装

项目经验123_第9张图片

  • 抽奖整个活动过程的流程编排,主要包括:对活动的领取、对抽奖的操作、对中奖结果的存放,以及如何处理发奖
  • 对于每一个流程节点编排的内容,都是在领域层开发完成的,而应用层只是做最为简单的且很薄的一层
活动抽奖流程编排
//1.领取活动
*查询是否存在未执行抽奖领取活动单【user_take_activity 存在 state = 0,领取了但抽奖过程失败的,可以直接返回领取结果继续抽奖】
*查询活动账单(用户ID、活动ID、活动名称、时间、库存剩余、状态、策略ID、人均参与数、已领取次数) 校验:活动状态,日期,库存,个人库存等 
*活动信息校验处理【活动库存、状态、日期、个人参与次数】
*扣减活动库存【目前为直接对配置库中的 lottery.activity 直接操作表扣减库存,后续优化为Redis扣减】
*插入领取活动信息【个人用户把活动信息写入到用户表】
*用户领取活动时候,新增记录:strategy_id、state 两个字段,这两个字段就是为了处理用户对领取镜像记录的二次处理未执行抽奖的领取单,以及state状态控制事务操作的幂等性() 
    
//2.执行抽奖

//3.结果落库保存奖品单

//4.发送MQ,触发发奖流程

//5.返回结果    

使用MQ解耦抽奖发货流程

Apache Kafka是一个分布式发布 - 订阅消息系统和一个强大的队列,可以处理大量的数据,并使您能够将消息从一个端点传递到另一个端点。 Kafka适合离线和在线消息消费。 Kafka消息保留在磁盘上,并在群集内复制以防止数据丢失。 Kafka构建在ZooKeeper同步服务之上。 它与Apache Storm和Spark非常好地集成,用于实时流式数据分析。

以下是Kafka的几个好处:

  • 可靠性 - Kafka是分布式,分区,复制和容错的。
  • 可扩展性 - Kafka消息传递系统轻松缩放,无需停机。
  • 耐用性 - Kafka使用分布式提交日志,这意味着消息会尽可能快地保留在磁盘上,因此它是持久的。
  • 性能 - Kafka对于发布和订阅消息都具有高吞吐量。 即使存储了许多TB的消息,它也保持稳定的性能。

Kafka非常快,并保证零停机和零数据丢失

spring:
  kafka:
    bootstrap-servers: localhost:9092
    producer:
      # 发生错误后,消息重发的次数。
      retries: 1
      #当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算。
      batch-size: 16384
      # 设置生产者内存缓冲区的大小。
      buffer-memory: 33554432
      # 键的序列化方式
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      # 值的序列化方式
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
      # acks=0 : 生产者在成功写入消息之前不会等待任何来自服务器的响应。
      # acks=1 : 只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应。
      # acks=all :只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应。
      acks: 1
    consumer:
      # 自动提交的时间间隔 在spring boot 2.X 版本中这里采用的是值的类型为Duration 需要符合特定的格式,如1S,1M,2H,5D
      auto-commit-interval: 1S
      # 该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理:
      # latest(默认值)在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的记录)
      # earliest :在偏移量无效的情况下,消费者将从起始位置读取分区的记录
      auto-offset-reset: earliest
      # 是否自动提交偏移量,默认值是true,为了避免出现重复数据和数据丢失,可以把它设置为false,然后手动提交偏移量
      enable-auto-commit: false
      # 键的反序列化方式
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      # 值的反序列化方式
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
    listener:
      # 在侦听器容器中运行的线程数。
      concurrency: 5
      #listner负责ack,每调用一次,就立即commit
      ack-mode: manual_immediate
      missing-topics-fatal: false

项目经验123_第10张图片

  • 从用户发起抽奖到中奖后开始,就是MQ处理发奖的流程
  • MQ 消息的发送是不具备事务性的,会有一定概率触发发送失败。MQ 发送完成后需要知道是否发送成功,进行库表状态更新,如果发送失败则需要使用 worker 来补偿 MQ 发送
  • 最后 MQ 发送完成到消费,也是可能有失败的,比如处理失败、更新库表失败等,但无论是什么失败都需要保证 MQ 进行重试处理
  • 而保证 MQ 消息重试的前提就是服务的幂等性,否则你在重试的过程中就造成了流程异常,比如更新次数多了、数据库插入多了、给用户发奖多了等等,尤其是发生资损是更可怕的
//1.生产消息
*把所有的生产消息都放到 KafkaProducer 中,并对外提供一个可以发送 MQ 消息的方法
*配置的类型转换为 StringDeserializer 所以发送消息的方式是 JSON 字符串,当然这个编解码器是可以重写的,满足你发送其他类型的数据
    
//2.消费消息
*判断消息是否存在
*(处理MQ消息) 转换对象(也可以重写Serializer<T>)、获取发送奖品工程,执行发奖、打印日志
消息消费完成(ack.acknowledge())

//3.抽奖流程解耦
*关于MQ的处理,会调动 kafkaProducer.sendLotteryInvoice 发送一个中奖结果的发货单    
*消息发送完毕后进行回调处理(future.addCallback),更新数据库中 MQ 发送的状态,若成功则更新mq_status=1否则更新mq_state = 2(等待定时任务扫码补偿MQ消息) 【这里还有可能在更新库表状态的时候失败,但没关系这些都会被 worker 补偿处理掉】   
*返回结果    
*现在从用户领取活动、执行抽奖、结果落库,到 发送MQ处理后续发奖的流程就解耦了,因为用户只需要知道自己中奖了,但发奖到货是可以等待的,毕竟发送虚拟商品的等待时间并不会很长,而实物商品走物流就更可以接收了。所以对于这样的流程进行解耦是非常有必要的,否则你的程序逻辑会让用户在界面等待更久的时间    

xxx-job

活动状态扫描

扫描待处理的活动列表,状态为:通过、活动中
*通过 -> 时间符合时 -> 活动中
*活动中 -> 时间到期时 -> 关闭
    

扫描库表补偿发货单MQ消息

项目经验123_第11张图片

//1.路由组件提供了必要方法
*在路由组件中,提供获取分库数、分表数和设置库表路由,也就是手动设置的操作,这样可以把扫描的路由结果确定下来

//2.消息补偿任务
*验证参数
*获取分布式任务配置参数信息(参数配置格式:1,2,3 也可以是指定扫描一个,也可以配置多个库,按照部署的任务集群进行数量配置,均摊分别扫描效率更高)   
*获取分库分表配置下的分表数
*获取当前任务扫描的指定分库
*判断配置指定扫描库数,是否存在
*循环扫描对应表
*扫描库表数据(设置路由、查询数据)
*补偿MQ消息,MQ消息发送完成,更新数据库表 user_strategy_export.mq_state = 1
     MQ 消息发送失败,更新数据库表 user_strategy_export.mq_state = 2 【等待定时任务扫码补偿MQ消息】

分布式锁处理活动秒杀

项目经验123_第12张图片

//1.库存扣减操作
*获取抽奖活动库存 key
*扣减库存,目前占用库存数
*超出库存判断,进行恢复原始库存    
*以活动库存占用编号,生成对应加锁Key,细化锁的颗粒度
*生成分布式锁

//2.活动秒杀流程处理
//在领取活动的模板方法中,优化掉原来直接使用数据库行级锁的流程 -> Redis库存的扣减
//扣减库存后,在各个以下的流程节点中,如果有流程失败则进行缓存库存的恢复操作

//4.发送MQ消息,处理数据一致性
//MQ 的发送,只发生在用户首次领取活动时,如果是已经领取活动但因为抽奖等流程失败,二次进入此流程,则不会发送 MQ 消息    

TODO

规则引擎量化人群参与活动

则引擎开发需要的相关的配置类表: rule_tree、rule_tree_node、rule_tree_node_line

CREATE TABLE `rule_tree` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `tree_name` varchar(64) DEFAULT NULL COMMENT '规则树NAME',
  `tree_desc` varchar(128) DEFAULT NULL COMMENT '规则树描述',
  `tree_root_node_id` bigint(20) DEFAULT NULL COMMENT '规则树根ID',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10002 DEFAULT CHARSET=utf8;

CREATE TABLE `rule_tree_node` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `tree_id` int(2) DEFAULT NULL COMMENT '规则树ID',
  `node_type` int(2) DEFAULT NULL COMMENT '节点类型;1子叶、2果实',
  `node_value` varchar(32) DEFAULT NULL COMMENT '节点值[nodeType=2];果实值',
  `rule_key` varchar(16) DEFAULT NULL COMMENT '规则Key',
  `rule_desc` varchar(32) DEFAULT NULL COMMENT '规则描述',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=123 DEFAULT CHARSET=utf8;

CREATE TABLE `rule_tree_node_line` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `tree_id` bigint(20) DEFAULT NULL COMMENT '规则树ID',
  `node_id_from` bigint(20) DEFAULT NULL COMMENT '节点From',
  `node_id_to` bigint(20) DEFAULT NULL COMMENT '节点To',
  `rule_limit_type` int(2) DEFAULT NULL COMMENT '限定类型;1:=;2:>;3:<;4:>=;5<=;6:enum[枚举范围];7:果实',
  `rule_limit_value` varchar(32) DEFAULT NULL COMMENT '限定值',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

运用组合模式搭建规则引擎领域服务,包括:logic 逻辑过滤器、engine 引擎执行器

项目经验123_第13张图片

  • 基于量化决策引擎,筛选用户身份标签,找到符合参与的活动号。拿到活动号后,就可以参与到具体的抽奖活动中了
  • 通常量化决策引擎也是一种用于差异化人群的规则过滤器,不只是可以过滤出活动,也可以用于活动唯独的过滤,判断是否可以参与到这个抽奖活动中
  • 抽奖系统后,后面会使用规则引擎领域服务,在应用层做一层封装后,由接口层进行调用使用。也就是用户参与活动之前,要做一层规则引擎过滤

数据库设计

product

商品属性表

项目经验123_第14张图片

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7ghV47t7-1651902183185)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220315154451970.png)]

属性分组

项目经验123_第15张图片

项目经验123_第16张图片

属性 & 属性分组关联表

项目经验123_第17张图片

品牌

项目经验123_第18张图片

项目经验123_第19张图片

商品三级分类

项目经验123_第20张图片

项目经验123_第21张图片

品牌分类关联

项目经验123_第22张图片

项目经验123_第23张图片

商品评价回复关系

image-20220315155529369

spu信息

项目经验123_第24张图片

image-20220315160948007

spu属性值

项目经验123_第25张图片

spu信息介绍

image-20220315161033072

spu图片

项目经验123_第26张图片

商品评价

项目经验123_第27张图片

sku信息

项目经验123_第28张图片

sku图片

项目经验123_第29张图片

sku销售属性 & 值

项目经验123_第30张图片

基础

系统架构

前后分离开发,分为内网部署和外网部署,外网是面向公众访问的。
访问前端项目,可以有手机APP,电脑网页;内网部署的是后端集群,
前端在页面上操作发送请求到后端,在这途中会经过Nginx集群,
Nginx把请求转交给API网关(springcloud gateway)(网关可以根据当
前请求动态地路由到指定的服务,看当前请求是想调用商品服务还是购
物车服务还是检索服务),从路由过来如果请求很多,可以负载均衡地调
用商品服务器中一台(商品服务复制了多份),当商品服务器出现问题也
可以在网关层面对服务进行熔断或降级(使用阿里的sentinel组件),网关
还有其他的功能如认证授权、限流(只放行部分到服务器)等。

到达服务器后进行处理(springboot为微服务),服务与服务可能会相互
调用(使用feign组件),有些请求可能经过登录才能进行(基于OAuth2.0的
认证中心。安全和权限使用springSecurity控制)

服务可能保存了一些数据或者需要使用缓存,我们使用redis集群(分片+哨兵集
群)。持久化使用mysql,读写分离和分库分表。

服务和服务之间会使用消息队列(RabbitMQ),来完成异步解耦,分布式事务
的一致性。有些服务可能需要全文检索,检索商品信息,使用ElaticSearch。

服务可能需要存取数据,使用阿里云的对象存储服务OSS。

项目上线后为了快速定位问题,使用ELK对日志进行处理,使用LogStash收
集业务里的各种日志,把日志存储到ES中,用Kibana可视化页面从ES中检
索出相关信息,帮助我们快速定位问题所在。

在分布式系统中,由于我们每个服务都可能部署在很多台机器,服务和服务
可能相互调用,就得知道彼此都在哪里,所以需要将所有服务都注册到注册
中心。服务从注册中心发现其他服务所在位置(使用阿里Nacos作为注册
中心)。

每个服务的配置众多,为了实现改一处配置相同配置就同步更改,就需要配
置中心,也使用阿里的Nacos,服务从配置中心中动态取配置。

服务追踪,追踪服务调用链哪里出现问题,使用springcloud提供的Sleuth、
Zipkin、Metrics,把每个服务的信息交给开源的Prometheus进行聚合分析,
再由Grafana进行可视化展示,提供Prometheus提供的AlterManager实时
得到服务的警告信息,以短信/邮件的方式告知服务开发人员。

还提供了持续集成和持续部署。项目发布起来后,因为微服务众多,每一个都打
包部署到服务器太麻烦,有了持续集成后开发人员可以将修改后的代码提交到
github,运维人员可以通过自动化工具Jenkins Pipeline将github中获取的代码打
包成docker镜像,最终是由k8s集成docker服务,将服务以docker容器的方式运行。

SPU和SKU区别

  • 类目: 类目是一个树状结构的系统。如:手机 -> 智能手机 -> 苹果手机类目,在这里面手机是一级类目,苹果手机是三级类目即叶子类目
  • SPU:iphone 13 (商品聚合信息的最小单位)
  • SKU: iphone 13 256G 黑色 (商品的不可再分的最小单元)
  • 从广义上讲,类目 > SPU > SKU

跨越问题

跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。

同源策略:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域

请求先发送到网关,网关在转发给其他服务 事先都要注册到注册中心

@Configuration
public class GulimallCorsConfiguration {

    @Bean
    public CorsWebFilter corsWebFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        CorsConfiguration corsConfiguration = new CorsConfiguration();

        // 配置跨越
        corsConfiguration.addAllowedHeader("*"); // 允许那些头
        corsConfiguration.addAllowedMethod("*"); // 允许那些请求方式
        corsConfiguration.addAllowedOrigin("*"); //  允许请求来源
        corsConfiguration.setAllowCredentials(true); // 是否允许携带cookie跨越
        // 注册跨越配置
        source.registerCorsConfiguration("/**",corsConfiguration);

        return new CorsWebFilter(source);
    }
}

session

@Configuration
public class GulimallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();

        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");
        return cookieSerializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
        return new GenericJackson2JsonRedisSerializer();
    }
}

//application.yml
spring.session.store-type: redis

商品服务

三级分类

//1.查出所有分类

//2.组装成父子的树形结构
*找到所有的一级分类
*找到当前一级分类的子分类
*排序   

商品上架

es

PUT gulimall_product
{
  "mappings": {
    "properties": {
      "skuId": {
        "type": "long"
      },
      "spuId": {
        "type": "long"
      },
      "skuTitle": {
        "type": "text",
        "analyzer": "ik_smart"
      },
      "skuPrice": {
        "type": "keyword"
      },
      "skuImg": {
        "type": "keyword"
      },
      "saleCount": {
        "type": "long"
      },
      "hosStock": {
        "type": "boolean"
      },
      "hotScore": {
        "type": "long"
      },
      "brandId": {
        "type": "long"
      },
      "catelogId": {
        "type": "long"
      },
      "brandName": {
        "type": "keyword"
      },
      "brandImg": {
        "type": "keyword"
      },
      "catalogName": {
        "type": "keyword"
      },
      "attrs": {
        "type": "nested",
        "properties": {
          "attrId": {
            "type": "long"
          },
          "attrName": {
            "type": "keyword"
          },
          "attrValue": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

canal

MySQL主从复制原理

  • MySQL master ,启动binlog机制,将变更数据写入binlog文件
  • slave(I/O thread),从Master 主库拉取 数据,将它拷贝到Slave的中继日志(relay log)中
  • slave(SQL thread),回放Binlog,更新从库数据

canal工作原理

  • canal模拟mysql的 slave 与 master的交互协议,伪装自己是一个salve向master发送dump协议
  • mysql master收到mysql slave(canal)发送的dump请求,开始推送binlog增量日志给slave(也就是canal)
  • mysql slave(canal伪装的)收到binlog增量日志后,就可以对这部分日志进行解析,获取主库的结构及数据变更
//1.查出当前spuID对应的所有skuID

//2.查出当前spu的所有attr属性ID
*查询需要检索的属性(search_type)
*去重
*封装为List<SkuEsModel.Attrs>

//3.发送远程调用,库存系统查询是否有库存    

//4.封装每个sku的信息

//5.调用search服务进行保存    
*远程调用成功,修改当前spu的状态
*远程调用失败,重复调用? 接口幂等性? 重试机制?    

查询商品

//1.准备检索请求
*模糊匹配(skuTitle)
*过滤(属性,分类,品牌,价格区间,库存)
*排序(saleCount_asc/desc、skuPrice_asc/desc)
*分页(from=param.getPage() - 1 * PAGE_SIZE  size=PAGE_SIZE)
*高亮
*聚合分析
    
//2.执行检索请求


//3.分析响应数据,封装成需求的格式    
*返回所有查询到的商品
*JSON.parseObject()转换对象
*从聚合信息中对(属性信息的处理、品牌信息的处理、分类信息的处理)
*分页信息-页码、总记录数、总页码
*分析每个attrs传过来的查询参数值,取消了面包屑以后,要跳转到那个地方。将请求地址的url里面的当前置空,拿到所有的查询条件,去掉当前   

认证服务

短信认证码

//判断是否为最近60s以内
*是的话60s以内不能再发
    
//生成随机验证码(10分钟有效期)    

oauth2

//授权码模式
*通过浏览器将用户引导到码云三方认证页面上(GET请求)
*码云认证服务器通过回调地址{redirect_uri}将 用户授权码 传递给 回调地址上
*根据获取的code发送POST请求获取accessToken
*根据accessToken发送GET请求获取Gitee用户的消息
*判断用户是登录还是注册    

购物车

ThreadLocal

继承HandlerInterceptor接口,重写preHandle()postHandle()
//preHandle() 目标执行之前
*从session中取出登录的用户信息存放到threadLocal中

获取购物车

//获取threadLocal中的登录信息
//根据登录查询中reids存入的购物车信息

添加到购物车

//userId,skuId,nums
*根据userId获取BoundHashOperations<String, Object, Object>
*根据skuId判断当前购物车是否有该商品
*若购物车无此商品CompletableFuture异步编排远程查询当前要添加商品的信息、销售属性并存入redis
*购物车有此商品,修改数量并存入redis    

订单

Feign丢失请求头

@Configuration
public class FeignConfig {

    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                //1.RequestContextHolder拿到刚进来的这个请求
                ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                if (attributes != null){
                    //老请求
                    HttpServletRequest request = attributes.getRequest();
                    //同步请求头数据 Cookie
                    String cookie = request.getHeader("Cookie");
                    //给新请求同步了老请求的cookie
                    requestTemplate.header("Cookie", cookie);
                }
            }
        };
    }
}

//在方法中
//1.获取之前的请求
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//2.每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);

rabbitMQ

//1.服务收到消息就回调  
*spring.rabbitmq.publisher-confirms=true
//设置确认回调ConfirmCallback
*只要消息抵达Brokertrue, (消息丢失),做好消息确认机制(publisher、consumer【手动ack】),每一个发送的消息都在数据库做好记录。定期将失败的消息再次发送一遍   
    
//2.消息正确抵达队列进行回调 
*spring.rabbitmq.publisher-return=true
*spring.rabbitmq.template.mandatory=true
//设置确认回调ReturnCallback
*只要消息没有投递给指定的队列(失败回调)    
    
//3.消费端确认(保证每条消息都被正常消费,此时broker才可以删除此消息)
*开启手动签收模式 spring.rabbitmq.listener.simple.acknowledge-mode: manual
*消费者手动确认模式。只要没有明确告诉MQ,货物被签收。没有ack,消息就一直是unacked状态, 即使consumer宕机,消息不会丢失,会重新变为Ready,下一次有新的Consumer连接进来,就会发给他   
*签收方式,channel.basicAck(deliveryTag,false)签收;业务成功完成就应该签收
channel.basicNack(deliveryTag,false,true)拒签;业务失败,拒签
*签收过程出现异常,消息重新归队 channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);

订单服务

 //  order.delay.queue 绑定 order.release.order
	@Bean
    public Binding orderCreateOrderBingding() {
        return new Binding("order.delay.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.create.order",
                null);
    }

    @Bean
    public Binding orderReleaseOrderBingding() {
        return new Binding("order.release.order.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.order",
                null);
    }

    /**
     * 订单释放直接和库存释放进行绑定
     */
    @Bean
    public Binding orderReleaseOtherBingding(){
        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.other.#",
                null);
    }


    @Bean
    public Binding orderSecKillOrrderQueueBinding() {
        return new Binding(
                "order.seckill.order.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.seckill.order",
                null);
    }

库存服务

    @Bean
    public Binding stockReleaseBinding(){
        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.release.#",
                null);
    }

    @Bean
    public Binding stockLockedBinding(){
        return new Binding("stock.delay.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.locked",
                null);
    }

下单操作

//1. 在订单展示页
*避免feign远程调用或者异步调用丢失请求头的操作
*远程查询所有的收货地址列表
*远程查询购物车所有选中的购物项
*查询用户积分等操作
*设置防重令牌    

//2.验证防重令牌【令牌的对比和删除必须保证原子性】(lua脚本)
*String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
*原子验证令牌和删除令牌

//3.创建订单,订单项等信息、金额对比
*生成订单号、创建订单信息(运费金额、收获人信息、订单的相关状态信息)
*构建所有订单项数据
*计算价格、积分相关
*金额对比成功
    
//4.库存锁定,只要有异常回滚订单数据 
*@Transactional,按照下单的收货地址,找到一个就近仓库,锁定库存,若没有仓库有这个商品的库存,抛出异常(回滚订单)
*如果每一个商品都锁定成功,将当前商品锁定的工作单记录发给 MQ (延时队列)
*rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",stockLockedTo) 
 
//5.订单创建成功给MQ发送消息
(放入延时队列中)    
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",
                                                    orderCreateTo.getOrder());    

关闭订单

//1. 查询当前这个订单的最新状态
*关闭订单
*发送MQ消息 rabbitTemplate.convertAndSend("order-event-exchange","order.release.other", orderTo);  
*保证消息一定会发送出去,每一个消息都可以做好日志记录(给数据库保存每一个消息的详细信息)
*定期扫描数据库将失败的消息再发送
*将没发送成功的消息进行重试发送   

秒杀服务

商品上架、查询

//扫描最近三天需要参与秒杀的活动
*上架商品
*缓存活动信息
*缓存活动的关联商品信息
*sku的基本信息、设置当前商品的秒杀时间信息、随机码、
使用库存作为分布式的信号量  限流:、 商品可以秒杀的数量作为信号量
    
//返回当前时间可以参与的秒杀商品信息
*确定当前时间属于哪个秒杀场次
*遍历keys,判断当前的key是否为当前场次
*获取这个秒杀场次需要的所有商品编号
*从redis Hash结构中获取商品信息    

秒杀流程

//记录耗时时间

//获取当前用户的信息

//获取当前秒杀商品的详细信息

//校验合法性
*校验时间的合法性
*校验随机码和商品id
*验证购物数量是否合理
*验证这个人是否已经购买过, SETEX,占位成功说明从来没有买过,
RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SKU_STOCK_SEMAPHORE + randomCode)
boolean b = semaphore.tryAcquire(num);    

你可能感兴趣的:(安全框架,面试)