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

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 不匹配就返回重复执行的错误信息,这样来保证幂等操作。

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

第一次请求获取token

第二次请求带着这个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;
「具体步骤:」

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

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

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

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

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

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

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

此外,每次请求接口很难保证都有相同的返回值,所以不适合幂等性设计场景,但是在防重场景中是可以的使用的。
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值已经修改了,那么前面必定已经成功过一次,后面都是重复的请求
在这里插入图片描述
「具体步骤:」

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

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

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

如果影响0行,说明是重复请求,则直接返回成功。
5.代码实践
以上我们都是给出了一些大概的解决方案跟思路,接下来Leo哥大家以Token机制为例,用代码实现如果解决接口的幂等性。

首先我们来回顾一下Token机制的整个流程。
在这里插入图片描述
首先准备一个springboot工程项目,只需要添加两个依赖即可。
在这里插入图片描述

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;
    }
}
主要是生成一个全局唯一不重复的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 :
 */

```java
@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;
    }
}

「自定义幂等注解」

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


```java
/**
 * @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删除了。
在这里插入图片描述

  • 28
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值