Blage's Coding Blage's Coding
Home
算法
  • 手写Spring
  • SSM
  • SpringBoot
  • JavaWeb
  • JAVA基础
  • 容器
  • Netty

    • IO模型
    • Netty初级
    • Netty原理
  • JVM
  • JUC
  • Redis基础
  • 源码分析
  • 实战应用
  • 单机缓存
  • MySQL

    • 基础部分
    • 实战与处理方案
    • 面试
  • ORM框架

    • Mybatis
    • Mybatis_Plus
  • SpringCloudAlibaba
  • MQ消息队列
  • Nginx
  • Elasticsearch
  • Gateway
  • Xxl-job
  • Feign
  • Eureka
  • 面试
  • 工具
  • 项目
  • 关于
🌏本站
🧸GitHub (opens new window)
Home
算法
  • 手写Spring
  • SSM
  • SpringBoot
  • JavaWeb
  • JAVA基础
  • 容器
  • Netty

    • IO模型
    • Netty初级
    • Netty原理
  • JVM
  • JUC
  • Redis基础
  • 源码分析
  • 实战应用
  • 单机缓存
  • MySQL

    • 基础部分
    • 实战与处理方案
    • 面试
  • ORM框架

    • Mybatis
    • Mybatis_Plus
  • SpringCloudAlibaba
  • MQ消息队列
  • Nginx
  • Elasticsearch
  • Gateway
  • Xxl-job
  • Feign
  • Eureka
  • 面试
  • 工具
  • 项目
  • 关于
🌏本站
🧸GitHub (opens new window)
  • 网关

  • 抽奖

    • 抽奖项目
      • 1.DDD架构
        • insfrastructure基础层
        • domain领域层
        • application应用层
        • interfaces接口层
        • RPC通信层
        • common通用包
      • 2.ConcurrentHashMap与SecureRandom
      • 3.策略模式
      • 4.模板模式
      • 5.工厂模式
      • 6.状态模式
      • 7.mybatis标签
      • 8.ID生成策略
      • 9.AOP概念
      • 10.Mybatis拦截器
      • 11.数据库路由组件设计
        • 业务场景
        • mybatis拦截器动态替换表名
        • AOP切面计算路由
        • 更换数据源(分库原理)
        • 动态路由导致事务问题
        • 待解决问题
      • 12.META-INF/spring.factories与springConfig配置依赖
      • 13.@ConfigurationProperties和afterPropertiesSet()
      • 14.static和final
      • 15.Mybatis驼峰与lombok
      • 16.报名活动
      • 17.组合模式&决策树
        • 组合模式
        • 决策树
      • 18.门面模式
      • 19.MapStruct
        • 性能比较
        • 注解使用
        • 实现转换器
      • 20.kafka流量削峰
      • 21.xxl-job实时任务
      • 22.redis滑块锁
      • 23.redis序列化器
      • 24.整合springboot和xxljob
      • 25.Dubbo通信对象序列化
      • 26.多服务包调用下Maven打包技巧
    • 项目梳理
  • 电商

  • 外卖

  • 项目笔记
  • 抽奖
phan
2023-05-15
目录

抽奖项目

# 抽奖项目

lottery.drawio

# 1.DDD架构

# insfrastructure基础层

提供数据底层基础功能服务,包括数据库、缓存、ES底层操作,进行数据持久化。

  • dao:数据库daosql语句映射接口。
  • po:数据库表映射的对象。
  • repository:领域层仓储接口的具体实现。
  • util:工具包,封装redis,es等组件数据底层操作的对象。

# domain领域层

提供不同领域服务实现。具体如何把业务抽象到对应服务,可以根据该业务需要操作的库表、使用的仓储对象来归类到具体业务。每个领域是一个大的方面。

  • model:包含该领域层使用到的所有对象。
    • aggregates:聚合对象,一般包含多个VO对象聚合在一起。
    • req:service服务层调用服务时的请求对象。
    • res:service服务层服务响应返回的封装对象。
    • vo:一般作为repository仓储层服务的形参或者返回类型,以及service中服务请求响应类型;设计时属性仅包含PO对象中关键属性信息,排除掉PO中冗杂字段。
  • repository仓储接口:提供dao上一层的数据仓储服务,通过编排组合dao对象来根据业务需求操作数据库。
  • service服务层:每个领域下细分成不同的服务实现。比如活动领域下包含活动分发、活动流转、活动领取多种服务。以服务接口+实现的方式暴露给外部,除此之外根据设计模式还可能包含抽象类、config对象池、support供应类(一般用于query查询出对象并封装成聚合对象,提供服务实现类使用)。

# application应用层

负责对domain层暴露服务接口的组合和编排。一般以接口+实现类的形式暴露给接口层门面类使用。除此之外,MQ生产者消费者以及xxljob组件对象使用也在应用层。

# interfaces接口层

用于暴露服务给RESTFUL服务请求,facade门面类(实现通信层的服务接口)通过解析用户输入的配置文件,并组合调用application提供的服务。初次之外,服务内部的VO对象转化为服务传输通信的DTO对象的类型转换器也在这声明定义。

# RPC通信层

提供暴露当前服务给其它服务使用。

  • dto:不同服务之间通信时的传输对象
  • req:服务接口的请求对象
  • res:服务接口的响应对象
  • 服务接口:由接口层门面类实现

# common通用包

用来定义状态码、类型码等常量值;统一回复对象等等。

# 2.ConcurrentHashMap与SecureRandom

  • ConcurrentHashMap

多并发场景下用来做缓存,保证线程安全,其中computeIfAbsen方法用于获取key对应value,同时合并首次获取不到执行key插入的操作。

String[] map = concurrentHashMap.computeIfAbsent(strategyId, key -> new String[RATE_TUPLE_LENGTH]);
1
  • SecureRandom

secureRandom.nextInt(100) + 1生成随机数,使用SecureRandom类保证线程安全,且生成的随机数更安全。

# 3.策略模式

  • 业务场景

策略领域下的抽奖算法服务使用了策略模式。首先先在BaseAlgorithm类中实现算法接口的一般算法行为,包括生成随机值,初始化map策略池,其中包含两种map,一种是保存当前策略的映射表,映射表是一个概率散列值—奖品Id的映射表,它是一个String数组,通过下标索引进行映射;另一种map是保存当前抽奖策略的所有奖品信息。

//strategyId——>string[],其中String[i]代表概率散列值为i对应的奖品Id
protected Map<Long, String[]> rateTupleMap = new ConcurrentHashMap<>();
protected Map<Long, List<AwardRateVO>> awardRateInfoMap = new ConcurrentHashMap<>();
1
2
3

不同算法会继承这个算法基础类,并根据采用的不同策略场景来实现randomDraw()执行抽奖方法。

策略模式在调用时引用的是接口对象,通过接口对象调用的方法实际上是接口注入的Bean实现的方法。

  • @Resource

为了防止策略模式下对算法接口Autowired自动装配出现问题,并采用@Resource的byName注入方式,并在不同算法实现类上的注解指明Bean名称。autowired注入的对象一定得存在。source可以不存在,它会自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配

@Component("entiretyRateRandomDrawAlgorithm")
public class EntiretyRateRandomDrawAlgorithm extends BaseAlgorithm{}
1
2

image-20230405165509701

# 4.模板模式

  • 业务场景

策略领域下的抽奖服务采用模板模式设计,整个抽奖流程如下:

①根据入参策略ID获取抽奖策略配置

②校验和处理抽奖策略的数据初始化到内存

③获取那些被排除掉的抽奖列表,这些奖品可能是已经奖品库存为空,或者因为风控策略不能给这个用户薅羊毛的奖品

④执行抽奖算法,可以拿到算法接口执行已定义好的策略,也可以重新定义其它的抽奖算法,

⑤包装中奖结果

其中五个行为的参数控制和调用顺序由抽象模板类进行控制,而执行抽奖和获取库存为0的奖品ID这两种行为因为实现方有不同的方式变化,不适合定义成通用的方法,因此模板类中抽象这两种方法,交给子类去实现。

DrawConfig使用池化/映射思想,提供了一个根据策略ID取出对应抽奖算法Bean的Map池子

Support类主要提供查询操作,获取策略信息和商品信息提供给子类使用。

image-20230405171314313

# 5.工厂模式

  • 业务场景

采用工厂模式发放奖品。首先基于策略模式实现优惠券、实物奖品、文字奖品等奖品类的发货方法,把奖品模拟成货物,每个奖品各自都实现不同的发货方法。接着把四种奖品的发奖统一放在配置类的Map中,最后声明定义工厂,工厂从map中根据奖品类型ID取出获得对应的奖品,减少if-else使用。

image-20230405173430626

# 6.状态模式

  • 业务场景

每个活动都包含不同的状态,包括提审、撤审、运行、关闭等等,当执行完某些操作后会触发活动状态的改变,比如活动开始秒杀状态会从通过转为运行。每一种状态下转移到其它状态的条件都是不同的,因此可以使用状态模式。对象的行为基于他的状态来改变,这个状态必须是一个接口或抽象类。

image-20230405191413561

实现分为两块,首先是状态类,基于它的多态性质采用策略模式实现(抽象类+实现),每种状态基于自身响应的转换规则实现对应行为,最后所有类都会放入到一个map中根据状态码获取。另一部分是流转接口和流转服务(也就是上下文类),流转服务类需要持有一个抽象类状态属性和状态转移函数,通过该抽象类引用来调用不同状态的行为后,再调用函数实现流转。这种内部状态转移是交给context类从map中取状态来实现。

image-20230405191812397

另一种内部流转方式是把状态转移交给状态类来实现,流转服务调用状态行为时,把自己this指针传进去(context对象),然后状态类再通过形参set修改上下文类的状态位。

class Context
{
    private State state;
    public void setState(State state)
    {
        this.state=state;
    }
    public void Handle()
    {
        state.Handle(this);
    }
}
StateA extends State
{
    public void Handle(Context context)
    {
        context.setState(new StateB());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 外部传参流转

状态转换依赖于外部传参(新的状态),context.do()后再调用context.setState实现。这种方式仅适用于context内部拿不到其它状态类。

# 7.mybatis<foreach>标签

mybatis实现可以通过<foreach>标签,实现把dao形参里的list集合插入到表中。好处在于高效,只用从连接池获取一次连接执行一条sql语句。

<insert id="insertList" parameterType="java.util.List">
        INSERT INTO award(award_id, award_type, award_name, award_content, create_time, update_time)
        VALUES
        <foreach collection="list" item="item" index="index" separator=",">
            (
            #{item.awardId},
            #{item.awardType},
            #{item.awardName},
            #{item.awardContent},
            NOW(),
            NOW()
            )
        </foreach>
</insert>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 8.ID生成策略

使用策略模式将三种ID生成算法进行包装,外部的调用方会需要根据不同的场景来选择出适合的ID生成策略。使用阿帕奇工具包来实现,包括雪花算法、日期拼接算法、随机数算法。

  • 订单号:大量高并发,采用雪花算法
  • 活动号activityId:少量,采用日期生成算法
  • 随机数号strategyId:少量,采用随机数算法

雪花算法:安全性和高并发由数据中心和工作节点共十比特来决定,当然可以根据业务需求扩展位数和生成方法,这里采用的是根据本地网卡IP的hashcode生成。

@Component
public class SnowFlake implements IIdGenerator {
    private Snowflake snowflake;
    @PostConstruct
    public void init() {
        long workId;
        try {
            workId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
        } catch (Exception e) {
            workId=NetUtil.getLocalhost().hashCode();
        }
        //保留后五位作为数据节点id
        workId=workId>>16&31;
        //数据中心id
        long dataCenterId=1L;
        snowflake = IdUtil.createSnowflake(workId, dataCenterId);
    }
    @Override
    public Long nextId() {
        return snowflake.nextId();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 9.AOP概念

  • 切面:切点+处理

  • 连接点joinpoint:允许作为切入点的资源,它需要插入横切关注点。、

  • 切点pointcut:用来指定在什么地方进行织入。切入点的指明可以通过切入点表达式,包括@annotation指明注解名称、指明包名,还可以使用|| &&进行拼接。

  • advice:声明各种增强方法,包括环绕增强...使用方法一般是在@around()里面使用&&指定多个切点条件,指明注解名的同时还要指明来自哪个包(防止其它jar包的注解出现同名)。

image-20230405195304802

环绕增强一般两种使用方式:

①@pointcut和@around联合使用。其中@around的value要指明切点前面切点指明的函数。

 @Pointcut("@annotation(com.last.lottery.db.router.annotation.DBRouter)")
 public void addPoint() {}
 
 @Around("addPoint()")
 public Object doRouter(ProceedingJoinPoint jp, DBRouter dbRouter) throws Throwable {}
1
2
3
4
5

②单独在@around中使用切入表达式,并使用&&拼接多个条件。

# 10.Mybatis拦截器

Mybatis可以拦截如下四大对象:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)拦截执行器(查询缓存,数据库操作,事务管理)
  • ParameterHandler (getParameterObject, setParameters)拦截sql语句参数处理(给sql语句动态赋值具体实现,也就是#{}里面的参数)
  • ResultSetHandler (handleResultSets, handleOutputParameters)拦截结果集处理和组装(resulttype将结果映射成响应的结果对象)
  • StatementHandler (prepare, parameterize, batch, update, query)拦截sql语法构建的处理(创建封装statement对象。parameterize调用parameterhandler类方法对sql站位符进行赋值。prepare方法根据Connection连接获取statement对象)

MappedStatement:对mapper.xml某个sql方法的封装,相当于一个sql语句,通过Invocation 对象的 getArgs() 方法获取到,getArgs()[1]表示传入sql语句第二个参数。

Invocation.getTarget()获取拦截方法所在的类class

delegate.mappedStatement:存储映射语句信息。映射语句定义了该执行哪个curd操作,以及sql语句与java对象如何映射。

# 11.数据库路由组件设计

# 业务场景

  • 应对分库分表的不同场景。分库解决的高并发场景,缓解同一个数据库的压力。分表解决的是单表数据量大的问题。如用户活动报名登记表适合分库,因为用户只要不超过限额,可以反复来回报名活动。而订单表则需要分库分表,因为订单表包括活动信息、抽奖策略、奖品信息等等,一条数据量大。实现分库分表的核心在于动态切换数据源dataSource,不同的用户在进行数据库读写时,会根据他的uId使用路由算法计算获得库索引和表索引,这样用户就可以知道他的数据存放在哪个库哪个表。

  • 整个过程使用拦截器和AOP来实现。通过AOP和注解获取该SQL语句的分表策略(分库?分表?索引Key),并计算出新的路由库索引和表索引(反射拿到路由属性值+路由算法)。拦截器的作用则是在执行SQL语句之前,修改插入新的库名和表名,从而路由到对应的位置读取数据。

  • 路由算法实现使用hashmap的扰乱函数来对hashcode进行均匀散列,并把计算得到的库索引和表索引存放在DB上下文类中,DB上下文类使用ThreadLocal来保存库表索引(ThreadLocal具有线程隔离的特点)。

# mybatis拦截器动态替换表名

@Signature注解:指定拦截器的类型、拦截器的方法、方法的入参。这里选择拦截StatementHandler构造sql语句之前。

  • 获取StatementHandler对象,并拿到该对象的元数据
  • 从元数据对象拿到sql对象mappedStatement
  • 通过mappedStatement.getId()获取sql语句所映射的接口名(Dao)
  • 拿到接口.class名称后可以通过class.getAnnotation()获取注解名称。(只有通过反射拿到了.class才能够拿到注解)
  • 根据注解里定义的字段属性值,判断是否采用分表策略,不分表则照常执行不需要修改。
  • 如果需要分表,从拦截类statementHandler获取sql语句,通过正则表达式找到表名的位置,并从DBContextHolder上下文类中取出计算好的表索引替换到sql语句。
  • 通过反射修改BoundSql中的sql字符串语句,因为BoundSql中的属性都为private final。

# AOP切面计算路由

首先明白AOP环绕增强执行时机是在切点方法执行之前,也就是dao上注解方法被调用才会进行扩展。

  • 从注解拿到的字段key判空,若为空从config(spring启动会直接从yml文件中读取路由组件配置)根据默认key计算路由
  • 通过反射,拿到切入点方法的实参——>根据字段名field拿到对应的值
  • 根据key值使用哈希扰乱函数进行散列
  • 执行切入点方法
  • 最后关闭threadlocal防止内存泄露

# 更换数据源(分库原理)

  • 动态数据源类DynamicDataSource需要继承AbstractRoutingDataSource类,并重写determineCurrentLookupKey方法。
  • 所谓数据源本质就是一个保存有数据库信息的Map,多数据源切换的核心在于,通过AbstractRoutingDataSource动态织入程序。配置的多个数据源会放在AbstractRoutingDataSource的 targetDataSources和defaultTargetDataSource中,通过afterPropertiesSet()方法分别复制到resolvedDataSources和resolvedDefaultDataSource中。
  • 每次查询数据库会调用AbstractRoutingDataSource的getConnection()——>调用determineTargetDataSource()方法——>调用determineCurrentLookupKey方法获取数据库的key(db_02),然后从resolvedDataSources中根据key拿到该数据库的数据源配置,最后根据该配置获取connection。
  • 如何实现改写数据源:①重写determineCurrentLookupKey方法,并从threadLocal中取出dbKey返回。②通过重写setEnvironment方法,把yml配置的每个数据源都配置到Map<dbKey,数据源>中③通过set方法将Map中的数据源配置设置到AbstractRoutingDataSource里的TargetDataSources和DefaultTargetDataSource对象。

# 动态路由导致事务问题

不同数据源下会导致@Transactional失效,因为数据操作不属于同一个数据库,实际上属于分布式事务的问题。重写了AbstractRoutingDataSource方法后,在事务下数据源是切换不了的,需要重写事务方法。

因此采用手动设置路由+手动开启事务,保证同一个事务内进行的是同一个数据库的操作。这些部分不交给组件的切面使用去做(不使用分库分表注解),而是通过调用组件暴露的路由接口**,把计算的路由索引存到上下文对象类,当执行sql语句触发拦截器后从上下文类取出新的表名并更新**。

idbRouterStrategy.doRouter(partakeReq.getuId());
return transactionTemplate.execute(status->{
    try {
    } catch (DuplicateKeyException e) {
        status.setRollbackOnly();
        return Result.buildResult(Constants.ResponseCode.INDEX_DUP);
    }
    return Result.buildSuccessResult();
});
1
2
3
4
5
6
7
8
9

# 待解决问题

AbstractDataRouting替代默认数据源,实现动态多数据源切入。

尚未解决多数据源问题,比如hikari,或者Druid,不能使用数据连接池。

# 12.META-INF/spring.factories与springConfig配置依赖

  • spring.factories

Spring加载时会去META-INF文件夹下的spring.factories文件加载扫描@Configuration注解的类,导入时会扫描外部Jar包的Bean,并交给本地项目容器管理,这就是spring加载代码根目录之外的Bean的方法。如果不加那么所导入的jar包所有Bean都会加载到本地项目的spring容器中,在本地项目自动注入失败。

  • Spring自动配置依赖

spring-boot-configuration-processor:将自己的配置、你自己创建的配置类、生成元数据信息,从而在配置文件中(application.yml)可以方便的显示看到配置的属性。

spring-boot-autoconfigure:自动配置,默认配置项

# 13.@ConfigurationProperties和afterPropertiesSet()

  • @ConfigurationProperties

@ConfigurationProperties(prefix = "mybatis"):可以在当前注解类取到yml配置的数据(当前注解类需要提供get,set方法)。

@EnableConfigurationProperties(MyProperties.class):注解会将配置参数类注册到容器中,然后在当前注解类可以直接通过对象使用配置参数。

其中prefix绑定前缀命名规范:仅支持纯小写字母、数字、下划线

作用:可以在yml中填写配置时,提示当前组件可以配置的变量名。

  • afterPropertiesSet

在spring的bean的生命周期中,实例化->生成对象->属性填充后会进行afterPropertiesSet方法。

# 14.static和final

static静态方法需要通过类名调用,static的数据和代码块会在jvm中存放在方法区。而类对象会保存在堆中。

final修改的变量可以通过反射修改变量值。

stream()map()块内不能用外部变量,只能用final。

BeanUtils.getProperty(bean,attr):获取bean对象的attr属性字段值。

# 15.Mybatis驼峰与lombok

  • Mybatis中的配置文件只能指定一份,要么在spring的yml中configuration下添加所有的配置,要么在资源文件下创建mybatis-config.xml文件,所有配置添加里面,并用config-location指明位置。否则会报错
  • uId字段使用lombok@Data注解后,mybatis开启驼峰导致数据库接收不到数据。原因是当使用@Data注解时,自动生成的setter,getter方法为getUId,不符合javaBean规范。 解决方法是遇到第二个字母大写的字段一定要重写getter,setter方法。

# 16.报名活动

  • 业务场景

报名活动使用模板模式实现。抽象类中定义整个报名活动执行流程和顺序:

①查询是否存在报了名但是未抽奖的记录(当前再次报名活动就是在刷单)

②获取活动账单,并将活动报名数量插入缓存当中

③活动信息校验:包括日期校验,库存校验,状态校验。

④扣减活动库存:可以根据不同活动执行不同的扣减策略,无限次数或者是限次数。

⑤添加个人报名记录

其中活动信息校验处理、扣减活动库存、添加个人报名记录抽象出来交给子类实现。

# 17.组合模式&决策树

# 组合模式

  • 业务场景

报名活动时除了用户直接指定特定的活动之外,还可以使用决策树根据用户信息来决策出最合适的活动ID,以减少活动成本。

遇到流程控制(类似于业务办理流程、简历面试流程)、条件筛选、多层级树形菜单加载等等都可以考虑使用决策树。

  • 组合模式

组合模式:对象具有部分-整体的层次结构可以使用组合模式(书籍-目录,子节点-非叶子节点)。实现的关键点在于组合对象(目录,非叶子节点)需要维护一个单个对象的集合。而具体如何使用组合模式统一操作组合对象与单个对象,有几种设计:

①通过类里的type字段来标识节点类型

②如果节点之间还需要控制不同的行为,可以继承抽象父类,然后在实现类里面维护不同类型对象的标识字段。外部调用时只需要操作抽象接口,通过接口实现的get、set方法暴漏不同节点类型的属性。

public class TreeNodeVO {
    private Long treeId;
    private Long treeNodeId;
    private Integer nodeType;
    private String ruleKey;
    private String ruleDesc;
    private String nodeValue;
    private List<TreeNodeLineVO> treeNodeLineVOList;
}
1
2
3
4
5
6
7
8
9

# 决策树

  • VO对象设计

树节点:非叶子节点包含决策字段类型(年龄,性别...)、子节点链路集合。而叶子节点的节点值保存的是活动ID。

链路节点(边节点):头节点、尾节点、决策比较类型(大于,小于,等于),边的决策值

  • 过滤决策

首先明确一点,进行决策的树节点不保存有决策值,所有非叶子节点的决策值都是从一个根据用户信息封装成的Map<Type,value>得到,根据非叶子节点的决策字段来从Map取出决策值。

每一条链路进行决策过滤时,根据当前节点的决策值、边节点比较类型、边界点决策值来进行判断,从而决策出符合条件的一条链路,获取下一步要走的节点ID。最终到达树的叶子节点后决策出活动ID。

这里定义了边节点比较类型的常量,大于、小于等比较规则在数据库中保存为int值。

  • 封装决策器和决策服务

使用工厂模式+策略模式实现不同规则的决策器。首先工厂logicFilterMap会根据不同的节点类型取出决策器,不同的决策器会根据决策器类型从请求对象中封装的用户信息Map取出对应字段的决策值,并进行过滤决策。

image-20230406103439028

决策服务则控制整个流程,包括获取所有决策树的节点信息——>遍历整个树节点进行决策——>包装响应对象.

while (Constants.NodeType.STEM.equals(treeNodeVO.getNodeType())) {
    /** 获取当前决策节点的子树茎 */
    List<TreeNodeLineVO> treeNodeLineVOList = treeNodeVO.getTreeNodeLineVOList();
    String ruleKey = treeNodeVO.getRuleKey();
    /** 根据当前决策字段拿到决策器 */
    LogicFilter logicFilter = logicFilterMap.get(ruleKey);
    String matterValue = logicFilter.matterValue(req);
    Long childId = logicFilter.filter(matterValue, treeNodeLineVOList);
    /** 校验是否决策出结果 */
    if (Constants.Global.TREE_NULL_NODE.equals(childId)) {
        return null;
    }
    treeNodeVO = treeNodeMap.get(childId);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 18.门面模式

  • 业务场景

在RPC调用时可以不暴露应用层接口,便于外系统调用。不暴露子系统之间的模块交互与实现,实现客户端和子系统之间解耦。

执行抽奖封装一个门面,其中包括了子系统活动领取、根据策略算法抽奖、结果生成订单落库的组合调用,客户端不需要关心子系统之间的组合和不同服务请求的封装。

# 19.MapStruct

# 性能比较

执行性能上get/set>MapStruct>BeanUtils

Java程序执行的过程,是由编译器先把java文件编译成class字节码文件,然后由JVM去解释执行class文件。Mapstruct正是在java文件到class这一步帮我们实现了转换方法,即做了预处理,提前编译好文件。

IMapping注入爆红。代码没执行之前自动注入该转换器接口IDEA会出现红线提示,但是运行可以通过,原因就是在于该标签可以动态的完成DTO-DO之间的转换,会在target包中生成对应的实现类。

# 注解使用

  • @Mapping

通过source和target配置不同字段名称之间的赋值。同名属性不需要指定source和target,可以不适用该注解。

当源和目的某一边缺少某个属性时,转换时可以通过指定@Mapping里的constant或者defaultValue给另一边赋值。

源和目的的属性格式不一致(类型一样)时,可以指定dateFormat给目的属性值转换为特定的格式,如@Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")。

源和目的存在某个属性类型不一致时,可以通过expression指定类,或者实现转换器类,然后在Mapper中uses指定使用。

  • Mapper

componentModel:指定当前接口生成的实现类的组件类型。如果指定是spring,如果指定是spring,会自动给当前实现类注解@Component。

unmappedTargetPolicy = ReportingPolicy.IGNORE:忽略多个映射器中未映射的属性,也就是映射的源或者目标类没有某个属性时,忽略该错误。

  • @InheritConfiguration

当两个方法的映射器配置相同时,可以使用该注解,MapStruct会检索其它的已配置方法并用于当前方法的注解配置。保证相同配置器只有一个。

  • @mappingconfig

共享配置,可以通过继承或者是指定mapper config=?使用

# 实现转换器

@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE, unmappedSourcePolicy = ReportingPolicy.IGNORE)
public interface AwardMapping extends IMapping<DrawAwardVO, AwardDTO> {
    @Mapping(target = "userId", source = "uId")
    @Override
    AwardDTO sourceToTarget(DrawAwardVO var1);

    @Override
    DrawAwardVO targetToSource(AwardDTO var1);
}
1
2
3
4
5
6
7
8
9

# 20.kafka流量削峰

  • 中奖后异步消息发奖

在异步发奖消费场景中,中奖落库后(user_strategy_export表插入订单数据,但授奖位还为0),生产者通过MQ推送发奖通知,消费者收到后修改授奖状态位。从而实现解耦削峰,用户只关注抽奖结果,而后续其它过程都可以交给MQ异步处理。

数据库表设计上给export订单表设置了MQ消息发送状态,根据生产者发送消息后的回调状态来修改消息发送状态,对于发送失败消息的采用定时任务补偿(这里如果消息状态修改失败也不会有影响,最终只需要判断成功code码),保证了生产者到broker之间的高可用。

消费者消费失败,可以通过offset偏移量机制(只有消费成功才提交)+指定auto-offset-reset+手动提交偏移量(执行ack.acknowledge())诸多机制来保证消费者与broker之间的高可用。其中某些机制可能导致重复消费(earliest),因此需要保证消费幂等性。

  • 报名活动成功后异步扣减活动库存

原本活动库存扣减使用数据库行级锁(乐观锁)处理扣减,但是存在并发问题,如果库存为1时,两个用户同时都查出库存都大于0,那么它们都可以执行更新操作,导致库存为负数。

UPDATE activity SET stock_surplus_count = stock_surplus_count - 1
WHERE activity_id = #{activityId} AND stock_surplus_count > 0
1
2

通过redis活动库存报名完活动后,使用MQ发送消息异步更新数据库活动库存,做数据最终一致性处理。

# 21.xxl-job实时任务

在使用上xxl-job更多适用于对实时性有要求的定时任务,或者是在跨服务或者是跨业务相互调用的场景下保证整个系统的高可用(feign、dubbo等因为网络波动调用失败,回调通知后使用xxljob进行补偿).

  • 应用场景1

扫描抽奖活动状态。在秒杀活动开始之前,根据当前时间和活动开始时间的时空关系,调用状态流转服务,审核通过的活动状态扫描后修改为为活动中。通过把已过期活动中的状态扫描为关闭。

  • 应用场景2

MQ消息补偿业务。对前面MQ发送发奖消息失败的情况,首先扫描出消息发送失败的订单,包括消息发送失败(state=2)和迟迟没有发送消息的情况(state=0),然后再通过定时任务进行补偿重新发送消息。

其中由于需要通过扫描所有数据库来找出发送失败的消息,因此需要循环的方式把每个库下的多张表中的每条用户记录都进行扫描。所以需要在分库分表组件中,提供出可以设置路由到的库和表。

两种做法,一种是直接在xxl-job任务配置时在任务参数指定需要扫描的数据库编号。另一种则是通过IDBRouterStrategy的Bean对象实现,从类里面的dbConfig中拿到spring配置分库分表的数量。最后循环设置DBContextHolder的库表索引即可。

根据MQ发送消息的回调状态,修改MQ状态(不修改则会重复补偿已经成功MQ消息)。

# 22.redis滑块锁

场景:活动库存扣减

通过redis实现了滑块锁,在高并发场景下,用户只有拿到分布式滑块锁,才能够完成redis的活动库存扣减。分布式锁key设置为活动编号+库存(排队锁,当前活动库存数量没减少,那么其它用户就获取不到),可以使粒度降低,如果单独只是活动编号作为key可能出现有库存而不能秒杀的场景。分布式锁加在活动库存减一操作的两头,减小锁的力度,提高系统性能。

除了滑块锁之外,redis还存放了秒杀活动的报名库存(热点数据)。整个扣减流程如下:

  • 查询活动详情后,把活动库存数量通过setifabsent(不用set的原因是防止从数据库读的脏数据更新到缓存中)加入缓存中,同时可以给库存数量也就是热点数据添加过期时间比如2h,防止过多的热点数据占用redis内存空间。
  • 扣减流程中,首先以活动id+活动库存数量为Key获取锁
  • 如果获取成功,那么先判断库存是否有剩余,然后扣减redis中活动库存数量
  • 释放滑块锁
  • 扣减完数据后发送MQ消息活动库存减一。注意这里不能用扣减活动返回的缓存中的库存数据通过赋值更新,只能够UPDATE activity SET stock_surplus_count = stock_surplus_count-1,因为这里MQ执行的顺序在分布式场景不能保证,可能会把先扣减的库存后消费更新。

img

此外,如果系统并发体量较大,还需要把 MQ 的数据不要直接对库更新,仅仅更新缓存中的库存数量,最后可以使用定时任务拿缓存的库存数量来更新数据库库存,然后删除缓存中的库存数据。以此减少对数据库表的操作。

实际上redis并不适合实时性数据,只能保证最终一致性。因为写数据库后删除缓存能保证一致性但是不能满足高并发,而如果不删除缓存又会导致读到脏数据。

# 23.redis序列化器

JdkSerializationRedisSerializer:RedisTemplate默认序列化方式,前提是被序列化对象必须实现Serializable接口,序列化后保存的是字节序列。(序列化后结果庞大,占据redis内存)

StringRedisSerializer:是StringRedisTemplate默认的序列化方式,只能对字符串进行序列化,无法对普通对象进行序列化,因此需要JSON.toJSONString进行转化。

Jackson2JsonRedisSerializer:速度快不需要实现serializable接口,将对象序列化成json串进行存储。在序列化时需要提供序列化对象的.class类型信息。

注意:使用jackson或者genericJackson存在的坑

redisTemplate.opsForValue().set(key, 1L);
Long value = redisTemplate.opsForValue().get(key); 
// 此时获得的值的类型为Integer类型, 直接进行强转会进行报错
// 其实这也不能说是序列化存在的问题,而是json的数字类型与java的数据类型不能兼容
1
2
3
4

这里给出一种序列化器配置:

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        
        /** 设置redis键和值的序列化器*/
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(jackson2JsonRedisSerializer);
        /** 设置hash的序列化器*/
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 24.整合springboot和xxljob

创造一个xxl-job组件,客户端启动spring后自动将任务注册到调度中心admin中。

查看调度中心controller源码后,发现新建执行器和任务都是向数据库中添加XxlJobGroup和XxlJobInfo这两个对象,也就是spring启动时往数据库插入执行器和任务两种记录即可。可以在@PostConstruct实现。

难点在于项目启动时获取@XxlJob注解里的内容。首先拿到applicationContext并拿到spring中所有的bean,然后再通过.class.getAnnotation拿到注解,但是最后还是要拿到方法上的注解。查了下发现xxljob是通过MethodIntrospector.selectMethods实现。这里留个坑后续把@XxlJob也一起合并

注意:获取自定义注解里面的值,那么一定要能够找到注解类,通过XX.getClass().getAnnotation(MyAnnotation.class)才能拿到注解接口,进而拿到注解里面的值。

# 25.Dubbo通信对象序列化

dubbo进行微服务通信时,双方之间进行信息传输的对象都需要实现序列化接口(包括类下的子属性对象,非java基本类型;继承关系只需要序列化子类),调用方发送请求的req对象,以及服务提供方的应答res对象。

# 26.多服务包调用下Maven打包技巧

  • 打包顺序遵循父pom——>被调用服务的jar包——>子服务jar包
  • 打包操作一律clean——>package——>install
  • install操作表示将当前jar包加载安装到本地Maven仓库当中
  • 调用其它服务的依赖时,需要指定version版本号或者交给父pom通过dependencyManagement管理。因为具体的jar包都是保存在仓库中groupId+artifactId+version所拼接的文件目录下。
  • 可以指定仓库让maven在本地仓库找包。
  • 永久关闭防火墙systemctl disable firewalld /firewald.service
编辑 (opens new window)
上次更新: 2023/12/15, 15:49:57
部署配置
项目梳理

← 部署配置 项目梳理→

Theme by Vdoing | Copyright © 2023-2024 blageCoder
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式