接口幂等性解决方案

接口幂等性解决方案

假如有个服务提供了一个订单支付接口(服务为负载微服务),用户在前端调用时一不小心点击了两次,生成了两次支付请求,然后这一笔订单进行了两次支付,扣了两次钱,这就是接口没有保证幂等性的结果;

幂等性概念

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。

在编程中,一个幂等操作的特点是其任意多次执行所产生的的影响均与一次执行的影响相同,

幂等函数,幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数

更复杂的操作幂等保证是利用唯一交易号(流水号)实现.

幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的

保证幂等性的核心
  • 每个请求都需要有一个唯一的标识
  • 每次处理完请求之后,必须有一个记录标识这个请求已经被处理过了
  • 每次接收请求之后,处理请求之前,判断当前请求是否已经被处理过了
技术方案
  • 查询操作:查询操作天生就是幂等性操作。查询一次和查询多次,在查询条件不变的情况下,查询结果是一样的
  • 删除操作:删除操作天生就是幂等性操作。删除一次和删除多次, 在删除条件不变的情况下,删除的返回结果可能不同,但是最终的数据库删除结果是相同的(第一次删除成功,第二次会返回删除失败,最终结果还是删除成功,第一次删除失败,第二次删除失败,最终结果依然是删除失败)
  • 添加操作:添加操作可以通过数据库中的唯一值进行校验,从而保证只能添加一次
  • 更新操作:更新操作可以通过在数据库中设置递增的版本号来实现,如果版本号已被更改,那么就不能就不能进行第二次更新操作
常见解决方案
  • 业务表内使用算法生成唯一索引
  • 业务表内设置一个注册机机制
  • 业务表内增加version(mybatis plus已实现),进行基于版本号的更新
  • 基于mysql/redis的机制进行解决
基于redis的keyvalue机制实现

创建一个注解,用在需要进行防止重复点击的方法接口上


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


/**
 * 幂等性注解,默认五秒重复点击
 * @author windrunner9527
 */
// 该注解可以放在方法上
@Target(ElementType.METHOD)
// 该注解程序运行时也在
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotency {

    String value() default "5";

}

对这个注解进行切面


import com.windrunner9527.idempotency.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
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.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 对幂等性注解进行切面
 * @author windrunner9527
 */
// 日志
@Slf4j
// 切面
@Aspect
// 添加spring容器
@Component
// 开启代理
@EnableAspectJAutoProxy
public class IdempotencyAspect {

    /**
     * 获取重入锁
     */
    Lock lock = new ReentrantLock();


    /**
     * 对使用了幂等性注解的方法进行方法切面
     */
    @Pointcut("@annotation(Idempotency)")
    public void cupPoint(){

    }


    /**
     * 对上面的方法进行环绕
     * @param point
     * @return
     * @throws Throwable
     */
    @Around("cupPoint()")
    public Object idempotency(ProceedingJoinPoint point) throws Throwable {
        // 获取servlet上下文信息属性
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        // 获取request和response
        HttpServletRequest request = requestAttributes.getRequest();
        HttpServletResponse response = requestAttributes.getResponse();

        // 获取当前执行的方法的信息
        Method method = ((MethodSignature) point.getSignature()).getMethod();
        // 获取当前方法上的idmpotency注解
        Idempotency annotation = method.getAnnotation(Idempotency.class);
        // 获取当前方法上的idmpotency的值
        String time = annotation.value();
        log.info("当前方法设置重复点击幂等性时间为:"+time+"秒");

        // 从请求头中获取认证的token
        String authentication = request.getHeader("Authentication");
        // 如果请求头中没有获取token,那么就返回Message
        if (authentication == null){
            return "当前请求头中没有携带token,请登录后使用本功能";
        }
        // 获取当前请求调用的是哪个请求路径
        String servletPath = request.getServletPath();
        log.info("当前用户请求的路径参数:"+servletPath);

        // 拼接token和当前的请求路径
        String res = authentication + servletPath;

        //上锁,防止多次获取值
        lock.lock();
        // 如果当前有这个key,那么说明这个方法已经被这个token的携带者点击过了,进行幂等性判断,不能连续点击
        if (RedisUtils.hasKey(res)){
            log.warn("点击速度过快,请稍后重试");
            return "点击速度过快,请稍后重试";
        }
        // 如果当前没有这个key,那么将这个key放到redis中,并加上过期时间
        RedisUtils.set(res,null,Long.parseLong(time));
        // 解锁
        lock.unlock();

        // 执行被切面的方法
        Object proceed = point.proceed();

        // 返回方法执行后的值
        return proceed;
    }

}

当需要使用防止重复点击时,将这个注解方法放置到需要使用的controller上,就可以控制时效和点击频率

posted @ 2020-09-09 10:42  风行9527  阅读(303)  评论(0)    收藏  举报