JAVA面试题分享五百四十六:什么是接口的幂等性,如何保证接口的幂等性?

目录

什么是接口的幂等性,如何保证接口的幂等性?

1.什么是幂等性

2.什么是接口幂等性

3.应用场景

4.解决方案

4.1 inset之前先select

4.2 数据库唯一索引

4.3 Token机制

4.4 悲观锁机制

4.5 乐观锁机制

4.6 建防重表

5.代码实践


什么是接口的幂等性,如何保证接口的幂等性?

1.什么是幂等性

幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同。在计算机中编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数或幂等方法是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。

2.什么是接口幂等性

所谓接口幂等性,就是一次和多次请求某一个资源对于资源本身应该具有同样的结果。

接口的 「幂等性(Idempotence)」 是指一个操作在执行一次和多次执行时,其结果是一样的。换句话说,无论这个操作被执行多少次,它对系统状态的影响都是相同的。幂等性是分布式系统中的一个重要概念,它有助于确保系统的健壮性和一致性。

在网络通信和 API 设计中,幂等性尤为重要,因为它可以防止由于网络重传、客户端或服务器端的重复请求等问题导致的数据不一致。例如,一个幂等的 HTTP 请求(如 GET 或 PUT 请求)在多次发送时,服务器应该能够正确处理,确保不会对资源造成意外的影响。

3.应用场景

不知道以下场景,朋友们是否遇到过:

  • 「前端重复提交表单:」 在填写一些表格时候,用户填写完成提交,很多时候会因网络波动没有及时对用户做出提交成功响应,致使用户认为没有成功提交,然后一直点提交按钮,这时就会发生重复提交表单请求。

  • 「用户恶意进行刷单:」 例如在实现用户投票这种功能时,如果用户针对一个用户进行重复提交投票,这样会导致接口接收到用户重复提交的投票信息,这样会使投票结果与事实严重不符。

  • 「接口超时重复提交:」 很多时候 HTTP 客户端工具都默认开启超时重试的机制,尤其是第三方调用接口时候,为了防止网络波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。

  • 「消息进行重复消费:」 当使用 MQ 消息中间件时候,如果发生消息中间件出现错误未及时提交消费信息,导致发生重复消费。

没错,这些都是幂等性问题。

接口幂等性是指用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。

这类问题多发于接口的:

  • insert操作,这种情况下多次请求,可能会产生重复数据。

  • update操作,如果只是单纯的更新数据,比如:update user set status=1 where id=1,是没有问题的。如果还有计算,比如:update user set status=status+1 where id=1,这种情况下多次请求,可能会导致数据错误。

4.解决方案

4.1 inset之前先select

通常情况下,在保存数据的接口中,我们为了防止产生重复数据,一般会在insert前,先根据name字段select一下数据。如果该数据已存在,则执行update操作,如果不存在,才执行  insert操作。

4.2 数据库唯一索引

大多数情况下,我们为了防止数据重复提交,我们都会在表中添加唯一索引,这个一个非常简单而且有奇效的方案。

alter table `order` add UNIQUE KEY `t_code` (`code`);

加了唯一索引之后,第一次请求数据可以插入成功。但后面的相同请求,插入数据时会报Duplicate entry '002' for key 'order.t_code异常,表示唯一索引有冲突。

「具体流程图如下:」

4.3 Token机制

针对客户端连续点击或者调用方的超时重试等情况,例如提交订单,此种操作就可以用 Token 的机制实现防止重复提交。简单的说就是调用方在调用接口的时候先向后端请求一个全局 ID(Token),请求的时候携带这个全局 ID 一起请求**(Token 最好将其放到 Headers 中)**,后端需要对这个 Token 作为 Key,用户信息作为 Value 到 Redis 中进行键值内容校验,如果 Key 存在且 Value 匹配就执行删除命令,然后正常执行后面的业务逻辑。如果不存在对应的 Key 或 Value 不匹配就返回重复执行的错误信息,这样来保证幂等操作。

该方案跟之前的所有方案都有点不一样,需要两次请求才能完成一次业务操作。

  1. 第一次请求获取token

  2. 第二次请求带着这个token,完成业务操作。

「具体流程图如下:」

4.4 悲观锁机制

比如某银行有个转账场景,用户A手里有200块钱,想转出100元,正常情况下,用户A转账之后只剩下了100元,SQL如下:

update user amount = amount-100 where id=888;

但是实际情况下,并非如此,如果有个相同的请求进来,用户A的账户就会一直扣减,直到变成负数。这种情况,用户A直接哭死,在业务场景中也是不允许出现的。

通常情况下通过如下sql锁住单行数据:

select * from user id=888 for update;

「具体步骤:」

  1. 多个请求同时根据id查询用户信息。

  2. 判断余额是否不足100,如果余额不足,则直接返回余额不足。

  3. 如果余额充足,则通过for update再次查询用户信息,并且尝试获取锁。

  4. 只有第一个请求能获取到行锁,其余没有获取锁的请求,则等待下一次获取锁的机会。

  5. 第一个请求获取到锁之后,判断余额是否不足100,如果余额足够,则进行update操作。

  6. 如果余额不足,说明是重复请求,则直接返回成功。

悲观锁需要在同一个事务操作过程中锁住一行数据,如果事务耗时比较长,会造成大量的请求等待,影响接口性能。

此外,每次请求接口很难保证都有相同的返回值,所以不适合幂等性设计场景,但是在防重场景中是可以的使用的。

4.5 乐观锁机制

因为悲观锁是比较消耗的性能的操作,那么我们为了提高接口性能,完全可以使用乐观锁。需要再表中添加一个version字段。

比如:

alter table user add version int(2);

每当我们更新数据的时候,需要对我们的版本号+1

update user set amount = amount+100,version=version+1 where id = 888 and version =1  

更新数据的同时version+1,然后判断本次update操作的影响行数,如果大于0,则说明本次更新成功,如果等于0,则说明本次更新没有让数据变更。

等到下一个请求过来的时候,依然回去执行这行SQL,此时发现,根本不可能满足,version= 1  这个条件,因为version值已经修改了,那么前面必定已经成功过一次,后面都是重复的请求。

「流程图如下:」

「具体步骤:」

  1. 先根据id查询用户信息,包含version字段

  2. 根据id和version字段值作为where条件的参数,更新用户信息,同时version+1

  3. 判断操作影响行数,如果影响1行,则说明是一次请求,可以做其他数据操作。

  4. 如果影响0行,说明是重复请求,则直接返回成功。

4.6 建防重表

有时候我们的表中需要一些重复数据,只有一些特殊场景才不需要重复数据,此时我们上面的唯一索引方案可能就不太行了。

这时候,直接在表中加唯一索引,显然是不太合适的。

针对这种情况,我们可以通过建防重表来解决问题。

简单来说就是我们再单独建立一张表,只需要含有id和唯一索引,当然唯一索引可以是多个字段比如:name、code等组合起来的唯一标识

「流程图如下:」

「具体步骤:」

  1. 用户通过浏览器发起请求,服务端收集数据。

  2. 将该数据插入mysql防重表。

  3. 判断是否执行成功,如果成功,则做mysql其他的数据操作(可能还有其他的业务逻辑)。

  4. 如果执行失败,捕获唯一索引冲突异常,直接返回成功。

5.代码实践

以上我们都是给出了一些大概的解决方案跟思路,接下来Leo哥大家以Token机制为例,用代码实现如果解决接口的幂等性。

首先我们来回顾一下Token机制的整个流程。

下面开始直接上代码。

首先准备一个springboot工程项目,只需要添加两个依赖即可。

然后开始编写Redis工具类跟一个简单的Token工具类。

「RedisService」

package org.javatop.idempotent.token;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * @author : Leo
 * @version 1.0
 * @date 2024-02-01 21:03
 * @description :
 */
@Component
public class RedisService {

    @Autowired
    private RedisTemplate redisTemplate;

    public boolean setEx(String key, Object value, Long expireTime) {
        boolean result = false;
        try {
            ValueOperations ops = redisTemplate.opsForValue();
            ops.set(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 判断key是否存在
     * @param key key
     * @return
     */
    public boolean exists(String key) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }

    /**
     * 删除key
     * @param key key
     * @return
     */
    public boolean remove(String key) {
        if (exists(key)) {
            return Boolean.TRUE.equals(redisTemplate.delete(key));
        }
        return false;
    }
}

「TokenService」

主要是生成一个全局唯一不重复的Token,以及前端请求过来被拦截后需要检验token的方法。

package org.javatop.idempotent.token;

import io.micrometer.common.util.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.javatop.idempotent.exception.IdempotentException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.UUID;

/**
 * @author : Leo
 * @version 1.0
 * @date 2024-02-01 21:01
 * @description :
 */
@Component
public class TokenService {

    @Autowired
    RedisService redisService;

    public String createToken() {
        String uuid = UUID.randomUUID().toString();
        redisService.setEx(uuid, uuid, 10000L);
        return uuid;
    }

    public boolean checkToken(HttpServletRequest request) throws IdempotentException {
        String token = request.getHeader("token");
        if (StringUtils.isEmpty(token)) {
            token = request.getParameter("token");
            if (StringUtils.isEmpty(token)) {
                throw new IdempotentException("token 不存在");
            }
        }
        if (!redisService.exists(token)) {
            throw new IdempotentException("重复的操作");
        }
        boolean remove = redisService.remove(token);
        if (!remove) {
            throw new IdempotentException("重复的操作");
        }
        return true;
    }
}

「自定义幂等注解」

我们自定义一个幂等注解,来对我们想要幂等性一致的接口进行标识。

/**
 * @author : Leo
 * @version 1.0
 * @date 2024-02-01 21:17
 * @description : 幂等注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
}

「添加拦截器」

在拦截器中,我们解析出所有的请求,标注有幂等注解的请求,我们去检验他的token,然后来决定下一步操作。

package org.javatop.idempotent.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.javatop.idempotent.annotation.AutoIdempotent;
import org.javatop.idempotent.exception.IdempotentException;
import org.javatop.idempotent.token.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

/**
 * @author : Leo
 * @version 1.0
 * @date 2024-02-01 21:14
 * @description :
 */
@Component
public class IdempotentInterceptor implements HandlerInterceptor {
    @Autowired
    TokenService tokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        AutoIdempotent idempotent = handlerMethod.getMethod().getAnnotation(AutoIdempotent.class);
        if (idempotent != null) {
            try {
                return tokenService.checkToken(request);
            } catch (IdempotentException e) {
                throw e;
            }
        }
        return true;
    }
}

「测试」

最后编写接口进行测试。

 首先生成一个Token,然后把这个token放到hello接口的请求头上面。

可以看到,第一次可以正常访问接口 

但当你第二次访问该接口的时候,已经提示你操作重复了。因为在我们第一次访问接口之后,就把Redis中的token删除了。

  • 24
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
java面试笔试资料包括JAVA基础核心知识点深度学习Spring面试题等资料合集: JAVA核心知识点整理-282页 Java与哈希算法.docx Java中Lambda表达式的使用.docx JAVA多线程之线程间的通信方式.docx Java注解详解.docx Java线程池.docx JDK1.8Stream操作.docx JDK8有新特性.docx JVM堆三代.docx JVM的垃圾回收机制详解和调优.docx Spring源码分析之IoC.docx 关于线程和线程池的学习与使用.docx 深入理解JVM垃圾回收机制.docx 深入理解多线程实现的另一种方式Callable.docx 红黑树简介.docx 线程死锁及解决办法.docx 线程锁之重入锁.docx 线程间的通信.docx 虚拟机内存结构和垃圾回收docx.docx 锁分类的了解.docx 集合的扩容机制.png SpringMVC部分.docx Spring部分.docx 第一题.pdf 第七题 谈谈MySQL支持的事务隔离级别 (1).pdf 第三题 对比HashTable HashMap TreeMap有什么不同.pdf 第二题 Exception Error区别.pdf 第题 如何保证集合是线程安全的.pdf 第八题 Java并发类库提供的线程池有哪几种 分别有什么特点.pdf 第六题 synchronized和ReentLock有什么区别.pdf 第四题 ArrayList LinkedList Vector的区别.pdf docker讲得最清楚.doc Dubbo是什么?能做什么?.doc java 基于TCP协议的Socket编程和通信.doc Java面试高级篇—说说TCP,UDP和socket,Http之间联系和区别.doc MySQL千万级的大表要怎么优化(读写分离、水平拆分、垂直拆分).doc redis缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级.doc RPC (Remote Procedure Call)即远程过程调用.doc Spring 面试问题 TOP 50(干货推荐收藏必备).doc springboot常见面试题.doc svn和git的区别及适用场景.doc ZooKeeper.doc 为什么分布式一定要有Redis.doc 分布式、高并发、多线程,到底有什么区别.doc 分布式事务.doc 四款消息队列大比拼.docx 多台web服务器之间共享session.docx 消息中间件Kafka与RabbitMQ.doc 电商项目描述注意点.doc 秒杀业务的流量削峰场景如何解决.doc 面试题:Kafka、ActiveMQ、RabbitMQ、RocketMQ 有什么优缺点.doc
当面试官问到Java接口自动化测试的面试题时,你可以回答如下内容: Java接口自动化测试是指使用Java编程语言来进行接口测试的自动化过程。在接口自动化测试中,我们可以使用各种工具和框架来发送HTTP请求,验证响应结果,并进行断言和报告生成等操作。 以下是一些常见的Java接口自动化测试面试题及其答案: 1. 什么是接口自动化测试? 接口自动化测试是指使用自动化工具和框架来模拟和验证接口的行为和功能。通过发送HTTP请求,获取接口的响应结果,并进行断言和验证,以确保接口的正确性和稳定性。 2. 请介绍一下你在接口自动化测试中使用的工具和框架。 在Java接口自动化测试中,常用的工具和框架有: - Apache HttpClient:用于发送HTTP请求和获取响应结果。 - RestAssured:一个流行的Java库,用于编写易读且易于维护的接口测试代码。 - TestNG:一个功能强大的测试框架,用于编写和执行接口测试用例。 - JUnit:另一个常用的Java测试框架,也可以用于编写和执行接口测试用例。 - Postman:一个流行的API开发和测试工具,可以用于发送HTTP请求并验证接口的响应结果。 3. 请介绍一下接口自动化测试的流程。 接口自动化测试的流程通常包括以下几个步骤: - 确定测试目标和需求:明确要测试的接口和测试的功能。 - 设计测试用例:根据接口的需求和功能,设计相应的测试用例。 - 编写测试代码:使用Java编程语言,编写发送HTTP请求、验证响应结果和断言的代码。 - 执行测试用例:运行编写好的测试代码,发送请求并验证响应结果。 - 生成报告和分析结果:根据测试结果生成测试报告,并分析接口的性能和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

之乎者也·

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值