【微服务】接口幂等性常用解决方案

news2024/9/20 14:25:18

一、前言

在微服务开发中,接口幂等性问题是一个常见却容易被忽视的问题,同时对于微服务架构设计来讲,好的幂等性设计方案可以让程序更好的应对一些高并发场景下的数据一致性问题。

二、幂等性介绍

2.1 什么是幂等性

通常我们说的幂等性,大多数情况下指的是服务端的接口幂等性。所以接口幂等性指的是,用户对于同一个操作发起的一次请求,或者多次请求结果是一致的。

举例来说:一个用户在手机APP提200块钱,一不小心点击了两次,理论上应该只取出200块钱,而不应该出来400(当然,真实场景下取钱操作是一个复杂事务,不可能一个接口点击就出来了)。在这种场景下,即使用户点了两次也应该只取出一次的钱,这就是接口幂等性。

2.2 幂等性问题产生原因

什么情况下会出现接口幂等性问题呢,通常来说主要有下面几个原因:

  • 用户重复提交,当客户端发起多次相同的请求时,服务器未能正确处理重复请求,导致产生幂等性问题。

    • 一般指用户填写好表单信息后,由于服请求响应较慢,从而多次点击提交按钮。

  • 非法调用,比如像第三方,通过逆向手段调试到了接口地址,然后通过爬虫或接口工具多次调用。

  • 并发操作

    • 在高并发环境下,可能出现多个请求同时到达服务器,如果服务器未能正确处理并发请求,可能会导致幂等性问题的产生。

  • 事务处理不当

    • 如果接口需要进行事务处理,并且没有正确地管理事务的提交和回滚,也可能导致幂等性问题的产生。

  • 失败重试

    • 在分布式项目中,被调用方出现超时或异常时,触发了调用方的重试补偿机制。

  • 重复消息

    • 通常是指引入MQ的项目中,对于同一个消息,生产者多次发送,或消费者重复消费。

2.3 为什么需要接口幂等

接口幂等性对于系统设计和开发具有重要意义,尤其是在电商、金融、交易等数据一致性要求比较严苛的场景下,幂等性的保障就显得格外重要。具体来说,幂等性的作用主要如下。

2.3.1 减少重复操作的影响

在网络通信中,可能由于各种原因导致请求的重复发送,如果接口是幂等的,即使接收到了重复的请求,系统也可以保持一致的状态,避免产生额外的副作用。

比如服务A调用服务B接口进行转账,假设A调用B时超时了,一般来说,超时的原因可能是网络传输丢包,也可能是处理请求的服务还没有接收到请求,或者接收到请求了但是还未来得及处理,或者请求处理了但是在结果返回的途中丢了。如果此时A进行重试的话,师傅会发生多笔转账呢?所以在这种情况下,如果下游的B服务接口如果没有做好幂等性保障的话,就会出现很严重的问题。

2.3.2 提高系统可靠性

当系统中接口具有幂等性时,即使出现异常情况或故障,系统也可以更容易恢复到正常状态,从而降低系统崩溃的风险。

举例来说,在mysql表中设计了基于version的字段,在每次对表的数据进行update时,为了保障接口幂等性,可以基于version先查,然后更新,在这种方式下,即便应用程序意外宕机或故障,也可以方便的根据version值进行回溯,快速恢复之前的数据。

2.3.3 简化客户端调用逻辑

对于客户端来说,无需关心接口的幂等性,只需按照业务需求发送请求,降低了客户端的复杂度和错误率。

当服务A调用服务B的时候,对于服务A来说就是客户端,并不打算因为调用B失败而特意做其他的业务处理,在这种情况下就需要B服务,即被调用方做好接口的幂等性处理,从而A在调用时难度和复杂性就降低了。

2.3.4 便于系统扩展和集成

当接口具有幂等性时,系统可以更容易地进行横向扩展和集成,不必担心多次请求会破坏系统状态。

三、接口幂等性与防重复提交

3.1 接口幂等性与防重复提交概念

这是在实际应用中很多同学混淆的概念,但是两者都是在开发中服务端需要解决的问题,其实来说接口幂等性和防重复提交是两个不同但相关的概念。具体来说:

  • 接口幂等性是指一个接口的多次重复调用所产生的影响与一次调用的影响相同。换句话说,无论某个接口被调用多少次,其结果都是一致的。这样设计的接口可以更容易处理各种问题,比如网络超时、断网重试等情况,而不会导致数据错误或状态混乱。

  • 防重复提交是指在用户提交数据或请求时,系统需要确保同样的数据或请求不会被重复处理多次。这通常涉及到在前端或后端做一些措施,比如生成唯一标识、使用Token或者设置时间间隔来避免用户多次提交同样的请求。

在实际应用中,接口幂等性和防重提交一般会结合起来使用,以确保系统稳定性和数据准确性。接口设计时考虑到幂等性可以简化系统的逻辑处理,而防重复提交则是为了提升用户体验和数据的完整性。

3.2 接口幂等性与防重复提交异同点

相同点:

  • 都与接口或请求的重复操作有关;

  • 都涉及到处理系统中重复请求可能带来的问题,如数据不一致、资源浪费等;

不同点:

  • 接口幂等性是指一个接口多次调用所产生的影响与一次调用的影响相同。接口本身具有幂等性的特点,无论调用多少次,结果都是一致的。

  • 重复提交指用户或系统在短时间内多次提交同一个请求或数据。重复提交通常发生在用户界面上,可能导致数据重复处理或资源浪费。

四、接口幂等性解决方案

4.1 使用核心业务字段唯一性约束

在很多业务场景中,业务表都会设置某个字段的唯一性约束,从而确定数据的唯一性,通常来说可以添加字段的唯一索引。比如订单表的订单号,用户表用户编码等。如果相同的请求再次发送过来,由于字段的唯一性约束,将会触异常而被捕获。如下是一段伪代码:

S       tring orderId = "puk-3309-A";
        Order order = new Order();
        order.setOrderId(orderId);
        try {
            orderDao.save(order);
        }catch (Exception e){
            //唯一性约束异常
            return "订单已创建";
        }

完整流程如下:

4.1.1 唯一约束方案优缺点

优点:

  • 使用简单,代码集成难度较低;

  • 可靠性好;

缺点:

  • 基于数据库自身的特性,不够优雅;

  • 在应对较大的并发时,具有一定的局限性;

4.2 使用乐观锁解决幂等性问题

乐观锁的使用场景非常多,是一种很好的用于解决并发冲突,幂等性问题的方案,在mysql中,乐观锁可以避免对行数据加锁从而提升系统的并发性能。通常依赖于数据版本号,或时间戳等字段进行控制,适用于需要对数据变更进行版本管理的场景。

如下有一张表,表中有一个version的字段

CREATE TABLE `seek_order` (
  `id` int(11) NOT NULL,
  `amount` int(12) DEFAULT NULL,
  `version` int(12) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

利用乐观锁的方式怎么解决接口的幂等性问题呢?请参考如下的操作流程。

具体来说,完整步骤如下:

  • 查询数据

    • select id,amount,version from seek_order where id=1;

  • 更新数据

    • update seek_order set amount = amount+10 ,version=version+1 where id=1 and version=1;

  • 判断更新的行数

    • 大于0,说明本次更新成功,如果等于0,说明本次没有对数据进行变更;

  • 本次操作数据的同时,还修改了数据的版本号,如果此时并发请求过来,再次执行相同的sql时候,update 并不会真正更新数据,从而update的执行影响行数为0,因为上一个update完成之后,数据的version已经变成2了,所以version=1肯定无法满足条件了;

  • 为了保证接口幂等性,接口可以直接返回客户端本次处理成功,因为version已经修改了,所以签名的请求一定成功过一次,后面都是重复请求;

与之类似的还有状态机字段,比如处理订单的时候,通常订单表中会有一个订单处理的状态,比如0代表创建,1代表待支付,2为已支付...,相同的update请求过来时需要带上status,如果本次update的影响行数为0,说明之前已经有更新成功了,如果影响行数为1,说明本次修改成功。

4.3 分布式锁解决幂等性问题

上面前两种解决方案中,其实都是利用了数据库的分布式锁机制,但是在实际开发中,一般并不太推荐使用,一是并发性能有局限,同时在捕获的异常中进行处理起来不够优雅,所以如果基于锁的特性来解决,可以采用分布式锁的方案来解决解决幂等性问题。

分布式锁有很多选择,像主流的基于redis的分布式锁解决方案,基于zookeeper的分布式锁解决方案等

如果以redis为例进行说明,在解决接口幂等性问题时,以生成订单的场景说明,可以参考如下流程

具体来说,操作步骤如下:

  • 生成唯一订单code作为唯一业务字段;

  • 使用redis的分布式锁,利用code作为key,同时需要设置key的超时时间;

  • 判断是否能设置成功,如果能设置成功,说明是第一次请求,则进入核心逻辑处理;

  • 如果设置失败,说明是重复请求,直接返回成功即可;

注意,分布式锁一定需要设置一个合理的超时时间,设置过短,无法合理的防止重复请求,设置太长,则会浪费redis的存储空间;

4.4 预置令牌解决幂等性问题

预置令牌,即token方案,简单来说,实现步骤如下:

  • 客户端发起业务操作前,先请求服务端颁发token,服务端生成一个token返回给客户端;

  • 服务端存储token到redis中,并设置过期时间;

  • 客户端发起操作请求,比如创建订单请求,携带token;

  • 服务端接收请求,并查询redis中是否存在token;

    • 如果不存在,则执行业务操作;

    • 否则,可认为是重复请求直接返回;

    • 业务逻辑操作完成后需要删除token;

4.4 本地消息事件表

在微服务场景中,经常利用MQ对微服务进行解耦,在使用MQ过程中,一个容易出现的问题就是,消息的重复发送,或消息的重复消费,在消息消费端,如果没有对消息做幂等性处理的话,可能会引发数据不一致问题。针对这种幂等性问题的场景,可以考虑采用本地消息事件表来解决。

具体操作流程如下:

  • 生成者发送消息到MQ,消息具备唯一的标识这里记为messageId;

  • 消费者第一次接收到消息并消费,将业务处理结果入库,同时记录业务唯一标识与messageId,表示处理过;

  • 如果因为某种原因,发生重复消费,先拿着业务ID或messageId去映射表中查询;

    • 如果已经存在,说明已经处理过,是重复请求;

五、代码实现

接下来演示基于token的接口幂等性方案在代码中的实现过程。

5.1 工程搭建

5.1.1 创建一张表

CREATE TABLE `seek_order` (
  `id` int(11) NOT NULL,
  `amount` int(12) DEFAULT NULL,
  `version` int(12) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

5.1.2 创建maven工程

工程目录如下

5.1.3 导入maven依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.17</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

    </dependencies>

5.1.4 添加配置文件

server:
  port: 8088

spring:
  application:
    name: client-service

  datasource:
    url: jdbc:mysql://IP:3306/数据库名
    driverClassName: com.mysql.jdbc.Driver
    username: root
    password: root

  redis:
    host: 127.0.0.1
    port: 6379

mybatis:
  mapper-locations: classpath:mapper/*.xml
  #目的是为了省略resultType里的代码量
  type-aliases-package: com.congge.entity
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

5.2 代码实现过程

5.2.1 获取token接口

一般来说,token需要与当前登录人信息进行关联,这里简单起见,使用客户端请求IP为唯一标识,同时,在设置redis的key时候带上过期时间。

public String getToken(HttpServletRequest request) {
        String ipAddr = IpUtil.getIpAddr(request);
        String token = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set(ipAddr,token, 1,TimeUnit.MINUTES);
        return token;
}

5.2.2 执行业务逻辑

按照上述的流程,客户端需要先获取token,然后在真正执行业务逻辑时携带token;

    @Transactional
    public String createOrder(HttpServletRequest request) {
        String token = request.getHeader("token");
        String ipAddr = IpUtil.getIpAddr(request);
        String redisToken = redisTemplate.opsForValue().get(ipAddr);
        if(StringUtils.isEmpty(redisToken)){
            throw new RuntimeException("重复请求");
        }
        if(!redisToken.equals(token)){
            throw new RuntimeException("无效token");
        }
        SeekOrder seekOrder = new SeekOrder();
        int maxId = seekOrderDao.getMaxId();
        seekOrder.setId(maxId+1);
        seekOrder.setAmount(11);
        seekOrder.setVersion(1);
        seekOrderDao.saveOrder(seekOrder);
        //执行成功删除token
        redisTemplate.delete(ipAddr);
        return "订单创建成功";
    }

5.3 功能测试

5.3.1 客户端获取token

调用获取token接口,http://localhost:8088/token

5.3.2 创建订单

在获取到token之后,接下来执行创建订单接口,需要把上一步的token带入到请求header中

再次执行创建订单接口,由于约定了请求必须携带token,第一次创建完成之后,删除了token,所以再次创建订单时候抛异常,也可以直接返回创建成功。

5.4 优化改进

可以看到,在上面创建订单的处理逻辑中,对请求是否重复的判断是比较冗余的,或者说放到创建订单的主流程中不是很优雅,但这种处理又是必须要的,于是可以考虑在某个地方统一处理,这里我们定义一个过滤器,在过滤器中进行处理。

5.4.1 自定义拦截器

package com.congge.filter;

import com.congge.service.IpUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebFilter(urlPatterns = "/*",filterName = "orderCheckFilter")
public class OrderCheckFilter implements Filter {

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        System.out.println(request.getRequestURI());
        if(!"/create".equals(request.getRequestURI())){
            filterChain.doFilter(servletRequest,servletResponse);
            return;
        }
        //只拦截创建订单的请求
        String token = request.getHeader("token");
        String ipAddr = IpUtil.getIpAddr(request);
        String redisToken = redisTemplate.opsForValue().get(ipAddr);
        if(StringUtils.isEmpty(redisToken)){
            throw new RuntimeException("重复请求");
        }
        if(!redisToken.equals(token)){
            throw new RuntimeException("无效token");
        }
        return;
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("orderCheckFilter init");
    }

    @Override
    public void destroy() {
        System.out.println("orderCheckFilter destroy");
    }

}

5.4.2 创建订单逻辑改造

将原本token处理的那一段逻辑移除

    @Transactional
    public String createOrder(HttpServletRequest request) {
        String ipAddr = IpUtil.getIpAddr(request);
        SeekOrder seekOrder = new SeekOrder();
        int maxId = seekOrderDao.getMaxId();
        seekOrder.setId(maxId+1);
        seekOrder.setAmount(11);
        seekOrder.setVersion(1);
        seekOrderDao.saveOrder(seekOrder);
        //执行成功删除token
        redisTemplate.delete(ipAddr);
        return "订单创建成功";
    }

5.4.3 启动类添加注解

启动类别忘了添加下面的注解

@SpringBootApplication
@MapperScan(basePackages = {"com.congge.mapper"})
@ServletComponentScan("com.congge.filter")
public class SeekOrderApp {

    public static void main(String[] args) {
        SpringApplication.run(SeekOrderApp.class,args);
    }
}

5.4.4 功能测试验证

按照上面的步骤再次测试,当第二次相同的创建订单请求过来时,将会抛出异常

六、写在文末

本文详细介绍了在微服务开发场景中幂等性问题的解决方案,并结合一个实际场景给出了代码案例,幂等性问题的处理在实际开发中是一个不可忽视的问题,尤其是对于数据的一致性要求比较高的场景,有兴趣的同学可以基于本文提到的其他方案继续深入探究,本篇到此结束感谢观看。

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

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

相关文章

leetcode 714

leetcode 714 题目 例子 思路1 使用dp[n][2] 存储最佳利润值&#xff0c;动态规划的思路&#xff0c;重要的是转移方程。 代码1 class Solution { public: int maxProfit(vector& prices, int fee) { int n prices.size(); //dp[i][0] 前i天手里没有股票的最大利润 //…

Share-ChatGPT官网UI/文件上传/联网搜索/GPTS 一并同步

地址&#xff1a;Share-ChatGPT 文章目录 界面UI&#xff0c;GPTS&#xff0c;读论文&#xff0c;数据分析&#xff0c;写论文视频演示仓库地址 界面 支持多账号同时管理&#xff0c;合理利用资源&#xff1a; UI&#xff0c;GPTS&#xff0c;读论文&#xff0c;数据分析&a…

老家稳定月薪3000工作和互联网企业3万怎么选?

这是发生在身上的真事。其实刚回老家心里落差非常的大&#xff0c;老家也不需要信息安全非常专业的人才&#xff0c;最终因为各种综合原因选择了回老家&#xff0c;有父母的因素&#xff0c;有4年2次被优化的因素&#xff0c;有互联网行业35岁的因素等等。 我回老家发展很多人…

C语言(结构体,联合体,枚举的讲解)

这期我们来讲解结构体&#xff0c;联合体&#xff0c;以及枚举的讲解&#xff0c;首先我们从概念开始一步一步的了解。 1&#xff0c;结构体 1.1概念 C 语言中的结构体是一种用户自定义的数据类型&#xff0c;它允许你将不同类型的变量组合在一起&#xff0c;从而形成一个新…

集合(下)Map集合的使用

文章目录 前言一、Map接口二、Map接口的实现类 1.HashMap类2.TreeMap类总结 前言 Map集合没有继承Collection接口&#xff0c;不能像List集合和Set集合那样直接使用Collection接口的方法。Map集合其自身通过以key到value的映射关系实现的集合&#xff0c;也有相应的许多方法。类…

零基础机器学习(3)之机器学习的一般过程

文章目录 一、机器学习一般过程1.数据获取2.特征提取3.数据预处理①去除唯一属性②缺失值处理A. 均值插补法B. 同类均值插补法 ③重复值处理④异常值⑤数据定量化 4.数据标准化①min-max标准化&#xff08;归一化&#xff09;②z-score标准化&#xff08;规范化&#xff09; 5.…

[蓝桥杯 2023 省 A] 颜色平衡树:从零开始理解树上莫队 一颗颜色平衡树引发的惨案

十四是一名生物工程的学生&#xff0c;他已经7年没碰过信息学竞赛了&#xff0c;有一天他走在蓝桥上看见了一颗漂亮的颜色平衡树&#xff1a; [蓝桥杯 2023 省 A] 填空问题 - 洛谷 十四想用暴力解决问题&#xff0c;他想枚举每个节点&#xff0c;每个节点代表一棵树&#xff0…

风险评估在网络安全领域的应用与实践

一、引言 在数字化浪潮席卷全球的今天&#xff0c;网络安全已成为企业运营和发展的核心问题。随着信息技术的快速发展&#xff0c;企业面临着日益复杂的网络安全威胁&#xff0c;如黑客攻击、数据泄露、恶意软件等。这些威胁不仅可能导致企业重要信息的丢失或泄露&#xff0c;…

C++模版(基础)

目录 C泛型编程思想 C模版 模版介绍 模版使用 函数模版 函数模版基础语法 函数模版原理 函数模版实例化 模版参数匹配规则 类模版 类模版基础语法 C泛型编程思想 泛型编程&#xff1a;编写与类型无关的通用代码&#xff0c;是代码复用的一种手段。 模板是泛型编程…

Retelling|Facebook2

录音 Facebook 2 Retelling|Facebook2 复述转写 Hi, Im Helen Campbell, from DJ interpretation, European Commission, Im going to talk about Facebook. You Im sure that you are more familiar with Facebook, a lot, a lot more familiar than I than me. But Ive read…

JavaSE系统性总结全集(精华版)

目录 1. 面向对象&#xff08;封装&#xff0c;继承&#xff0c;多态&#xff09;详解 1.1 面向过程和面向对象的区别 1.2面向对象的三大特性 1.2.1 封装 1.2.2 继承 1.2.3 多态 1.2.4 方法重写和方法重载的区别&#xff08;面试题&#xff09; 1.2.5 访问权限修饰符分…

clickhouse学习笔记02(小滴课堂)

ClickHouse核心基础-常见数据类型讲解 插入数据&#xff1a; decimal类型的数据&#xff0c;整数部分超了会报错&#xff0c;小数部分超了会截取。 查看表结构&#xff1a; 查询&#xff1a; 插入&#xff1a; 更新操作&#xff1a; 这个和mysql的语句不太一样。 删除语句和my…

java的ArrayList类

ArrayList<E>E是自定义数据类型 ArrayList类&#xff1a; 构造函数&#xff1a; 成员方法&#xff1a; public boolean add(E e)&#xff1a; 将指定元素加到集合末尾 Appends the specified element to the end of this list. public class Array {public static…

视觉轮速滤波融合1讲:理论推导

视觉轮速滤波融合理论推导 文章目录 视觉轮速滤波融合理论推导1 坐标系2 轮速计2.1 运动学模型2.2 外参 3 状态和协方差矩阵3.1 状态3.2 协方差矩阵 4 Wheel Propagation4.1 连续运动学4.2 离散积分4.2.1 状态均值递推4.2.2 协方差递推 5 Visual update5.1 视觉残差与雅可比5.2…

P1135 奇怪的电梯 (双向bfs)

输入输出样例 输入 5 1 5 3 3 1 2 5输出 3说明/提示 对于 100%100% 的数据&#xff0c;1≤N≤200&#xff0c;1≤A,B≤N&#xff0c;0≤Ki​≤N。 本题共 1616 个测试点&#xff0c;前 1515 个每个测试点 66 分&#xff0c;最后一个测试点 10 分。 重写AC代码&#xff1…

C++_回文串

目录 回文子串 最长回文子串 分割回文串 IV 分割回文串 II 最长回文子序列 让字符串成为回文串的最少插入次数 回文子串 647. 回文子串 思路&#xff0c;i j表示改范围内是否为回文串&#xff0c; ②倒着遍历是为了取出dp[i 1][j - 1] ③i j 只有一对&#xff0c;不会重复…

每天上万简历,录取不到1%!阿里腾讯的 offer 都给了哪些人?

三月天杨柳醉春烟~正是求职好时节~ 与去年秋招的冷淡不同&#xff0c;今年春招市场放宽了许多&#xff0c;不少企业纷纷抛出橄榄枝&#xff0c;各大厂的只差把“缺人”两个字写在脸上了。 字节跳动技术方向开放数10个类型岗位&#xff0c;研发需求占比60%&#xff0c;非研发新增…

【数据结构】双向奔赴的爱恋 --- 双向链表

关注小庄 顿顿解馋๑ᵒᯅᵒ๑ 引言&#xff1a;上回我们讲解了单链表(单向不循环不带头链表)&#xff0c;我们可以发现他是存在一定缺陷的&#xff0c;比如尾删的时候需要遍历一遍链表&#xff0c;这会大大降低我们的性能&#xff0c;再比如对于链表中的一个结点我们是无法直接…

C/C++ 语言中的 ​if...else if...else 语句

C/C 语言中的 ​if...else if...else 语句 1. if statement2. if...else statement3. if...else if...else statementReferences 1. if statement The syntax of the if statement is: if (condition) {// body of if statement }The code inside { } is the body of the if …

《剑指 Offer》专项突破版 - 面试题 93 : 最长斐波那契数列(C++ 实现)

题目链接&#xff1a;最长斐波那契数列 题目&#xff1a; 输入一个没有重复数字的单调递增的数组&#xff0c;数组中至少有 3 个数字&#xff0c;请问数组中最长的斐波那契数列的长度是多少&#xff1f;例如&#xff0c;如果输入的数组是 [1, 2, 3, 4, 5, 6, 7, 8]&#xff0…