分布式重试服务平台 Easy-Retry

文章目录

    • @[toc]
  • 1.简介
    • 1.1[爱组搭官网](http://aizuda.com/)
    • 1.2介绍
    • 1.3 相关地址
  • 2.架构
    • 2.1系统架构图
    • 2.2 客户端与服务端数据交互图
  • 3.业内成熟重试组件对比
  • 4.快速开始
    • 4.1 服务端项目部署
      • 4.1.0 初始化脚本
      • 4.1.1 源码部署
      • 4.1.2 Docker部署
    • 4.2 客户端集成配置
      • 4.2.1 添加依赖
      • 4.2.2 配置
      • 4.2.3 基于@Retryable注解实现重试
      • 4.2.4 Retryable 详解
      • 4.2.5自定义生成重试任务
  • 5.源码赏析
    • 5.1 客户端自动装配入口
    • 5.2 *Netty* 客户端
    • 5.3 客户端注册扫描Retryable和ExecutorMethodRegister
    • 5.4 客户端重试触发入口
    • 5.5 客户端重试类型
    • 5.6 客户端重试执行器GuavaRetryExecutor
    • 5.7 客户端上报方式
    • 5.8 netty server
    • 5.8 Handler
    • 5.9 服务端向客户端发起重试
    • 5.10 服务端手动下发重试策略
    • 5.11 客户端接收服务端下发重试的端点RetryEndPoint
    • 5.12 服务端的schedule任务
  • 6.集群架构
  • 7.总结

1.简介

  在介绍这款开源产品前先给大家介绍一个开源组织:aizuda–爱组搭

图片

1.1爱组搭官网

http://aizuda.com/

图片

  可以看到Easy-Retry就是爱组搭的开源项目之一。

1.2介绍

  在分布式系统大行其道的当前,系统数据的准确性和正确性是重大的挑战,基于CAP理论,采用柔性事务,保障系统可用性以及数据的最终一致性成为技术共识 为了保障分布式服务的可用性,服务容错性,服务数据一致性 以及服务间调用的网络问题。依据"墨菲定律",增加核心流程重试, 数据核对校验成为提高系统鲁棒性常用的技术方案。

特性

  • 易用性 业务接入成本小。避免依赖研发人员的技术水平,保障重试的稳定性

  • 灵活性 能够动态调整配置,启动/停止任务,以及终止运行中的重试数据

  • 操作简单 分钟上手,支持WEB页面对重试数据CRUD操作。

  • 数据大盘 实时管控系统重试数据

  • 多样化退避策略 Cron、固定间隔、等级触发、随机时间触发

  • 容器化部署 服务端支持docker容器部署

  • 高性能调度平台 支持服务端节点动态扩容和缩容

  • 多样化重试类型 支持ONLY_LOCAL、ONLY_REMOTE、LOCAL_REMOTE多种重试类型

  • 重试数据管理 可以做到重试数据不丢失、重试数据一键回放

  • 支持多样化的告警方式 邮箱、企业微信、钉钉、飞书

1.3 相关地址

easy-retry官方文档地址

https://www.easyretry.com/

项目地址

https://toscode.mulanos.cn/aizuda/easy-retry

gitHub地址

https://github.com/aizuda/easy-retry

字节跳动: 如何优雅地重试

https://juejin.cn/post/6914091859463634951

java优雅重试机制spring-retry

https://mp.weixin.qq.com/s/vqmON5EOT17YDVLo-1JLNQ

2.架构

2.1系统架构图

图片

2.2 客户端与服务端数据交互图

图片

3.业内成熟重试组件对比

区别SpringRetryGuavaRetryEasyRetry
编程语言JavaJavaJava
退避策略支持多种策略支持多种策略支持多种策略
依赖生态Spring 框架不依赖任何框架Spring框架、GuavaRetry
重试类型内存重试内存重试多种策略 内存重试+服务端重试
存储介质内存内存内存+数据库
是否管控重试流量支持多维度管控(单机重试管控、链路重试管控、重试流速管控等)
数据安全会丢失重试数据会丢失重试数据基于LOCAL_REMOTE或ONLY_REMOTE持久化数据
管理重试数据不支持不支持支持暂停、停止、新增、修改重试数据

4.快速开始

4.1 服务端项目部署

4.1.0 初始化脚本

doc/sql/easy_retry.sql

图片

该sql脚本在项目中的位置如图所示。

准备easy_retry数据,执行上面的初始化脚本:

图片

4.1.1 源码部署

  • 下载源码

     https://gitee.com/aizuda/easy-retry.git
     或
     https://github.com/aizuda/easy-retry.git
    
  • maven 打包镜像

maven clean install
  • 修改配置
/easy-retry-server/src/main/resources/application.yml

配置文件修改:

spring:
  datasource:
    name: easy_retry
    url:  jdbc:mysql://localhost:3306/x_retry?useSSL=false&characterEncoding=utf8&useUnicode=true
    username: root
    password: root
    ....其他配置信息....
easy-retry:
  lastDays: 30 # 拉取重试数据的天数
  retryPullPageSize: 100 # 拉取重试数据的每批次的大小
  nettyPort: 1788  # 服务端netty端口
  totalPartition: 32  # 重试和死信表的分区总数
  • 启动
java -jar easy-retry-server.jar

4.1.2 Docker部署

  • 下载镜像
docker pull byteblogs/easy-retry:1.5.0
  • 创建容器并运行
/**
* 如需自定义 mysql 等配置,可通过 "-e PARAMS" 指定,参数格式 PARAMS="--key1=value1  --key2=value2" ;
* 配置项参考文件:/easy-retry-server/src/main/resources/application.yml
* 如需自定义 JVM内存参数 等配置,可通过 "-e JAVA_OPTS" 指定,参数格式 JAVA_OPTS="-Xmx512m" ;
*/
docker run \
  -e PARAMS="--spring.datasource.username=root --spring.datasource.password=root  --spring.datasource.url=jdbc:mysql://IP:3306/easy_retry?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai " \
  -p 8080:8080 \
  -p 1788:1788 \
  --name easy-retry-server-1  \
  -d byteblogs/easy-retry:1.5.0

如果你已经正确启动系统了,那么你可以输入以下地址就可以进入管理系统了

http://localhost:8080

后台体验地址

地址: http://preview.easyretry.com/ 
账号: admin 密码: admin

图片

4.2 客户端集成配置

4.2.1 添加依赖

项目中引入依赖

<dependency>
    <groupId>com.aizuda</groupId>
    <artifactId>easy-retry-client-starter</artifactId>
    <version>1.5.0</version>
</dependency>

4.2.2 配置

启动类上添加注解开启easy-retry功能

@SpringBootApplication
@EnableEasyRetry(group = "example_group")
public class ExampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ExampleApplication.class, args);
    }
}

配置服务地址:

easy-retry:
  server: 
    host: 127.0.0.1 #服务端的地址建议使用域名
    port: 1788 #服务端netty的端口号

4.2.3 基于@Retryable注解实现重试

为需要重试的方法添加重试注解

@Retryable(scene = "errorMethodForLocalAndRemote", localTimes = 3, retryStrategy = RetryType.LOCAL_REMOTE)
public String errorMethodForLocalAndRemote(String name) {
    double i = 1 / 0;
    return "这是一个简单的异常方法";
}

4.2.4 Retryable 详解

属性类型必须指定默认值描述
sceneString场景
includeThrowable包含的异常
excludeThrowable排除的异常
retryStrategyRetryTypeLOCAL_REMOTE重试策略
retryMethodRetryMethodRetryAnnotationMethod重试处理入口
idempotentIdIdempotentIdGenerateSimpleIdempotentIdGenerate幂等id生成器,默认的idempotentId生成器{@link SimpleIdempotentIdGenerate} 对所有参数进行MD5
retryCompleteCallbackRetryCompleteCallbackSimpleRetryCompleteCallback服务端重试完成(重试成功、重试到达最大次数)回调客户端
isThrowExceptionbooleantrue本地重试完成后是否抛出异常
bizNoStringbizNo spel表达式(opens new window)
localTimesint3本地重试次数 次数必须大于等于1
localIntervalint2本地重试间隔时间(s)
timeoutlong60 * 1000同步(async:false)上报数据需要配置超时时间
unitTimeUnitTimeUnit.MILLISECONDS超时时间单位
forceReportbooleanfalse是否强制上报数据到服务端

4.2.5自定义生成重试任务

注意:生成重试任务是将任务在客户端创建并上报到服务端,由服务端调度并通知客户端进行重试

ExecutorMethodRegister 详解

属性类型必须指定默认值描述
sceneString场景
includeThrowable包含的异常
excludeThrowable排除的异常
retryStrategyRetryTypeLOCAL_REMOTE重试策略
retryMethodRetryMethodRetryAnnotationMethod重试处理入口
idempotentIdIdempotentIdGenerateSimpleIdempotentIdGenerate幂等id生成器,默认的idempotentId生成器{@link SimpleIdempotentIdGenerate} 对所有参数进行MD5
retryCompleteCallbackRetryCompleteCallbackSimpleRetryCompleteCallback服务端重试完成(重试成功、重试到达最大次数)回调客户端
bizNoStringbizNo spel表达式
asyncbooleantrue异步上报数据到服务端
timeoutlong60 * 1000同步(async:false)上报数据需要配置超时时间
unitTimeUnitTimeUnit.MILLISECONDS超时时间单位
forceReportbooleanfalse是否强制上报数据到服务端

新建一个自定义任务执行器

// 这个一个自定义任务执行器
@ExecutorMethodRegister(scene = CustomSyncCreateTask.SCENE, async = false, timeout = 10000, unit = TimeUnit.MILLISECONDS, forceReport = true)
@Slf4j
public class CustomSyncCreateTask implements ExecutorMethod {

    public static final String SCENE = "customSyncCreateTask";

    @Override
    public Object doExecute(Object obj) {
        return "测试成功";
    }

}

在代码中执行重试

public void generateAsyncTaskTest() throws InterruptedException {

        Cat cat = new Cat();
        cat.setName("zsd");
        Zoo zoo = new Zoo();
        zoo.setNow(LocalDateTime.now());
        EasyRetryTemplate retryTemplate = RetryTaskTemplateBuilder.newBuilder()
            .withExecutorMethod(CustomAsyncCreateTask.class)
            .withParam(zoo)
            .withScene(CustomAsyncCreateTask.SCENE)
            .build();

        retryTemplate.executeRetry();

        Thread.sleep(90000);
    }

  ExecutorMethodRegister 这个也是一个注解,这个我猜测是跟手动重试相关。

5.源码赏析

5.1 客户端自动装配入口

package com.aizuda.easy.retry.client.starter;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.aizuda.easy.retry.client.core")
@ConditionalOnProperty(prefix = "easy-retry", name = "enabled", havingValue = "true")
public class EasyRetryClientAutoConfiguration {

}

  该自动装配类会将com.aizuda.easy.retry.client.core核心包下交给springBoot自动注入和管理。

5.2 Netty 客户端

图片

5.3 客户端注册扫描Retryable和ExecutorMethodRegister

图片

这两个注解的解析最终会被放到RetryerInfoCache这个类的的一个table中:

public class RetryerInfoCache {

    private static Table<String, String, RetryerInfo> RETRY_HANDLER_REPOSITORY = HashBasedTable.create();

    public static RetryerInfo put(RetryerInfo retryerInfo) {
       return RETRY_HANDLER_REPOSITORY.put(retryerInfo.getScene(), retryerInfo.getExecutorClassName(), retryerInfo);
    }

    public static RetryerInfo get(String sceneName, String executorClassName) {
        return RETRY_HANDLER_REPOSITORY.get(sceneName, executorClassName);
    }

}

  可以看出注册扫描信息始终是在内存中,没有上报给服务端的。

5.4 客户端重试触发入口

图片

  从上图可以看出重试是在加了Retryable注解的方法上采用Aspect的AOP动态代理,当目标方法被调用前会被拦截,AOP的思想就是对目标对象的代理和增强

@Aspect 注解用于标识或者描述AOP中的切面类型,基于切面类型构建的对象用于为目标对象进行功能扩展或控制目标对象的执行。

@Pointcut 注解用于描述切面中的方法,并定义切面中的切入点,后面会对切入点表达式进行详解

@Around注解 用于描述切面中方法,这样的方法会被认为是一个环绕通知,后面会对aop各个通知类型详解

ProceedingJoinPoint 类为一个连接点类型,此类型的对象用于封装要执行的目标方法相关的一些信息。一般用于@Around注解描述的方法参数。

通知类型
spring中定义了五种类型的通知,基于AspectJ框架标准,它们分别是:

环绕通知 (@Around) : 包围一个连接点的通知,最强大的一种通知类型,环绕通知可以在方法前后完成自定义的行为,它可以自己选择是否继续执行连接点或直接返回方法的返回值或抛异常结束执行

前置通知 (@Before) : 在指定连接点(join point)前执行的通知,但它不能阻止连接点前的执行(除非抛异常)

后置通知 (@After): 在指定连接点(join point)退出的时候执行(不管是正常返回还是异常退出)

返回通知 (@AfterReturning) : 在指定连接点(join point)正常返回后执行,如果抛出异常则不执行(和After通知同时存在则在After通知执行完之后再执行)

异常通知 (@AfterThrowing) : 在目标方法抛出异常退出时执行

通知执行顺序

假如这些通知全部写到一个切面对象中,其执行顺序及过程,如图:

图片

进入目标方法前先进入环绕通知(@Aroud)
在环绕通知里调用连接点(joinPoint)的proceed方法后进入前置通知(@Before)
前置通知执行完后进入目标方法(targetMethod)
目标方法逻辑执行完进入环绕通知里调用proceed方法后的逻辑
环绕通知全部执行完后进入后置通知(@After)
后置通知执行完后若目标方法正常返回后则进入返回通知(@AfterReturning),若目标方法抛出异常则进入异常通知(@AfterThrowing)
注:若是存在环绕通知(@Aroud)一定要调用连接点的proceed()方法,否则会在环绕通知后直接返回,跳过目标方法。

around环绕通知源码如下:

package com.aizuda.easy.retry.client.core.intercepter;

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.aizuda.easy.retry.client.core.cache.GroupVersionCache;
import com.aizuda.easy.retry.client.core.config.EasyRetryProperties;
import com.aizuda.easy.retry.client.core.exception.EasyRetryClientException;
import com.aizuda.easy.retry.client.core.intercepter.RetrySiteSnapshot.EnumStage;
import com.aizuda.easy.retry.client.core.strategy.RetryStrategy;
import com.aizuda.easy.retry.client.core.annotation.Retryable;
import com.aizuda.easy.retry.client.core.retryer.RetryerResultContext;
import com.aizuda.easy.retry.common.core.alarm.Alarm;
import com.aizuda.easy.retry.common.core.alarm.AlarmContext;
import com.aizuda.easy.retry.common.core.alarm.AltinAlarmFactory;
import com.aizuda.easy.retry.common.core.constant.SystemConstants;
import com.aizuda.easy.retry.common.core.enums.NotifySceneEnum;
import com.aizuda.easy.retry.common.core.enums.RetryResultStatusEnum;
import com.aizuda.easy.retry.common.core.log.LogUtils;
import com.aizuda.easy.retry.common.core.model.EasyRetryHeaders;
import com.aizuda.easy.retry.common.core.util.EnvironmentUtils;
import com.aizuda.easy.retry.common.core.util.JsonUtil;
import com.aizuda.easy.retry.server.model.dto.ConfigDTO;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.Ordered;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.UUID;

/**
 * @author: www.byteblogs.com
 * @date : 2022-03-03 11:41
 */
@Aspect
@Component
@Slf4j
public class RetryAspect implements Ordered {

    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static String retryErrorMoreThresholdTextMessageFormatter =
            "<font face=\"微软雅黑\" color=#ff0000 size=4>{}环境 重试组件异常</font>  \r\n" +
                    "> 名称:{}  \r\n" +
                    "> 时间:{}  \r\n" +
                    "> 异常:{}  \n"
            ;

    @Autowired
    @Qualifier("localRetryStrategies")
    private RetryStrategy retryStrategy;
    @Autowired
    private AltinAlarmFactory altinAlarmFactory;
    @Autowired
    private StandardEnvironment standardEnvironment;

    @Around("@annotation(com.aizuda.easy.retry.client.core.annotation.Retryable)")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        String traceId = UUID.randomUUID().toString();

        LogUtils.debug(log,"Start entering the around method traceId:[{}]", traceId);
        Retryable retryable = getAnnotationParameter(point);
        String executorClassName = point.getTarget().getClass().getName();
        String methodEntrance = getMethodEntrance(retryable, executorClassName);
        if (StrUtil.isBlank(RetrySiteSnapshot.getMethodEntrance())) {
            RetrySiteSnapshot.setMethodEntrance(methodEntrance);
        }

        Throwable throwable = null;
        Object result = null;
        RetryerResultContext retryerResultContext;
        try {
            result = point.proceed();
        } catch (Throwable t) {
            throwable = t;
        } finally {

            LogUtils.debug(log,"Start retrying. traceId:[{}] scene:[{}] executorClassName:[{}]", traceId, retryable.scene(), executorClassName);
            // 入口则开始处理重试
            retryerResultContext = doHandlerRetry(point, traceId, retryable, executorClassName, methodEntrance, throwable);
        }

        LogUtils.debug(log,"Method return value is [{}]. traceId:[{}]", result, traceId, throwable);

        // 若是重试完成了, 则判断是否返回重试完成后的数据
        if (Objects.nonNull(retryerResultContext)) {
            // 重试成功直接返回结果 若注解配置了isThrowException=false 则不抛出异常
            if (retryerResultContext.getRetryResultStatusEnum().getStatus().equals(RetryResultStatusEnum.SUCCESS.getStatus())
            || !retryable.isThrowException()) {
                return retryerResultContext.getResult();
            }
        }

        if (throwable != null) {
            throw throwable;
        } else {
            return result;
        }

    }

    private RetryerResultContext doHandlerRetry(ProceedingJoinPoint point, String traceId, Retryable retryable, String executorClassName, String methodEntrance, Throwable throwable) {

        if (!RetrySiteSnapshot.isMethodEntrance(methodEntrance)
                || RetrySiteSnapshot.isRunning()
                || Objects.isNull(throwable)
                // 重试流量不开启重试
                || RetrySiteSnapshot.isRetryFlow()
                // 下游响应不重试码,不开启重试
                || RetrySiteSnapshot.isRetryForStatusCode()
        ) {
            if (!RetrySiteSnapshot.isMethodEntrance(methodEntrance)) {
                LogUtils.debug(log, "Non-method entry does not enable local retries. traceId:[{}] [{}]", traceId, RetrySiteSnapshot.getMethodEntrance());
            } else if (RetrySiteSnapshot.isRunning()) {
                LogUtils.debug(log, "Existing running retry tasks do not enable local retries. traceId:[{}] [{}]", traceId, EnumStage.valueOfStage(RetrySiteSnapshot.getStage()));
            } else if (Objects.isNull(throwable)) {
                LogUtils.debug(log, "No exception, no local retries. traceId:[{}]", traceId);
            } else if (RetrySiteSnapshot.isRetryFlow()) {
                LogUtils.debug(log, "Retry traffic does not enable local retries. traceId:[{}] [{}]", traceId,  RetrySiteSnapshot.getRetryHeader());
            } else if (RetrySiteSnapshot.isRetryForStatusCode()) {
                LogUtils.debug(log, "Existing exception retry codes do not enable local retries. traceId:[{}]", traceId);
            } else {
                LogUtils.debug(log, "Unknown situations do not enable local retry scenarios. traceId:[{}]", traceId);
            }
            return null;
        }

        return openRetry(point, traceId, retryable, executorClassName, throwable);
    }

    private RetryerResultContext openRetry(ProceedingJoinPoint point, String traceId, Retryable retryable, String executorClassName, Throwable throwable) {

        try {

            // 标识重试流量
            initHeaders(retryable);

            RetryerResultContext context = retryStrategy.openRetry(retryable.scene(), executorClassName, point.getArgs());
            LogUtils.info(log,"local retry result. traceId:[{}] message:[{}]", traceId, context);
            if (RetryResultStatusEnum.SUCCESS.getStatus().equals(context.getRetryResultStatusEnum().getStatus())) {
                LogUtils.debug(log, "local retry successful. traceId:[{}] result:[{}]", traceId, context.getResult());
            }

            return context;
        } catch (Exception e) {
            LogUtils.error(log,"retry component handling exception,traceId:[{}]", traceId,  e);

            // 预警
            sendMessage(e);

        } finally {
            RetrySiteSnapshot.removeAll();
        }

        return null;
    }

    private void initHeaders(final Retryable retryable) {

        EasyRetryHeaders easyRetryHeaders = new EasyRetryHeaders();
        easyRetryHeaders.setEasyRetry(Boolean.TRUE);
        easyRetryHeaders.setEasyRetryId(IdUtil.getSnowflakeNextIdStr());
        easyRetryHeaders.setDdl(GroupVersionCache.getDdl(retryable.scene()));
        RetrySiteSnapshot.setRetryHeader(easyRetryHeaders);
    }

    private void sendMessage(Exception e) {

        try {
            ConfigDTO.Notify notifyAttribute = GroupVersionCache.getNotifyAttribute(NotifySceneEnum.CLIENT_COMPONENT_ERROR.getNotifyScene());
            if (Objects.nonNull(notifyAttribute)) {
                AlarmContext context = AlarmContext.build()
                        .text(retryErrorMoreThresholdTextMessageFormatter,
                                EnvironmentUtils.getActiveProfile(),
                                EasyRetryProperties.getGroup(),
                                LocalDateTime.now().format(formatter),
                                e.getMessage())
                        .title("retry component handling exception:[{}]", EasyRetryProperties.getGroup())
                        .notifyAttribute(notifyAttribute.getNotifyAttribute());

                Alarm<AlarmContext> alarmType = altinAlarmFactory.getAlarmType(notifyAttribute.getNotifyType());
                alarmType.asyncSendMessage(context);
            }
        } catch (Exception e1) {
            LogUtils.error(log, "Client failed to send component exception alert.", e1);
        }

    }

    public String getMethodEntrance(Retryable retryable, String executorClassName) {

        if (Objects.isNull(retryable)) {
            return StrUtil.EMPTY;
        }

        return retryable.scene().concat("_").concat(executorClassName);
    }

    private Retryable getAnnotationParameter(ProceedingJoinPoint point) {
        String methodName = point.getSignature().getName();
        Class<?> classTarget = point.getTarget().getClass();
        Class<?>[] par = ((MethodSignature) point.getSignature()).getParameterTypes();
        Method objMethod = null;
        try {
            objMethod = classTarget.getMethod(methodName, par);
        } catch (NoSuchMethodException e) {
            throw new EasyRetryClientException("注解配置异常:[{}}", methodName);
        }
        return objMethod.getAnnotation(Retryable.class);
    }

    @Override
    public int getOrder() {
        String order = standardEnvironment
            .getProperty("easy-retry.aop.order", String.valueOf(Ordered.HIGHEST_PRECEDENCE));
        return Integer.parseInt(order);
    }
}

5.5 客户端重试类型

图片

5.6 客户端重试执行器GuavaRetryExecutor

图片

  最终重试会调用到GuavaRetryExecutor的call方法,Easy-Retry本质上就是对GuavaRetry的深度封装,做了一些可视化和告警的能力。

5.7 客户端上报方式

图片

客户端上报方式分为:

异步上报数据:该方式借鉴了sentinel的滑动窗口的RetryTaskDTO做了监听然后进行call重试

同步上报数据:同client将重试任务上报给服务端

        NettyResult result = client.reportRetryInfo(Collections.singletonList(retryTaskDTO));

图片

5.8 netty server

图片

5.8 Handler

Handler可以大体分为两类:处理Get请求的Handler和处理Post请求的Handler,客户端心跳、版本和上报任务都属于Get请求或者是Post请求。

图片

5.9 服务端向客户端发起重试

  服务端的亮点就是使用了akka,Akka是一个开发库和运行环境,可以用于构建高并发、分布式、可容错、事件驱动的基于JVM的应用,使构建高并发的分布式应用更加容易服务端在启动前会做一个scan,把客户端上报给服务端的重试数据全部扫描出来:

AbstractScanGroup类中的doScan:

protected void doScan(final ScanTaskDTO scanTaskDTO) {

        LocalDateTime defLastAt = LocalDateTime.now().minusDays(systemProperties.getLastDays());

        String groupName = scanTaskDTO.getGroupName();
        LocalDateTime lastAt = Optional.ofNullable(getLastAt(groupName)).orElse(defLastAt);

        // 扫描当前Group 待重试的数据
        List<RetryTask> list = retryTaskAccessProcessor.listAvailableTasks(groupName, lastAt, systemProperties.getRetryPullPageSize(),
            getTaskType());

        if (!CollectionUtils.isEmpty(list)) {

            // 更新拉取的最大的创建时间
            putLastAt(scanTaskDTO.getGroupName(), list.get(list.size() - 1).getCreateDt());

            for (RetryTask retryTask : list) {

                // 重试次数累加
                retryCountIncrement(retryTask);

                RetryContext retryContext = builderRetryContext(groupName, retryTask);
                RetryExecutor executor = builderResultRetryExecutor(retryContext);

                if (!executor.filter()) {
                    continue;
                }

                productExecUnitActor(executor);
            }
        } else {
            // 数据为空则休眠5s
            try {
                Thread.sleep((DispatchService.PERIOD / 2) * 1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }

            putLastAt(groupName, defLastAt);
        }

    }

  AbstractScanGroup该抽象类有两个子类:ScanCallbackGroupActor和ScanGroupActor

   productExecUnitActor(executor); //抽象父类中定义的方法
   private void productExecUnitActor(RetryExecutor retryExecutor) {
        String groupIdHash = retryExecutor.getRetryContext().getRetryTask().getGroupName();
        Long retryId = retryExecutor.getRetryContext().getRetryTask().getId();
        idempotentStrategy.set(groupIdHash, retryId.intValue());

        // 重试成功回调客户端
        ActorRef actorRef = getActorRef();
        actorRef.tell(retryExecutor, actorRef);
    }
   // 两个子类中都有该重试客户端的方法
   @Override
    protected ActorRef getActorRef() {
        return ActorGenerator.execUnitActor();
    }
   //getActorRef()方法会调用ActorGenerator类里面的方法来生成一个Actor生成器,通过akka的属性然后将这个ExecUnitActor执行器的类注入到spring容器中
   getDispatchExecUnitActorSystem().actorOf(getSpringExtension().props(ExecUnitActor.BEAN_NAME)

  akka采用消息的发布订阅模型,生产者发布消息,消费者只订阅自己感兴趣的主题,然后接收消息,这样就具有解耦的功能。

  ExecUnitActor类里面的createReceive()方法才是具体给客户端发送重试请求的执行者:

package com.aizuda.easy.retry.server.support.dispatch.actor.exec;

import akka.actor.AbstractActor;
import cn.hutool.core.lang.Assert;
import com.aizuda.easy.retry.client.model.DispatchRetryDTO;
import com.aizuda.easy.retry.client.model.DispatchRetryResultDTO;
import com.aizuda.easy.retry.common.core.constant.SystemConstants;
import com.aizuda.easy.retry.common.core.log.LogUtils;
import com.aizuda.easy.retry.common.core.model.Result;
import com.aizuda.easy.retry.common.core.model.EasyRetryHeaders;
import com.aizuda.easy.retry.common.core.util.JsonUtil;
import com.aizuda.easy.retry.server.exception.EasyRetryServerException;
import com.aizuda.easy.retry.server.persistence.mybatis.mapper.RetryTaskLogMapper;
import com.aizuda.easy.retry.server.persistence.mybatis.po.RetryTask;
import com.aizuda.easy.retry.server.persistence.mybatis.po.RetryTaskLog;
import com.aizuda.easy.retry.server.persistence.mybatis.po.ServerNode;
import com.aizuda.easy.retry.server.service.convert.RetryTaskLogConverter;
import com.aizuda.easy.retry.server.support.IdempotentStrategy;
import com.aizuda.easy.retry.server.support.context.MaxAttemptsPersistenceRetryContext;
import com.aizuda.easy.retry.server.support.retry.RetryExecutor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.text.MessageFormat;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.concurrent.Callable;

/**
 * 重试结果执行器
 *
 * @author www.byteblogs.com
 * @date 2021-10-30
 * @since 2.0
 */
@Component("ExecUnitActor")
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Slf4j
public class ExecUnitActor extends AbstractActor  {

    public static final String BEAN_NAME = "ExecUnitActor";
    public static final String URL = "http://{0}:{1}/{2}/retry/dispatch/v1";

    @Autowired
    @Qualifier("bitSetIdempotentStrategyHandler")
    private IdempotentStrategy<String, Integer> idempotentStrategy;
    @Autowired
    private RetryTaskLogMapper retryTaskLogMapper;
    @Autowired
    private RestTemplate restTemplate;

    @Override
    public Receive createReceive() {
        return receiveBuilder().match(RetryExecutor.class, retryExecutor -> {

            MaxAttemptsPersistenceRetryContext context = (MaxAttemptsPersistenceRetryContext) retryExecutor.getRetryContext();
            RetryTask retryTask = context.getRetryTask();
            ServerNode serverNode = context.getServerNode();

            RetryTaskLog retryTaskLog = RetryTaskLogConverter.INSTANCE.toRetryTask(retryTask);
            retryTaskLog.setErrorMessage(StringUtils.EMPTY);

            try {

                if (Objects.nonNull(serverNode)) {
                    retryExecutor.call((Callable<Result<DispatchRetryResultDTO>>) () -> callClient(retryTask, retryTaskLog, serverNode));
                    if (context.hasException()) {
                        retryTaskLog.setErrorMessage(context.getException().getMessage());
                    }
                } else {
                    retryTaskLog.setErrorMessage("暂无可用的客户端POD");
                }

            }catch (Exception e) {
                LogUtils.error(log, "回调客户端失败 retryTask:[{}]", JsonUtil.toJsonString(retryTask), e);
                retryTaskLog.setErrorMessage(StringUtils.isBlank(e.getMessage()) ? StringUtils.EMPTY : e.getMessage());
            } finally {

                // 清除幂等标识位
                idempotentStrategy.clear(retryTask.getGroupName(), retryTask.getId().intValue());
                getContext().stop(getSelf());

                // 记录重试日志
                retryTaskLog.setCreateDt(LocalDateTime.now());
                retryTaskLog.setId(null);
                Assert.isTrue(1 ==  retryTaskLogMapper.insert(retryTaskLog),
                    () -> new EasyRetryServerException("新增重试日志失败"));
            }

        }).build();
    }

    /**
     * 调用客户端
     *
     * @param retryTask {@link RetryTask} 需要重试的数据
     * @return 重试结果返回值
     */
    private Result<DispatchRetryResultDTO> callClient(RetryTask retryTask, RetryTaskLog retryTaskLog, ServerNode serverNode) {

        DispatchRetryDTO dispatchRetryDTO = new DispatchRetryDTO();
        dispatchRetryDTO.setIdempotentId(retryTask.getIdempotentId());
        dispatchRetryDTO.setScene(retryTask.getSceneName());
        dispatchRetryDTO.setExecutorName(retryTask.getExecutorName());
        dispatchRetryDTO.setArgsStr(retryTask.getArgsStr());
        dispatchRetryDTO.setUniqueId(retryTask.getUniqueId());
        dispatchRetryDTO.setRetryCount(retryTask.getRetryCount());

        // 设置header
        HttpHeaders requestHeaders = new HttpHeaders();
        EasyRetryHeaders easyRetryHeaders = new EasyRetryHeaders();
        easyRetryHeaders.setEasyRetry(Boolean.TRUE);
        easyRetryHeaders.setEasyRetryId(retryTask.getUniqueId());
        requestHeaders.add(SystemConstants.EASY_RETRY_HEAD_KEY, JsonUtil.toJsonString(easyRetryHeaders));

        HttpEntity<DispatchRetryDTO> requestEntity = new HttpEntity<>(dispatchRetryDTO, requestHeaders);

        String format = MessageFormat.format(URL, serverNode.getHostIp(), serverNode.getHostPort().toString(), serverNode.getContextPath());
        Result<DispatchRetryResultDTO> result = restTemplate.postForObject(format, requestEntity, Result.class);

        if (1 != result.getStatus()  && StringUtils.isNotBlank(result.getMessage())) {
            retryTaskLog.setErrorMessage(result.getMessage());
        } else {
            DispatchRetryResultDTO data = JsonUtil.parseObject(JsonUtil.toJsonString(result.getData()), DispatchRetryResultDTO.class);
            result.setData(data);
            if (Objects.nonNull(data) && StringUtils.isNotBlank(data.getExceptionMsg())) {
                retryTaskLog.setErrorMessage(data.getExceptionMsg());
            }

        }

        LogUtils.info(log, "请求客户端 response:[{}}] ", JsonUtil.toJsonString(result));
        return result;

    }

}

  可以看出服务端给客户端发送重试是使用的是:restTemplate的方式

5.10 服务端手动下发重试策略

@PostMapping("/generate/idempotent-id")
    public Result<String> idempotentIdGenerate(@RequestBody @Validated GenerateRetryIdempotentIdVO generateRetryIdempotentIdVO){
        return new Result<>(retryTaskService.idempotentIdGenerate(generateRetryIdempotentIdVO));
}

RetryTaskServiceImplle类的idempotentIdGenerate()方法:

@Override
    public String idempotentIdGenerate(final GenerateRetryIdempotentIdVO generateRetryIdempotentIdVO) {
        ServerNode serverNode = clientNodeAllocateHandler.getServerNode(generateRetryIdempotentIdVO.getGroupName());
        Assert.notNull(serverNode, () -> new EasyRetryServerException("生成idempotentId失败: 不存在活跃的客户端节点"));

        // 委托客户端生成idempotentId
        String url = MessageFormat
            .format(URL, serverNode.getHostIp(), serverNode.getHostPort().toString(), serverNode.getContextPath());

        GenerateRetryIdempotentIdDTO generateRetryIdempotentIdDTO = new GenerateRetryIdempotentIdDTO();
        generateRetryIdempotentIdDTO.setGroup(generateRetryIdempotentIdVO.getGroupName());
        generateRetryIdempotentIdDTO.setScene(generateRetryIdempotentIdVO.getSceneName());
        generateRetryIdempotentIdDTO.setArgsStr(generateRetryIdempotentIdVO.getArgsStr());
        generateRetryIdempotentIdDTO.setExecutorName(generateRetryIdempotentIdVO.getExecutorName());

        HttpEntity<GenerateRetryIdempotentIdDTO> requestEntity = new HttpEntity<>(generateRetryIdempotentIdDTO);
        Result result = restTemplate.postForObject(url, requestEntity, Result.class);

        Assert.notNull(result, () -> new EasyRetryServerException("idempotentId生成失败"));
        Assert.isTrue(1 == result.getStatus(), () -> new EasyRetryServerException("idempotentId生成失败:请确保参数与执行器名称正确"));

        return (String) result.getData();
    }

关键代码如下:

    ServerNode serverNode = clientNodeAllocateHandler.getServerNode(generateRetryIdempotentIdVO.getGroupName());
     /**
     * 获取分配的节点,获取服务端的节点,服务端信息采用数据库(server_node表就是记录服务端的节点信息)做了一个集群,选择一个服务端来执行重试任务
     */
    public ServerNode getServerNode(String groupName) {

        GroupConfig groupConfig = configAccess.getGroupConfigByGroupName(groupName);
        List<ServerNode> serverNodes = serverNodeMapper.selectList(new LambdaQueryWrapper<ServerNode>().eq(ServerNode::getGroupName, groupName));

        if (CollectionUtils.isEmpty(serverNodes)) {
            return null;
        }

        ClientLoadBalance clientLoadBalanceRandom = ClientLoadBalanceManager.getClientLoadBalance(groupConfig.getRouteKey());

        String hostIp = clientLoadBalanceRandom.route(groupName, new TreeSet<>(serverNodes.stream().map(ServerNode::getHostIp).collect(Collectors.toSet())));
        return serverNodes.stream().filter(s -> s.getHostIp().equals(hostIp)).findFirst().get();
    }

ClientLoadBalanceManager类的选择客户端节点的算法有如下几种:

        CONSISTENT_HASH(1, new ClientLoadBalanceConsistentHash(100)), //一致性hash
        RANDOM(2, new ClientLoadBalanceRandom()), //随机
        LRU(3, new ClientLoadBalanceLRU(100)), // LRU

5.11 客户端接收服务端下发重试的端点RetryEndPoint

package com.aizuda.easy.retry.client.core.client;

import cn.hutool.core.lang.Assert;
import com.aizuda.easy.retry.client.core.IdempotentIdGenerate;
import com.aizuda.easy.retry.client.core.RetryArgSerializer;
import com.aizuda.easy.retry.client.core.cache.GroupVersionCache;
import com.aizuda.easy.retry.client.core.cache.RetryerInfoCache;
import com.aizuda.easy.retry.client.core.callback.RetryCompleteCallback;
import com.aizuda.easy.retry.client.core.exception.EasyRetryClientException;
import com.aizuda.easy.retry.client.core.intercepter.RetrySiteSnapshot;
import com.aizuda.easy.retry.client.core.retryer.RetryerInfo;
import com.aizuda.easy.retry.client.core.retryer.RetryerResultContext;
import com.aizuda.easy.retry.client.core.serializer.JacksonSerializer;
import com.aizuda.easy.retry.client.core.strategy.RetryStrategy;
import com.aizuda.easy.retry.client.model.GenerateRetryIdempotentIdDTO;
import com.aizuda.easy.retry.common.core.context.SpringContext;
import com.aizuda.easy.retry.common.core.enums.RetryResultStatusEnum;
import com.aizuda.easy.retry.common.core.enums.RetryStatusEnum;
import com.aizuda.easy.retry.common.core.log.LogUtils;
import com.aizuda.easy.retry.common.core.model.Result;
import com.aizuda.easy.retry.common.core.util.JsonUtil;
import com.aizuda.easy.retry.server.model.dto.ConfigDTO;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.aizuda.easy.retry.client.model.DispatchRetryDTO;
import com.aizuda.easy.retry.client.model.DispatchRetryResultDTO;
import com.aizuda.easy.retry.client.model.RetryCallbackDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.util.ReflectionUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Objects;

/**
 * 服务端调调用客户端进行重试流量下发、配置变更通知等操作
 *
 * @author: www.byteblogs.com
 * @date : 2022-03-09 16:33
 */
@RestController
@RequestMapping("/retry")
@Slf4j
public class RetryEndPoint {

    @Autowired
    @Qualifier("remoteRetryStrategies")
    private RetryStrategy retryStrategy;

    /**
     * 服务端调度重试入口
     */
    @PostMapping("/dispatch/v1")
   public Result<DispatchRetryResultDTO> dispatch(@RequestBody DispatchRetryDTO executeReqDto) {

        RetryerInfo retryerInfo = RetryerInfoCache.get(executeReqDto.getScene(), executeReqDto.getExecutorName());
        if (Objects.isNull(retryerInfo)) {
            throw new EasyRetryClientException("场景:[{}]配置不存在", executeReqDto.getScene());
        }

        RetryArgSerializer retryArgSerializer = new JacksonSerializer();

        Object[] deSerialize = null;
        try {
            deSerialize = (Object[]) retryArgSerializer.deSerialize(executeReqDto.getArgsStr(), retryerInfo.getExecutor().getClass(), retryerInfo.getMethod());
        } catch (JsonProcessingException e) {
            throw new EasyRetryClientException("参数解析异常", e);
        }

        DispatchRetryResultDTO executeRespDto = new DispatchRetryResultDTO();

        try {
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
            request.setAttribute("attemptNumber", executeReqDto.getRetryCount());

            RetryerResultContext retryerResultContext = retryStrategy.openRetry(executeReqDto.getScene(), executeReqDto.getExecutorName(), deSerialize);

            if (RetrySiteSnapshot.isRetryForStatusCode()) {
                executeRespDto.setStatusCode(RetryResultStatusEnum.STOP.getStatus());

                // TODO 需要标记是哪个系统不需要重试
                executeRespDto.setExceptionMsg("下游标记不需要重试");
            } else {
                executeRespDto.setStatusCode(retryerResultContext.getRetryResultStatusEnum().getStatus());
                executeRespDto.setExceptionMsg(retryerResultContext.getMessage());
            }

            executeRespDto.setIdempotentId(executeReqDto.getIdempotentId());
            executeRespDto.setUniqueId(executeReqDto.getUniqueId());
            if (Objects.nonNull(retryerResultContext.getResult())) {
                executeRespDto.setResultJson(JsonUtil.toJsonString(retryerResultContext.getResult()));
            }


        } finally {
            RetrySiteSnapshot.removeAll();
        }

        return new Result<>(executeRespDto);
    }

    /**
     * 同步版本
     */
    @PostMapping("/sync/version/v1")
    public Result syncVersion(@RequestBody ConfigDTO configDTO) {
        GroupVersionCache.configDTO = configDTO;
        return new Result();
    }

    @PostMapping("/callback/v1")
    public Result callback(@RequestBody RetryCallbackDTO callbackDTO) {
        RetryerInfo retryerInfo = RetryerInfoCache.get(callbackDTO.getScene(), callbackDTO.getExecutorName());
        if (Objects.isNull(retryerInfo)) {
            throw new EasyRetryClientException("场景:[{}]配置不存在", callbackDTO.getScene());
        }

        RetryArgSerializer retryArgSerializer = new JacksonSerializer();

        Object[] deSerialize = null;
        try {
            deSerialize = (Object[]) retryArgSerializer.deSerialize(callbackDTO.getArgsStr(), retryerInfo.getExecutor().getClass(), retryerInfo.getMethod());
        } catch (JsonProcessingException e) {
            throw new EasyRetryClientException("参数解析异常", e);
        }

        Class<? extends RetryCompleteCallback> retryCompleteCallbackClazz = retryerInfo.getRetryCompleteCallback();
        RetryCompleteCallback retryCompleteCallback = SpringContext.getBeanByType(retryCompleteCallbackClazz);

        if (RetryStatusEnum.FINISH.getStatus().equals(callbackDTO.getRetryStatus())) {
            retryCompleteCallback.doSuccessCallback(retryerInfo.getScene(), retryerInfo.getExecutorClassName(), deSerialize);
        }

        if (RetryStatusEnum.MAX_RETRY_COUNT.getStatus().equals(callbackDTO.getRetryStatus())) {
            retryCompleteCallback.doMaxRetryCallback(retryerInfo.getScene(), retryerInfo.getExecutorClassName(), deSerialize);
        }

        return new Result();
    }

    /**
     * 手动新增重试数据,模拟生成idempotentId
     *
     * @param generateRetryIdempotentIdDTO 生成idempotentId模型
     * @return idempotentId
     */
    @PostMapping("/generate/idempotent-id/v1")
    public Result<String> idempotentIdGenerate(@RequestBody @Validated GenerateRetryIdempotentIdDTO generateRetryIdempotentIdDTO) {

        String scene = generateRetryIdempotentIdDTO.getScene();
        String executorName = generateRetryIdempotentIdDTO.getExecutorName();
        String argsStr = generateRetryIdempotentIdDTO.getArgsStr();

        RetryerInfo retryerInfo = RetryerInfoCache.get(scene, executorName);
        Assert.notNull(retryerInfo, ()-> new EasyRetryClientException("重试信息不存在 scene:[{}] executorName:[{}]", scene, executorName));

        Method executorMethod = retryerInfo.getMethod();

        RetryArgSerializer retryArgSerializer = new JacksonSerializer();

        Object[] deSerialize = null;
        try {
            deSerialize = (Object[]) retryArgSerializer.deSerialize(argsStr, retryerInfo.getExecutor().getClass(), retryerInfo.getMethod());
        } catch (JsonProcessingException e) {
            throw new EasyRetryClientException("参数解析异常", e);
        }

        String idempotentId;
        try {
            Class<? extends IdempotentIdGenerate> idempotentIdGenerate = retryerInfo.getIdempotentIdGenerate();
            IdempotentIdGenerate generate = idempotentIdGenerate.newInstance();
            Method method = idempotentIdGenerate.getMethod("idGenerate", Object[].class);
            Object p = new Object[]{scene, executorName, deSerialize, executorMethod.getName()};
            idempotentId = (String) ReflectionUtils.invokeMethod(method, generate, p);
        } catch (Exception exception) {
            LogUtils.error(log, "幂等id生成异常:{},{}", scene, argsStr, exception);
            throw new EasyRetryClientException("idempotentId生成异常:{},{}", scene, argsStr);
        }

        return new Result<>(idempotentId);
    }
}

可以看到只有服务端重试会再次上报,手动重试的不会:

RetryerResultContext retryerResultContext = retryStrategy.openRetry(executeReqDto.getScene(), executeReqDto.getExecutorName(), deSerialize);

5.12 服务端的schedule任务

图片

schedule任务里面使用了:@SchedulerLock注解 和数据库加了一张表:shedlock表

图片

这种就可以然服务端是集群部署的时候只有一个节点可以执行定时任务了。

@SchedulerLock详解

https://blog.csdn.net/qq_45498460/article/details/119454759

6.集群架构

图片

7.总结

  到此easy-retry分布式开源重试框架已经分享完了,一般这种框架都是这种套路的,使用netty来做客户端和服务端的心跳、采集、监控、上报,只不过每一个侧重解决的业务痛点不一样,就比如xxl-job分布式任务框架,简单的业务中使用spring-retry就足够了,希望我的分享对你有帮助,请一键三连,么么哒!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/29372.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

青岛科技大学|物联网工程|物联网定位技术(第三讲)|15:40

目录 物联网定位技术&#xff08;第三讲&#xff09; 1. 试简述C/A码的作用、构成 请画出C/A码生成电路简图并给予原理性的说明 2. 试简述 P码的作用、构成 请画出P码生成电路简图&#xff0c;并给予原理性的说明 3. GPS信号是如何进行伪码扩频与解扩 请画图给予说明 4…

被抄袭声明

我&#xff08;受害者&#xff09;的博客主页&#xff1a; ChuckieZhu的博客_CSDN博客-MATLAB,Python,Django领域博主 抄袭者&#xff08;施害者&#xff09;博客主页&#xff1a; 洋洋菜鸟的博客_CSDN博客-python实例,数学建模,python基础领域博主 问题说明&#xff1a; …

vue结合elementui表格el-table实现弹窗checkbox自定义列显示隐藏,刷新保持上次勾选不丢失,附完整代码

el-table实现自定义列显示隐藏 有时候表格太多列&#xff0c;要是默认全都显示就会很拥挤&#xff0c;又不能固定只显示某些列&#xff0c;这时候我们可以让用户自定义要显示隐藏哪些列。 网上很多教程都是用的v-if&#xff0c;但是v-if非常麻烦&#xff0c;每一列都要写判断条…

Volo.Abp升级小记(二)创建全新微服务模块

文章目录 创建模块领域层应用层数据库和仓储控制器配置微服务 测试微服务微服务注册添加资源配置配置网关 运行项目 假设有一个按照 官方sample搭建的微服务项目&#xff0c;并安装好了abp-cli。 需要创建一个名为GDMK.CAH.Common的模块&#xff0c;并在模块中创建标签管理功能…

Centos环境 使用docker 部署MySQL 8.X详细版本

文章目录 安装docker配置docker 阿里镜像加速阿里云容器镜像服务ACR配置镜像源 安装部署MySQL拉取MySQL镜像创建挂载文件测试部署部署MySQL进入容器将它的mysql配置同步给宿主机删除test1测试容器 正式部署MySQL查看正式部署的容器状态配置远程连接字符集以及关闭跳过密码验证等…

基于STM32C8T6的智能小车项目时钟配置

一、时钟树简介 HSE 是高速的外部时钟信号&#xff0c;可以由有源晶振或者无源晶振提供&#xff0c;频率从 3-25MHZ 不等。当使用有源晶振时&#xff0c;时钟从 OSC_IN 引脚进入&#xff0c;OSC_OUT 引脚悬空&#xff0c;当选用无源 晶振时&#xff0c;时钟从 OSC_IN 和 OSC_OU…

【工作中遇到的性能优化问题】

项目场景&#xff1a; 页面左侧有一列表数据&#xff0c;点击列表项会查对应的表格数据和表单信息&#xff08;表单是根据数据配置生成的&#xff09;&#xff0c;并在右侧展示。如果数据量大&#xff0c;则非常卡。 需要对此页面进行优化。 问题描述 问题一、加载左侧数据时…

spring boot+easyui粮油质量管控防伪溯源系统源码

基于物联网技术、RFID技术和RSA、PGP加密算法开发的粮油质量追溯系统 粮油安全关系千千万万消费者的健康问题。近年来&#xff0c;许多食品行业安全事故频频涌现&#xff0c;成为社会关注焦点。粮油生产加工质量管控防伪溯源系统为粮油提供从种植、生产、加工、销售等各环节的…

L9110S电机驱动模块demo

0.资料 项目工程文件夹 分文件原理 1.认识L9110S 1、概述&#xff1a; 一个L9110S驱动可以控制一个电机&#xff0c;图中左右两个黑色芯片就是L9110S驱动。当然如果会硬件也可以直接把它们设计到单片机开发板上。 一个电机由两个针脚控制&#xff0c;我们用杜邦线让L9110S…

Modbus通信介绍 网络高级工具使用

目录 Modbus简介 ModbusTCP协议格式 》1.报文头&#xff08;共7字节&#xff09; 》2.功能码 》3.数据 练习&#xff1a;读传感器数据&#xff0c;读1个寄存器数据&#xff0c;写出主从数据收发协议。 练习&#xff1a;写出控制IO设备开关的协议数据&#xff0c;操作1个…

【2】Midjourney注册

随着AI技术的问世&#xff0c;2023年可以说是AI爆炸性成长的一年&#xff0c;近期最广为人知的AI服务除了chatgpt外&#xff0c;就是从去年五月就已经问世的AI绘画工具mid journey了。 ▲几个AI工具也代表了人工智能的热门阶段 只要输入一段文字&#xff0c;AI就会根据语意计算…

JAVA如何学习爬虫呢?

学习Java爬虫需要掌握以下几个方面&#xff1a; Java基础知识&#xff1a;包括Java语法、面向对象编程、集合框架等。 网络编程&#xff1a;了解HTTP协议、Socket编程等。 HTML、CSS、JavaScript基础&#xff1a;了解网页的基本结构和样式&#xff0c;以及JavaScript的基本语…

Web网页制作期末复习(1)——HTML5介绍、HTML5的DOCTYPE声明、HTML基本骨架、标题标签、段落 换行、水平线图片图片路径、超链接

目录 HTML5介绍 HTML5的DOCTYPE声明 HTML基本骨架 标题标签 段落、换行、水平线 图片 图片路径* 超链接 HTML5介绍 HTML5是用来描述网页的一种语言&#xff0c;被称为超文本标记语言。用HTML5编写的文件&#xff0c;后缀以.html结尾 HTML是一种标记语言&#xff0c;标…

基于机器学习算法:朴素贝叶斯和SVM 分类-垃圾邮件识别分类系统(含Python工程全源码)

目录 前言总体设计系统整体结构图系统流程图 运行环境Python 环境安装pytesseract注册百度云账号 模块实现1. 数据模块2. 模型构建3. 附加功能 系统测试1. 文字邮件测试准确率2. 网页测试结果 工程源代码下载其它资料下载 前言 本项目采用朴素贝叶斯和支持向量机&#xff08;S…

MySQL-SQL视图详细

♥️作者&#xff1a;小刘在C站 ♥️个人主页&#xff1a; 小刘主页 ♥️努力不一定有回报&#xff0c;但一定会有收获加油&#xff01;一起努力&#xff0c;共赴美好人生&#xff01; ♥️学习两年总结出的运维经验&#xff0c;以及思科模拟器全套网络实验教程。专栏&#xf…

原型模式(七)

不管怎么样&#xff0c;都要继续充满着希望 上一章简单介绍了抽象工厂模式(六), 如果没有看过,请观看上一章 一. 原型模式 引用 菜鸟教程里面的原型模式介绍: https://www.runoob.com/design-pattern/prototype-pattern.html 原型模式&#xff08;Prototype Pattern&#xf…

Camera | 11.瑞芯微摄像头采集图像颜色偏绿解决笔记

前言 在实际调试基于瑞芯微平台的camera过程中&#xff0c;发现显示的图片发绿&#xff0c; 现在把调试步骤分享给大家&#xff1a; 1、修改iq文件 sdk中位置&#xff1a; external/camera_engine_rkaiq/iqfiles/isp21/ov13850_ZC-OV13850R2A-V1_Largan-50064B31.xml【现在…

QT基础教程之一创建Qt项目

QT基础教程1创建Qt项目 根据模板创建 打开Qt Creator 界面选择 New Project或者选择菜单栏 【文件】-【新建文件或项目】菜单项 弹出New Project对话框&#xff0c;选择Qt Widgets Application 选择【Choose】按钮&#xff0c;弹出如下对话框 设置项目名称和路径&#xff0c;…

javaScript蓝桥杯---用什么来做计算

目录 一、介绍二、准备三、目标四、代码五、完成 一、介绍 古以算盘作为计算工具。算盘常为木制矩框&#xff0c;内嵌珠子数串&#xff0c;定位拨珠&#xff0c;可做加减乘除等运算。站在前人的肩膀上&#xff0c;后人研究出计算器&#xff0c;便利了大家的生活&#xff0c;我…

element-plus布局排版问题总结(更新ing)

文章目录 el-container空隙修改app组件 el-header容器空隙检查前端修改el-headerel-container el-container空隙 源码-更改了容器的显示&#xff0c;占满屏幕 <template><div class"common-layout"><el-container><el-header><el-row cl…