【面试精讲】如何保证接口的幂等性?常见的实现方案有哪些?

【面试精讲】如何保证接口的幂等性?常见的实现方案有哪些?

目录


一、什么是幂等性 & RPC调用 & HTTP调用

1、幂等性

在计算机科学中,幂等性是一个重要的概念,尤其在分布式系统和网络通信中。幂等性指的是执行某个操作一次与多次具有同样的效果。这意味着无论对同一个操作请求进行几次调用,结果都应当相同,不会因为多次执行而产生不同的影响。

例如,在数学中,绝对值函数就是幂等的,因为| |x| | = |x|;而在Web服务中,一个典型的例子是HTTP的GET请求,理想情况下无论请求多少次,服务器的资源状态都不改变。

幂等性在系统设计过程中极其重要,特别是在处理重复请求、错误恢复和系统状态同步时,可确保系统的一致性和稳定性。在面对网络不稳定、服务调用失败重试等情况时,幂等性的接口可以避免数据的重复修改,使得系统行为更加可预测。

2、RPC调用

RPC(Remote Procedure Call)即远程过程调用,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC使得开发包括网络分布式多进程程序在内的应用程序更加容易。

在RPC模型中,客户端和服务器之间通常存在一个透明的通信层。客户端只需像本地方法一样调用远程方法,并传递参数,剩余的工作如网络通信、数据编码等则由RPC框架负责。常见的RPC框架有gRPC、Apache Thrift等。

RPC的调用过程概括为以下几个步骤:

  1. 客户端调用客户端存根(stub)提供的过程,就像调用本地过程一样。
  2. 客户端存根将过程调用打包成消息,然后通过网络发送给服务器。
  3. 服务器端存根接收消息,解包并执行所请求的过程。
  4. 执行结果被服务器存根打包成消息,并通过网络发送回客户端。
  5. 客户端存根接收到结果消息,解包,并返回给客户端调用者。

RPC调用强调的是行为的调用,客户端对于服务提供的行为(即方法)进行远程调用。

3、HTTP调用

HTTP(Hypertext Transfer Protocol)调用是基于HTTP协议的网络请求。它是现代互联网中使用最广泛的协议之一,主要用于Web浏览器和服务器之间的通信,但也经常用于微服务之间的通讯。HTTP的请求-响应模型非常适合无状态的RESTful API设计。

HTTP调用过程如下:

  1. 客户端构建一个HTTP请求,其中包括方法(如GET、POST、PUT、DELETE等)、URL、Header以及可能的消息体(body)。
  2. 请求通过网络发送到服务器端。
  3. 服务器端解析HTTP请求,并根据请求内容进行处理。
  4. 服务器端返回一个HTTP响应,其中包括状态码(如200 OK、404 Not Found等)、响应头和响应体。

与RPC调用不同的是,HTTP调用强调的是资源的状态转换,通过对资源(URI定位)的增删改查(CRUD),达到服务间通信的目的。

4、幂等性在RPC和HTTP调用中的重要性

无论是RPC还是HTTP调用,幂等性都能够防止因网络问题或其他原因导致的重复请求引发数据冲突或状态不一致,这在微服务架构、分布式事务处理、负载均衡等场景中尤其重要。在HTTP中尤其突出,因为HTTP/1.1规范定义了一些方法(如GET、PUT、DELETE)为幂等的,而POST方法通常不是幂等的,因为它代表的是创建新资源的动作。

二、接口幂等性的原理与方案

在系统间通信过程中,尤其是在分布式系统中,接口的幂等性是维护数据一致性和系统稳定性的关键。本部分将首先介绍保证幂等性的原理,随后展开具体的解决方案。

1、保证幂等性的原理

幂等性的核心是确保一个或多个操作的重复执行不会对系统的状态产生额外影响。在分布式系统或者任何需要通过网络通信的系统架构中,由于网络延迟、系统故障、用户重复操作等因素,相同的请求可能会被发送多次。如果系统对每个独立的请求都按照一次新操作来处理,则可能会导致数据不一致或状态错误。

实现幂等性的基本原理包括:

  1. 识别请求:系统需要能够识别出重复的请求。这通常通过在请求中包含一个唯一标识符(如请求ID或事务ID)来实现。
  2. 存储操作状态:系统需记录操作的执行状态(例如已处理、正在处理、未处理),以及关键步骤的结果,确保即使在服务重启或失败后也能恢复和维持操作状态。
  3. 处理重复请求:当系统接收到一个已知的重复请求时,它可以采取适当的行动,如直接返回先前操作的结果,或者忽略重复的请求。

2、接口幂等性的实现方案

实现接口幂等性可以采取多种策略,以下是一些常见的方法:

  1. 利用唯一事务ID:为每个操作请求分配一个唯一的事务ID。服务端检查该ID,如果之前已经处理过,就直接返回原来的处理结果,否则进行处理并存储该ID与结果的关联。
  2. 乐观锁:使用乐观锁可以在更新数据时检查数据版本。每次数据修改时,版本号增加。如果请求中的版本号与服务器上的不匹配,则意味着有其他操作已经修改了数据,当前操作将被拒绝。
  3. Token机制:在操作开始前,客户端请求一个操作令牌(token),并在随后的操作中使用该token。服务器对每个token只允许一次有效的操作,从而防止重复处理。
  4. 指纹机制:根据请求的特征(如参数、时间戳等)生成请求指纹。服务端检查指纹是否已存在,存在则认为是重复请求。
  5. 幂等框架支持:一些现代框架和组件(如消息队列、数据库等)提供了内建的支持来处理幂等性,例如Kafka的Exactly-once语义,或者数据库的unique constraint。

每种方案都有其适用场景和限制。例如,事务ID和Token机制适用于需要严格幂等性保证的场景,而乐观锁和指纹机制可能更适合并发度不是特别高的环境。

3、实际应用场景

  • 电子商务:在订单支付环节,保证支付操作的幂等性可以防止因网络延迟或用户重复点击“支付”按钮导致的多次扣款。
  • 银行系统:转账操作需要幂等性保护,确保同一笔转账指令不会因为重复处理而导致资金异常。
  • 微服务架构:在微服务之间调用时,尤其是在服务自动重试机制存在的情况

三、接口幂等性解决方案的代码实现

在接口幂等性的策略中,我们通常要结合具体的业务场景选择适当的实现方式。这一部分将提供几种常见的幂等性保证方法的代码示例。

1、唯一事务ID

创建一个TransactionalApiController类,有一个process方法,用于处理带有唯一事务ID的请求。

系统通过检查一个全局的ConcurrentHashMap来识别重复的请求,并返回之前存储的响应结果。 

@RestController
@RequestMapping("/api")
public class TransactionalApiController {

    private ConcurrentHashMap<String, Object> requestCache = new ConcurrentHashMap<>();

    @PostMapping("/process")
    public ResponseEntity<Object> process(@RequestBody RequestData requestData) {
        String transactionId = requestData.getTransactionId();
        
        // 检查是否为重复请求
        if (requestCache.containsKey(transactionId)) {
            return ResponseEntity.ok(requestCache.get(transactionId));
        }

        // 处理业务逻辑
        Object result = performBusinessLogic(requestData);

        // 将结果存储和返回,以便后续重复请求可以使用
        requestCache.put(transactionId, result);
        
        return ResponseEntity.ok(result);
    }
    
    private Object performBusinessLogic(RequestData requestData) {
        // 实际业务处理逻辑...
        return new Object(); // 返回处理结果
    }
    
    static class RequestData {
        private String transactionId;
        // 其他业务数据
        
        // 省略getter、setter和构造器
    }
}

2、Token机制(防止重复提交)

客户端首先调用getToken方法获取一个唯一的Token。在提交信息时,客户端需要在HTTP头部携带这个Token,服务器端会验证Token是否有效,如果有效则执行业务逻辑并使该Token失效。 

@RestController
@RequestMapping("/api")
public class TokenApiController {

    private final TokenStore tokenStore;

    public TokenApiController(TokenStore tokenStore) {
        this.tokenStore = tokenStore;
    }

    @GetMapping("/token")
    public ResponseEntity<String> getToken() {
        // 生成并返回一个新的Token
        String token = UUID.randomUUID().toString();
        tokenStore.storeToken(token);
        return ResponseEntity.ok(token);
    }

    @PostMapping("/submit")
    public ResponseEntity<Object> submit(@RequestHeader("X-Request-Token") String token, @RequestBody Data data) {
        if (!tokenStore.checkAndInvalidateToken(token)) {
            throw new DuplicateRequestException("Token has already been used or does not exist.");
        }

        // 执行业务逻辑
        Object result = businessLogic(data);

        return ResponseEntity.ok(result);
    }

    // 在这里实现TokenStore,确保线程安全性和过期管理
}

3、乐观锁

EntityWithVersion实体中,我们使用了@Version注解标记了一个版本字段。在执行更新时,如果检测到版本不一致,则意味着另一个并发操作已经修改了数据,当前操作将抛出异常。

@Entity
public class EntityWithVersion {
    @Id
    private Long id;
    
    @Version
    private Integer version;

    // 其他字段和方法
}

@Repository
public interface EntityWithVersionRepository extends JpaRepository<EntityWithVersion, Long> {
}

@Service
public class OptimisticLockService {
    private final EntityWithVersionRepository repository;

    public OptimisticLockService(EntityWithVersionRepository repository) {
        this.repository = repository;
    }

    public EntityWithVersion updateEntity(Long id, SomeData newData) {
        EntityWithVersion entity = repository.findById(id)
                .orElseThrow(() -> new EntityNotFoundException(id));

        // 更新实体数据
        entity.setSomeField(newData.getSomeField());

        // 如果在这个更新操作的过程中,实体的版本号被其他线程修改,则JPA会抛出OptimisticLockingFailureException
        return repository.save(entity);
    }
}

总结

以上代码示例只是展示了如何在Java中实现幂等性控制的基本思路。实际项目中可能需要根据业务需求与系统架构进行相应的调整和优化。此外,还需考虑存储介质的性能、异常处理、日志记录等因素,以确保系统的稳定运行和良好的用户体验。

欢迎评论区留言讨论,如果本文对你有帮助 欢迎 关注 、点赞 、收藏 、评论!!!

 博主v:XiaoMing_Java

  📫作者简介:嗨,大家好,我是     小 明

互联网大厂后端研发专家,2022博客之星TOP3 / 博客专家 / CSDN后端内容合伙人、InfoQ(极客时间)签约作者、阿里云签约博主、全网 10 万粉丝博主。

🍅 文末获取联系 🍅  👇🏻 精彩专栏推荐订阅收藏 👇🏻

 

专栏系列(点击解锁)

学习路线(点击解锁)

知识定位

🔥Redis从入门到精通与实战🔥

Redis从入门到精通与实战

围绕原理源码讲解Redis面试知识点与实战

🔥MySQL从入门到精通🔥

MySQL从入门到精通

全面讲解MySQL知识与企业级MySQL实战

🔥计算机底层原理🔥

深入理解计算机系统CSAPP

以深入理解计算机系统为基石,构件计算机体系和计算机思维

Linux内核源码解析

围绕Linux内核讲解计算机底层原理与并发

🔥数据结构与企业题库精讲🔥

数据结构与企业题库精讲

结合工作经验深入浅出,适合各层次,笔试面试算法题精讲

🔥互联网架构分析与实战🔥

企业系统架构分析实践与落地

行业最前沿视角,专注于技术架构升级路线、架构实践

互联网企业防资损实践

互联网金融公司的防资损方法论、代码与实践

🔥Java全栈白宝书🔥

精通Java8与函数式编程

本专栏以实战为基础,逐步深入Java8以及未来的编程模式

深入理解JVM

详细介绍内存区域、字节码、方法底层,类加载和GC等知识

深入理解高并发编程

深入Liunx内核、汇编、C++全方位理解并发编程

Spring源码分析

Spring核心七IOC/AOP等源码分析

MyBatis源码分析

MyBatis核心源码分析

Java核心技术

只讲Java核心技术

版权声明:本文为博主作者:小 明原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/FMC_WBL/article/details/137758287

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
社会演员多的头像社会演员多普通用户
上一篇 2024年4月22日
下一篇 2024年4月22日

相关推荐