接口幂等性解决方法

1.数据库唯一主键实现接口幂等性

使用分布式ID充当主键,不适用mysql中的自增主键

可以使用uuID 或者雪花算法

2.乐观锁实现幂等性

在表中增加版本号字段标识。新增的时候添加版本号,更新的时候带着版本号去更新,版本号一致才更新处理。

3.分布式锁实现幂等性

通过控制锁的粒度来提高程序执行的性能,只锁当前的用户,相当于只锁自己。

4.通过token

1.服务端提供获取token 的方法,请求前客户端调用接口获取token

2.将token 存入redis 并设置过期时间

3.客户端请求带着token

4.服务端接受请求后根据token到redis中查到是否存在

5.如果存在就删除key,正常执行逻辑,如果不存在就抛异常,返回重复提交的错误提示。

6.服务器如果短时间内重复提交这个接口,因为两次请求token是一样的,所以第二次请求的时候,服务器校验token时,redis中已经没有了刚刚被第一次删掉的token,就表示是重复操作,所以第二次请求会校验失败,不作处理,这样就保证了业务代码,不被重复执行。

以下是token 的实现代码

package com.zlt.annotation;

import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONUtil;
import com.zlt.consts.CommonConstant;
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.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
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 java.lang.reflect.Method;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @author slf
 * @date 2022年11月21日 15:18
 */
@Aspect
@Component
@Slf4j
public class NoRepeatSubmitAop {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 定义切点
     */
    @Pointcut("@annotation(com.zlt.annotation.RepeatSubmit)")
    public void preventDuplication() {
    }

    @Around("preventDuplication()")
    public Object around(ProceedingJoinPoint joinPoint) throws Exception {
        /**
         * 获取请求信息
         */
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();

        HttpServletRequest request = attributes.getRequest();

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

        //获取防重复提交注解
        RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);

        // 获取token以及方法标记,生成redisKey和redisValue
        String token = request.getHeader("token");

        String url = request.getRequestURI();

        /**
         *  通过前缀 + url + token + 函数参数签名 来生成redis上的 key
         *
         */
        String redisKey = "PREVENT_DUPLICATION_PREFIX"
                .concat(url)
                .concat(token)
                .concat(getMethodSign(method, joinPoint.getArgs()));

        // 这个值只是为了标记,不重要
        String redisValue = redisKey.concat(annotation.value()).concat("submit duplication");

        if (!redisTemplate.hasKey(redisKey)) {
            // 设置防重复操作限时标记(前置通知)
            redisTemplate.opsForValue()
                    .set(redisKey, redisValue, annotation.expireSeconds(), TimeUnit.SECONDS);
            try {
                //正常执行方法并返回
                //ProceedingJoinPoint类型参数可以决定是否执行目标方法,
                // 且环绕通知必须要有返回值,返回值即为目标方法的返回值
                return joinPoint.proceed();
            } catch (Throwable throwable) {
                //确保方法执行异常实时释放限时标记(异常后置通知)
                redisTemplate.delete(redisKey);
                throw new RuntimeException(throwable);
            }
        } else {
            // 重复提交了抛出异常,如果是在项目中,根据具体情况处理。
            throw new RuntimeException("请勿重复提交");
        }


    }

    /**
     * 生成方法标记:采用数字签名算法SHA1对方法签名字符串加签
     *
     * @param method
     * @param args
     * @return
     */
    private String getMethodSign(Method method, Object... args) {
        StringBuilder sb = new StringBuilder(method.toString());
        for (Object arg : args) {
            sb.append(toString(arg));
        }
        return DigestUtil.sha1Hex(sb.toString());
    }

    private String toString(Object arg) {
        if (Objects.isNull(arg)) {
            return "null";
        }
        if (arg instanceof Number) {
            return arg.toString();
        }
        return JSONUtil.toJsonStr(arg);
    }

}

注解类 RepeatSubmit

package com.zlt.annotation;

import java.lang.annotation.*;

@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)

public @interface RepeatSubmit {
    /**
     * 防重复操作限时标记数值(存储redis限时标记数值)
     */
    String value() default "value";

    /**
     * 防重复操作过期时间(借助redis实现限时控制)
     */
    long expireSeconds() default 10;

}

 然后在请求的接口上加上主键     @RepeatSubmit(expireSeconds = 8)

    /**
     * 提交订单
     *
     * @param body 订单信息,{ cartId:xxx, addressId: xxx, couponId: xxx, message: xxx,
     *             grouponRulesId: xxx, grouponLinkId: xxx}
     * @return 提交订单操作结果
     */
    @PostMapping("submit")
    @ApiOperation("提交订单")
    @RepeatSubmit(expireSeconds = 8)
    public Object submit(@RequestBody OrderInfo body) {
        log.info("【请求开始】提交用户订单,请求参数,body:{}", body);
        Long userId = UserContext.getUserId();
        return wxOrderService.submit(userId, body);
    }

你可能感兴趣的:(java,SpringBoot,java)