通过redis实现SpringBoot接口幂等性的自定义注解

一、什么是幂等性

幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同。在计算机中编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。

幂等函数或幂等方法是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。

二、什么是接口幂等性

在HTTP/1.1中,对幂等性进行了定义。它描述了一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外),即第一次请求的时候对资源产生了副作用,但是以后的多次请求都不会再对资源产生副作用。

这里的副作用是不会对结果产生破坏或者产生不可预料的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。

三、为什么需要实现幂等性

在接口调用时一般情况下都能正常返回信息不会重复提交,不过在遇见以下情况时可以就会出现问题,如:

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

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

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

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

使用幂等性最大的优势在于使接口保证任何幂等性操作,免去因重试等造成系统产生的未知的问题。

四、引入幂等性后对系统的影响

幂等性是为了简化客户端逻辑处理,能放置重复提交等操作,但却增加了服务端的逻辑复杂性和成本,其主要是:

  • 把并行执行的功能改为串行执行,降低了执行效率。

  • 增加了额外控制幂等的业务逻辑,复杂化了业务功能;

所以在使用时候需要考虑是否引入幂等性的必要性,根据实际业务场景具体分析,除了业务上的特殊要求外,一般情况下不需要引入的接口幂等性。

       

             实现常规方式代码量要求较多,部署较为复杂,通过注解实现接口幂等性简单快捷。

需要的maven依赖

        <!--redis依赖-->
         <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        
        <!--fastjson-->
         <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.32</version>
        </dependency>

        

接下来配置安装好redis,springboot中application.yml文件配置

redis:
    host: 127.0.0.1         # Redis服务器地址
    port: 6379              # Redis服务器连接端口

接下来自定义注解类

package com.woniu.util;


import java.lang.annotation.*;

/**
 * 幂等注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
    /**
     * 幂等名称,作为redis缓存Key的一部分。
     */
    String value();

    /**
     * 幂等过期时间,即:在此时间段内,对API进行幂等处理。
     */
    long expireMillis();
}

然后是注解实现类

import com.alibaba.fastjson.JSON;
import com.woniu.util.Idempotent;
import com.woniu.util.ResponseResult;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;


@Aspect
@Component
@ConditionalOnClass(RedisTemplate.class)
public class IdempotentAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(IdempotentAspect.class);
    /**
     * redis缓存key的模板
     */
    private static final String KEY_TEMPLATE = "idempotent_%s";

    @Resource
    private RedisTemplate<String,String> redisTemplate;

    /**
     * 根据实际路径进行调整
     */
    @Pointcut("@annotation(com.woniu.util.Idempotent)")
    public void executeIdempotent() {
    }



    @Around("executeIdempotent()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {

        // 获取request
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        // 得到请求头是否有 token ,没有就从参数找
        String token = request.getHeader("token");
        if (StringUtils.isEmpty(token)) {
            token = request.getParameter("token");
        }

        ValueOperations<String, String> opsValue = redisTemplate.opsForValue();

        //获取方法
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();

        //获取幂等注解
        Idempotent idempotent = method.getAnnotation(Idempotent.class);
        
        //根据 key前缀 + @Idempotent.value() + 方法签名 + 参数 构建缓存键值
        String key=String.format(token+"_"+idempotent.value() + "_" + generate(method.toString(), joinPoint.getArgs()));
        
        //通过加锁确保只有一个接口能够正常访问
        String s = opsValue.get(key);
        if (s==null) {
            //缓存请求的key
            opsValue.set(key,"false",idempotent.expireMillis(), TimeUnit.MILLISECONDS);
            return joinPoint.proceed();
        } else {
            LOGGER.info("当前时间:"+System.currentTimeMillis()+"");
            LOGGER.info("方法名"+method.toString());

           //这里根据你实际业务需求接口幂返回结果,我这里使用工具类返回前端结果,你也可以解开注释直接抛异常
           // throw new ServiceException("你的操作太频繁了!");
            ResponseResult<Void> responseResult = new ResponseResult<>(8181, "你的操作太频繁了!");
            return responseResult;
        }
    }

    //json生成redis key
    public  String generate(Object method, Object joinPoint){
        String newMethod= JSON.toJSONString(method);
        String newJoinPoint=JSON.toJSONString(joinPoint);
        return newMethod+newJoinPoint;
    }
}

     

       这个是返回给前端结果的泛型工具类,有需要可以添加,也可以根据需要直接抛异常,异常方法这里就不贴了,自行百度

package com.woniu.util;

import lombok.Data;

@Data
public class ResponseResult<T> {
    private int status;
    private String msg;
    private T data;

    public ResponseResult(){}

    public ResponseResult(int status, String msg){
        this.status = status;
        this.msg = msg;
    }
    public ResponseResult(T data, String msg, int status){
        this(status,msg);
        this.data = data;
        this.msg = msg;
    }

    public static ResponseResult ok(){
        ResponseResult result = new ResponseResult();
        result.setStatus(ResultCode.SUCCESS.getCode());
        result.setMsg(ResultCode.SUCCESS.getMessage());
        return result;
    }

    public static ResponseResult error(ResultCode resultCode){
        ResponseResult result = new ResponseResult();
        result.setStatus(resultCode.getCode());
        result.setMsg(resultCode.getMessage());
        return result;
    }

    public static ResponseResult<Void> SUCCESS = new ResponseResult<>(200,"成功");
    public static ResponseResult<Void> INTEVER_ERROR = new ResponseResult<>(500,"服务器错误");
    public static ResponseResult<Void> NOT_FOUND = new ResponseResult<>(404,"未找到");

}

         最后在controller接口方法上添加如下注解,注解value中写上完整的后台接口请求地址,实现类会获取请求用户的token和请求的后台接口地址拼接成一个key存入redis,下一次用户的点击会判断这个请求key是否存在 。expireMillis表示幂等过期时间,时间过期后会删除这个字符串

@Idempotent(value = "/user/update", expireMillis = 5000L)

     这个注解不是所有接口都需要添加,添加此注解需要根据实际业务情况来看。有些方法短时间需要请求多次的话,会出现异常

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值