微服务框架需要处理哪些问题?

news2024/11/15 17:59:48

文章目录

    • 简述
    • 架构选择
    • 统一版本管理
      • 基础框架包管理
      • 业务框架包管理
    • 模型分层
    • 全局上下文管理
      • 数据结构定义
      • 上下文的传播
    • 前后端数据格式协定
      • 统一数据格式
      • 字段规范协定
    • 异常处理
    • orm配置
      • 公共字段处理
      • 分页处理
      • 字段加解密
    • 缓存
      • key的序列化
      • 哪些数据进行缓存
    • 消息队列
      • key的规范
      • 队列的管理
    • 注册中心
    • 配置中心
      • 日志配置的热更新
      • 普通配置的热更新
      • 公共配置的抽取
    • 网关
      • 统一鉴权
      • 负载均衡
      • 限流降级
    • 日志的收集
      • 普通日志处理
      • 审计\操作日志处理
    • 文档管理
    • 定时任务

简述

其实很多人不理解框架和架构的界限,架构的范围比框架来的要宽泛很多,框架是架构实施的具体体现。所以我们这里谈的是作为一个框架,应该需要做出什么处理。而架构可能不止涉及开发这部分。架构是个很泛的概念,如以下架构的选择,只是一种服务调用的方式。

架构选择

见过各种各样的微服务实现方式,各家长短我们这边不讨论,各自对号入座,常见的为以下三种方式。当然也有比较另类的方式。
第一种对业务进行分层禁止反向调用。

订单
商品
库存

在服务都在同一层的情况下,又需要进行互调怎么办,在所有服务上加一层服务做为边界层,进行服务聚合。

边界层
订单
商品
库存

这种方式就是允许服务间进行互调,可能会产生服务调用循环问题。但是控制的好的话正常也不会有什么问题。

订单
商品
库存

另类的方式,比如把所有服务的sql整合到一个服务中,由单一的一个服务提供数据服务,这种是真的很另类,但是确实有人这么实现。

记得之前提过一个问题,就是feign-client 层到底是由服务开发者来写,还是由服务调用者来写?
从架构第一性原则出发,我也不说哪个更合适,因为出于权衡的角度来思考这个问题,有的人考虑了开发的便利性,决定由服务开发者来写;而有的人从微服务的松耦合原则上考虑,当然由服务调用者来写比较合适,因为如果由服务开发者来提供调用包的方式,会产生服务依赖,导致在cicd阶段,需要进行依赖编译,就需要用进行服务编排。而如果由调用者自己写接口调用,那么无需依赖,服务间没有直接依赖。当然还有第三种选择,就是抽取一个公共包,所有的服务接口全部写道公共包中的方式,这样所有服务只需要共同依赖一个接口包,这种方式可以说对前两种方式进行了折中。

统一版本管理

试想一下如果框架层面没有对版本做出统一的管理,随着时间的推移,每个开发各自引入各自的包会产生什么样的问题?当然就是各种包版本混乱各种包冲突之类的问题就随之而来。所以我们一般需要用一个pom将所有的版本统一管理起来,当遇到版本冲突时,可以根据此pom进行版本裁决,便于架构师对版本进行管理

基础框架包管理

如果公司有自己写的框架,以及中间件,这些框架作为基础框架需要进行单独管理。比如一些所有项目都需要共用的内容也可以纳入基础框架包中。

业务框架包管理

在基础框架包之上需要引入各种基础框架包。然后根据需要进行分包配置。比如有的项目需要用到mq 有的项目需要用到redis,而有的项目不需要,所以此时需要将这些组件的包进行单独拆包配置。

模型分层

我们一般对数据模型进行一个分层管理

  • po 持久层模型,对应数据库表结构
  • dto 传输层模型,作为接口入参,服务层与控制器层之间的数据传输
  • vo 视图层模型,对应接口返回

但是分层之后会出现一个问题就是模型的拷贝来拷贝去是比较浪费性能的,尤其是为了便利使用的各种BeanCopyUtil产生的各种血案。所有有的人会直接使用po穿透到前端,实际上这种方式对于一些小系统,或者小公司而言也没什么问题。但是正常情况下我们还是会对模型进行一个分层处理,这样能让业务模型更加精准,提高数据的安全性,但是最好规定下copy规范,使用getter、setter进行数据设置,这个可以通过安装开发插件的方式处理,也可以规定只能使用cglib的beancopy,禁止使用反射,反射性能低下,配置不恰当还可能照成metaspace溢出之类的问题。当然通过配置也能解决此类问题,无非就是浪费点性能和资源。

全局上下文管理

一般我们系统需要一些全局上下文的缓存,在一次请求中记录请求的状态信息仅供一次请求全局使用。一般使用threadlocal来处理,线程结束后需要进行清理,避免缓存泄漏或者出现线程复用产生脏数据。

数据结构定义

正常情况下数据结构分为特定数据,非特定数据

  • 特定数据,比如记录登入相关的信息
  • 非特定数据,比如一些业务处理的状态需要流转到下一个服务

上下文的传播

上下文的传播我们一般可以将一个请求中产生的全局状态通过请求传播到下一个服务中。一般可以将对应的状态数据通过请求头进行传播。除了服务间的传播之外,我们还应当考虑异步线程间的传播。

前后端数据格式协定

一个统一的数据格式有助于前端架构的统一化

统一数据格式

  • 错误码
  • 错误消息
  • 泛型数据

可以参考如下:所有的方式必须使用该类进行包装

@Data
public class JsonResult<T> implements Serializable {
    private static final long serialVersionUID = 1559840165163L;
    private Integer code;
    private String message;
    private T data;

    public JsonResult() {
    }

    public JsonResult(Integer code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public JsonResult(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public static <T> JsonResult<T> success(String message, T data) {
        JsonResult jsonResult = new JsonResult();
        jsonResult.setCode(Const.CODE_SUCCESS);
        jsonResult.setMessage(message);
        jsonResult.setData(data);
        return jsonResult;
    }

    public static <T> JsonResult<T> success(T data) {
        JsonResult jsonResult = new JsonResult();
        jsonResult.setCode(Const.CODE_SUCCESS);
        jsonResult.setMessage("操作成功");
        jsonResult.setData(data);
        return jsonResult;
    }

    public static <T> JsonResult<T> success() {
        JsonResult jsonResult = new JsonResult();
        jsonResult.setCode(Const.CODE_SUCCESS);
        jsonResult.setMessage("操作成功");
        jsonResult.setData(null);
        return jsonResult;
    }

    public static <T> JsonResult<T> error(String message) {
        JsonResult jsonResult = new JsonResult();
        jsonResult.setCode(Const.CODE_FAILED);
        jsonResult.setMessage(message);
        jsonResult.setData(null);
        return jsonResult;
    }

    public static <T> JsonResult<T> error(String message, T data) {
        JsonResult jsonResult = new JsonResult();
        jsonResult.setCode(Const.CODE_FAILED);
        jsonResult.setMessage(message);
        jsonResult.setData(data);
        return jsonResult;
    }

    public static <T> JsonResult<T> fail() {
        JsonResult jsonResult = new JsonResult();
        jsonResult.setCode(Const.CODE_FAILED);
        jsonResult.setMessage("操作失败");
        return jsonResult;
    }
    
    public static <T> JsonResult<T> fail(String message) {
        JsonResult jsonResult = new JsonResult();
        jsonResult.setCode(Const.CODE_FAILED);
        jsonResult.setMessage(message);
        return jsonResult;
    }
    @JsonIgnore
    public boolean isSuccess() {
        return Const.CODE_SUCCESS.equals(this.code);
    }
    @JsonIgnore
    public boolean isFail() {
        return !isSuccess();
    }

}

字段规范协定

除了统一返回之外,我们还需要对某些比较特殊的结构进行一些定制,比如日期格式的统一,还有对空列表的序列化,比如规定空列表是返回null,还是返回空列表,正常我们可以返回空列表,这样前端拿到空列表不需要在进行处理可以直接填充,还有一般的null是返回空,还是索性不返回这个字段等等。

  • 日期
  • 空列表

异常处理

在异常处理中,我们一般可以封装一个业务异常,方便开发在深层调用出现异常时直接抛出,而不用进行层层return,虽然牺牲了点性能,还是从第一性原则上讲,这种方式的对于开发便利性的收益是非常可观的。

同时我们需要进行异常的全局拦截,并将异常收敛在当前服务,再将异常包装成统一返回。但是将异常收敛之后,链路追踪类的组件也就无法进行捕获,所以需要当前项目进行链路上报,此时需要在对全局异常处理类再进行一层拦截,收集异常信息再进行上报。
对ExcelptionHandler进行切面处理上报异常给skywalking

@Aspect
public class ExceptionHandlerAspect {
    
    @Around(value = "@annotation(org.springframework.web.bind.annotation.ExceptionHandler)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if (arg instanceof Throwable) {
                ActiveSpan.error((Throwable)arg);
            }
        }
        return joinPoint.proceed();
    }
}

orm配置

在开发过程中,开发效率以及开发便利性是架构权衡很重要的一个质量属性,所以我们一般以实体为中心,在做架构设计的时候,为了这个质量属性,我们正常有代码生成器生成并且多次生成时幂等的。比如我一个实体有10个字段,现在我新增了一个字段,我再次生成之后是不会影响原本所有的现有代码。例如我们使用mybatis时我们会将xml文件分成2个,一个生成的、一个由开发编写的,生成的部分我们不允许修改。当我们重新生成代码时我么仅覆盖默认的xml以及实体。

公共字段处理

我们在做数据库设计时我们正常会规定一些公共字段,比如createt_time、update_time、create_user、update_user、deleted这些公共字段。那么此时我们需要对这些公共字段进行合理的配置,并将这些字段抽取成BasePO,让所有PO继承至这个BasePO。并在框架层面自动填充。
比如mybatis-plus中我们可以利用MetaObjectHandler处理此类问题

public class BasePOMetaObjectHandler implements MetaObjectHandler {
        private final static String update_time="updateTime";
        private final static String create_time="createTime";
        private final static String update_user_code="updateUserCode";
        private final static String create_user_code="createUserCode";
        private final static String update_username="updateUsername";
        private final static String create_username="createUsername";
        private final static String deleted="deleted";
        private final static String available="available";
        
    @Override
    public void insertFill(MetaObject metaObject) {
        LocalDateTime now = LocalDateTime.now();
        this.setFieldValByName(create_time, now,metaObject);
        this.setFieldValByName(update_time, now,metaObject);
        this.setFieldValByName(create_user_code, StringUtils.defaultIfEmpty(SystemContext.getUserInfo().getUserCode(),"0"),metaObject);
        this.setFieldValByName(create_username,StringUtils.defaultIfEmpty(SystemContext.getUserInfo().getUserName(),"system"),metaObject);
        this.setFieldValByName(deleted,0,metaObject);
        //this.setFieldValByName(available,1,metaObject);
    }
    
    @Override
    public void updateFill(MetaObject metaObject) {
        this.setFieldValByName(update_time, LocalDateTime.now(),metaObject);
        this.setFieldValByName(update_user_code,StringUtils.defaultIfEmpty(SystemContext.getUserInfo().getUserCode(),"0"),metaObject);
        this.setFieldValByName(update_username,StringUtils.defaultIfEmpty(SystemContext.getUserInfo().getUserName(),"system"),metaObject);
    }
}

分页处理

系统中一般存在大量的分页查询,在做分页时,我们通常也是封装一个统一入参,像以下这样,跟统一返回的作用是一样的

@Data
public class PageParam<T> implements Serializable {

    private static final long serialVersionUID = -7248374800878487522L;
    /**
     * <p>当前页</p>
     */
    private int pageNum = 1;
    /**
     * <p>每页记录数</p>
     */
    private int pageSize = 10;
    /**
     * <p>分页外的其它参数</p>
     */
    private T param;

字段加解密

字段的加解密一般分为三层

  • 接口层的数据加解密
  • 数据库层的字段加解密,这类加解密一般是身份证、手机号、银行卡子类的特殊字段,这一层我们可以采用的shading-sphere encrypt插件的方式处理,代理数据源,拦截preparment处理,0入侵,也可以使用mybatis的typeHandler处理,相对业务层有入侵
  • 日志打印的字段加解密

当然一般的系统这些内容可以不考虑,其实大部分系统对这个没什么要求,但是如果你需要申请一些资质的时候这个是必须的,或者从安全性角度出发,当你被托库了或者日志被泄漏了,此时加密将是你数据的最后一层保障。有很多大公司发生过此类事件,比如美团,当前的博客,更多的例子就不说了。

缓存

key的序列化

如果你使用redisTemplate设置缓存数据时,由于是对象,此时key也会被序列化层二进制,此时如果出现问题,你排查问题,需要对应的一个缓存管理界面,通过java接口反序列化,才能找到对应的key和数据,对于一些小公司而言非常不友好,因为一般公司没有时间给你去开发这样的一些功能,所以一般对序列化进行一些简单的配置是小公司的首选,将key序列化层字符串。

哪些数据进行缓存

  • 一些静态不怎么变化的数据 比如一些字典类的
  • 业务上的临时数据 比如短信验证码5分钟有效
  • 一些热数据 为了性能考虑做的一些优化

消息队列

除了老生常谈的一些问题,比如消息丢失的处理,业务特定场景之类的我们还要考虑一些规范性的东西

key的规范

大部分的框架对key的定义并没有做什么特别的处理,但是正常情况下我们一般会对key做一些特殊定义,比如这个消息是由哪个服务发送到哪个服务的,可以把这些信息标注到key上,排查问题时,就不用导出找代码,当然你也可以做一张对照表,排查问题的时候可以取对照表进行对照,但是维护一段时间之后就会出现断层,除非你能让开发按照你的标准坚定不移的执行下去。

队列的管理

分为两类

  • 框架层作为消费者的消息
  • 业务层间交互的消息

队列的创建一般由框架层处理,提供一个单独的包给对应的业务,这个包包含消息的发送,消息的消费者由开发自己处理。

注册中心

这些老生常谈的东西,就不多说了。

配置中心

日志配置的热更新

在spring-boot的体系下提供了一个LogSystem抽象,所有的日志框架都实现了该抽象类,所以可以基于这个抽象类我们在运行时进行刷新日志级别。

public class LoggingSystemConfiguration {
    private static final Logger log = LoggerFactory.getLogger(LoggerConfiguration.class);
    private static final String LOGGER_TAG = "logging.level.";

    @Resource
    private LoggingSystem loggingSystem;

    @ApolloConfig
    private Config config;

    @ApolloConfigChangeListener(interestedKeyPrefixes = "logging")
    private void configChangeListener(ConfigChangeEvent changeEvent) {
        log.info("logger configure refresh");
        refreshLoggingLevels();
    }


    private void refreshLoggingLevels() {
        Set<String> keyNames = config.getPropertyNames();
        for (String key : keyNames) {
            if (StringUtils.containsIgnoreCase(key, LOGGER_TAG)) {
                String strLevel = config.getProperty(key, "info");
                LogLevel level = LogLevel.valueOf(strLevel.toUpperCase());

                loggingSystem.setLogLevel(key.replace(LOGGER_TAG, ""), level);
                log.info("{}:{}", key, strLevel);
            }
        }
    }
}

普通配置的热更新

此处我们以apollo为例,普通配置的热更新我们需要@ApolloConfigChangeListener进行配置更新的监听然后刷新配置。

@Configuration
@ConditionalOnClass({ApolloAutoConfiguration.class,RefreshScope.class})
public class ApolloReFreshAutoConfig implements ApplicationContextAware {
    private final static Logger log=LoggerFactory.getLogger(ApolloReFreshAutoConfig.class);
    
    ApplicationContext applicationContext;
    @Resource
    RefreshScope refreshScope;

    @Bean
    public LoggingSystemConfiguration loggingSystemConfiguration() {
        return new LoggingSystemConfiguration();
    }

    public ApolloReFreshAutoConfig(final RefreshScope refreshScope) {
        this.refreshScope = refreshScope;
    }

    @ApolloConfigChangeListener({"application","bootstrap.yml","common.properties"})
    private void refresh(ConfigChangeEvent changeEvent) {
        log.info("Apollo config refresh start");
        this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
        refreshScope.refreshAll();
        log.info("Apollo config refresh end");

    }

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

公共配置的抽取

正常情况下我们一个项目可能共用一些公共组件,比如redis,mq在这种情况下我们当然不希望这些配置每个项目都写一遍,所以我们需要抽取一个公共配置,将这些配置的公共配置都放在公共配置中。
在这里插入图片描述

网关

统一鉴权

网关统一鉴权的数据来源正常我们可以分为两种方式

  • 通过服务接口获取
  • 直接通过缓存,用户服务在登入成功后将数据放入缓存,网关直接取缓存。

至于jwt这个东西真的没什么必要,既然都用缓存了,jwt在其中根本没有丝毫作用。

负载均衡

一般小企业的系统轮询就够了。正常就能满足大部分小公司的需求,日单量10万以内的系统正常不需要什么特别的配置。所以一般负载均衡的策略上更多我们会有其它的一些架构上的作用,比如无损发布等等这些处理上。
在这里插入图片描述

限流降级

这些方案也是老生常谈,不多说。
限流
在这里插入图片描述
降级
这个一般设置个开关,分成两种处理方式,一种手动关闭一些分支业务,一种自动处理,当错误数量达到阀值时自动关闭某些服务即可。

日志的收集

日志收集我们正常两种方式

普通日志处理

  • 落盘之后通过诸如filebeat之类的程序去抽取集中到某个地方,如es
  • 直接通过kafkaappender将日志打到网络端口去

审计\操作日志处理

这类日志我们一般需要存成特定的数据结构,可以通过mq统一收集之后单独由一个消费者进行处理成结构化的数据,方便业务上使用。

文档管理

这个我们正常选择swagger,这个无需我多说了。

定时任务

  • 中心化的xxl-job
  • 去中心化的elastic-job
  • quatz

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

34.Isaac教程--操作示例应用程序

操作示例应用程序 ISAAC教程合集地址文章目录操作示例应用程序与 Jupyter Notebook 的简单联合控制Shuffle Box with Simulator与 Jupyter Notebook 的简单联合控制 此示例使用 Jupyter Notebook 提供交互式联合控制。 这是处理用于操作组件&#xff08;包括 LQR 规划器&#…

PowerShell 执行策略

在使用 SAPIEN 的PowerShell Studio时出现如下错误&#xff1a;无法在当前系统上运行该脚本。有关运行脚本和设置执行策略的详细信息&#xff0c;请参阅 https:/go.microsoft.com/fwlink/?LinkID135170 中的 about_Execution_Policies。 ERROR: 所在位置 行:1 字符: 2 ERROR: …

python基础——函数编程

python基础——函数编程 文章目录python基础——函数编程一、实验目的二、实验原理三、实验环境四、实验内容五、实验步骤一、实验目的 掌握函数编程 二、实验原理 在Python中&#xff0c;定义函数的语法如下&#xff1a; def 函数名([参数列表])&#xff1a; ‘’‘注解’…

【人人都是算法专家】一文搞定AI算法竞赛(全网最详细)

Rocky Ding公众号&#xff1a;WeThinkIn写在前面 【人人都是算法专家】栏目专注于分享Rocky在AI行业中业务/竞赛/研究/产品维度的思考与感悟。欢迎大家一起交流学习&#x1f4aa; 大家好&#xff0c;我是Rocky。 之前Rocky总结过很多关于AI算法竞赛的方法论、经验思考以及细节…

分享123个ASP源码,总有一款适合您

ASP源码 分享123个ASP源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c; 123个ASP源码下载链接&#xff1a;https://pan.baidu.com/s/17G9rpRpCJX_D-6DV0j4uFg?pwd3rx8 提取码&#x…

qt json tree 读取json树状结构并显示

1.介绍 用qt的相关的几个类处理json格式的信息 json内容&#xff1a; { "root":{"sites": [{ "name":"菜鸟教程" , "url":"www.runoob.com" }, { "name":"google" , "url":&quo…

QT环境的搭建安装:VsCode及Qt Creator

前言&#xff1a; vscode的界面美观&#xff0c;并且和QT一样的跨平台&#xff0c;所以可以选择vscode作为开发环境。   QT5.9.X版本成熟&#xff0c;相应教程丰富&#xff0c;并且5.9.9版本具有生成CMake功能&#xff0c;所以本文以QT5.9.9版本为例&#xff0c;给出QT环境的…

美团出品 | YOLOv6 v3.0 is coming(性能超越YOLOv7、v8)

&#x1f680;&#x1f680;&#x1f680;美团出品 | YOLOv6 v3.0 is coming &#xff01;&#xff01;✨✨✨ 一、前言简介 &#x1f384;&#x1f388; &#x1f4da; 代码地址&#xff1a;美团出品 | YOLOv6 3.0代码下载地址 &#x1f4da; 文章地址&#xff1a;https://a…

仿写Dubbo-Java反射

概念 反射是Java的一个特性&#xff0c;反射允许程序运行时动态获取类的所有信息以及对其进行操作。反射在框架(spring&#xff0c;springboot&#xff0c;mybatis等)中的使用非常的广发&#xff0c;可谓是框架的灵魂。 获取Class对象 在使用反射之前&#xff0c;需要获取到Cla…

工欲善其事必先利其器——Elasticsearch安装

安装使用说明 首先在elasticsearch官网下载你想要安装的版本&#xff0c;我这里使用的是7.12.1版本 上传到你想要安装的目录&#xff08;/user/search/&#xff09; 然后解压&#xff0c;解压命令如下&#xff1a; tar -zxvf elasticsearch-7.12.1-linux-x86_64.tar.gz 解压后…

创建保存字符串的数组numpy.char以及用于处理字符串数组的函数

【小白从小学Python、C、Java】【计算机等级考试500强双证书】【Python-数据分析】创建字符串数组numpy.char.array()修改字符串数组大小写capitalize()&#xff1b;title()&#xff1b;lower()&#xff1b;upper()[太阳]选择题对于以下python代码title()方法输出的结果是?imp…

【并发编程十二】c++20线程同步——信号量(semaphore)

【并发编程十二】c20线程同步——信号量&#xff08;semaphore&#xff09;一、互斥二、条件变量三、future四、信号量1、信号量原理2、c 20 信号量3、demo简介&#xff1a; 本篇文章&#xff0c;是线程同步的最后一篇。我们详细的介绍下c标准库提供的线程同步方法——信号量&a…

嵌入式Linux-线程创建与终止

1. 线程的创建 1.1 创建线程 启动程序时&#xff0c;创建的进程只是一个单线程的进程&#xff0c;称之为初始线程或主线程&#xff0c;本小节我们讨论如何创建一个新的线程。 创建线程与创建进程的方法是一样的&#xff0c;让我们来看一下创建线程的函数&#xff1a; #incl…

Linux基本功系列之chown命令实战

文章目录一. 前言&#x1f680;&#x1f680;&#x1f680;二. chown命令介绍三. 语法格式及常用选项四. 参考案例3.1 改变指定文件的属组和属主3.2 改变指定文件的所属主与所属组&#xff0c;并显示过程3.3 改变指定目录及其内所有子文件的所属主与所属组3.4 只修改文件所属组…

P问题、NP问题、NP-Complete问题、NP-Hard问题分别代表什么含义?

绪论 在了解P、NP、NP-Complete、NP-Hard问题之前,先感性地感受一下这几个问题之间的区别和联系👇: 上图分为左右两个版本,推荐记住左边的比较通用。这是因为NP=P这个数学问题曾经被列为7大数学难题之一,而且是之首,甚至美国还悬赏100W美金,但是比较认可的结果是暂时…

Python数据可视化之条形图和热力图

Python数据可视化之条形图和热力图 提示&#xff1a;介绍 简单介绍Pthon可视化的图表使用 提示&#xff1a;热力图和条形图 文章目录Python数据可视化之条形图和热力图前言一、导入数据包二、选择数据集2.加载数据2.读入数据总结前言 提示&#xff1a;这里可以添加本文要记录的…

Acwing 1010. 拦截导弹

Acwing 1010. 拦截导弹一、问题描述二、算法分析三、代码实现一、问题描述 二、算法分析 这道题共分为两问&#xff0c;我们先看第一问。 该问的背后是一个很经典的最长单调子序列模型。 在这个模型中&#xff0c;我们的状态f[i]f[i]f[i]的定义是&#xff0c;以第iii个元素为结…

Cert Manager 申请SSL证书流程及相关概念-三

中英文对照表 英文英文 - K8S CRD中文备注certificatesCertificate证书certificates.cert-manager.io/v1certificate issuersIssuer证书颁发者issuers.cert-manager.ioClusterIssuer集群证书颁发者clusterissuers.cert-manager.iocertificate requestCertificateRequest证书申…

50个你离不开的 CLI 工具

作为开发人员&#xff0c;我们在终端上花费了大量时间。有很多有用的 CLI 工具&#xff0c;它们可以让您在命令行中的生活更轻松、更快速&#xff0c;而且通常更有趣。这篇文章概述了我最依赖的 50 个必备 CLI 工具。如果我遗漏了什么 - 请在评论中告诉我 :)在本文的结尾&#…

二叉树专题汇总

二叉树的前中后序遍历day11|144.二叉树的前序遍历、145.二叉树的后序遍历、94.二叉树的中序遍历_奈川直子的博客-CSDN博客二叉树的层序遍历、翻转二叉树、对称二叉树day12|层序遍历合集、226.翻转二叉树、101.对称二叉树_奈川直子的博客-CSDN博客N叉树最大深度、完全二叉树节点…