Handling the idempotence of Spring Boot interface

Handling the idempotence of Spring Boot interface

In distributed services, the same request will appear multiple times when the business is highly concurrent or may be called multiple times. At this time, if the inserted business operation is executed, multiple pieces of data will appear in the database, resulting in dirty data, which is also a waste of resources.
At this time, we need to prevent the processing operations of redundant business.

Implementation plan

Realize the idempotence of the interface, so that the request only succeeds once. Here you need to save a unique identification key, and obtain whether there is an identification when the next same request (the unique index of a similar table, the request timestamp is different but several core parameters are the same as the same request) is executed, and the execution will be blocked if repeated submissions.

Reference Redis dependencies

 		 <!--引入Redis支持-->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>
    </dependencies>

Code

1. Customize idempotent annotations

package com.nscw.freshkeruyun.config.annotation;

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

/**
 * 自定义幂等注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
    
    
    //注解自定义redis的key的前缀,后面拼接参数
    String key();

    //自定义的传入参数序列作为key的后缀,默认的全部参数作为key的后缀拼接。参数定义示例:{0,1}
    int[] custKeysByParameterIndexArr() default {
    
    };

    //过期时间,单位秒。可以是毫秒,需要修改切点类的设置redis值的代码参数。
    long expirMillis();
}

2. Generate key value tools

package com.nscw.freshkeruyun.utils;

import com.alibaba.fastjson.JSON;

import java.lang.reflect.Method;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
 * Copyright (C), 2019-2020
 * FileName: IdempotentKeyUtil
 * Author:   SixJR.
 * Date:     2020年11月17日 10:09
 * Description: 生成key值工具类
 * History:
 * <author>          <time>          <version>          <desc>
 */

public class IdempotentKeyUtil {
    
    
    /**
     * 对接口的参数进行处理生成固定key
     *
     * @param method
     * @param custArgsIndex
     * @param args
     * @return
     */
    public static String generate(Method method, int[] custArgsIndex, Object... args) {
    
    
        String stringBuilder = getKeyOriginalString(method, custArgsIndex, args);
        //进行md5等长加密
        return md5(stringBuilder.toString());
    }

    /**
     * 原生的key字符串。
     *
     * @param method
     * @param custArgsIndex
     * @param args
     * @return
     */
    public static String getKeyOriginalString(Method method, int[] custArgsIndex, Object[] args) {
    
    
        StringBuilder stringBuilder = new StringBuilder(method.toString());
        int i = 0;
        for (Object arg : args) {
    
    
			if (args[i] instanceof ServletRequest || args[i] instanceof ServletResponse || args[i] instanceof MultipartFile) {
    
    
                /* ServletRequest不能序列化,从入参里排除,否则报异常:java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
                ServletResponse不能序列化 从入参里排除,否则报异常:java.lang.IllegalStateException: getOutputStream() has already been called for this response */
                continue;
            }

            if (isIncludeArgIndex(custArgsIndex, i)) {
    
    
//                System.out.println("arg---------->"+arg);
                stringBuilder.append(toString(arg));
            }
            i++;
        }
        return stringBuilder.toString();
    }

    /**
     * 判断当前参数是否包含在注解中的自定义序列当中。
     *
     * @param custArgsIndex
     * @param i
     * @return
     */
    private static boolean isIncludeArgIndex(int[] custArgsIndex, int i) {
    
    
        //如果没自定义作为key的参数index序号,直接返回true,意味加入到生成key的序列
        if (custArgsIndex.length == 0) {
    
    
            return true;
        }

        boolean includeIndex = false;
        for (int argsIndex : custArgsIndex) {
    
    
            if (argsIndex == i) {
    
    
                includeIndex = true;
                break;
            }
        }
        return includeIndex;
    }

    /**
     * 使用jsonObject对数据进行toString,(保持数据一致性)
     *
     * @param obj
     * @return
     */
    public static String toString(Object obj) {
    
    
        if (obj == null) {
    
    
            return "-";
        }
        return JSON.toJSONString(obj);
    }

    /**
     * 对数据进行MD5等长加密
     *
     * @param str
     * @return
     */
    public static String md5(String str) {
    
    
        StringBuilder stringBuilder = new StringBuilder();
        try {
    
    
            //选择MD5作为加密方式
            MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(str.getBytes());
            byte b[] = mDigest.digest();
            int j = 0;
            for (int i = 0, max = b.length; i < max; i++) {
    
    
                j = b[i];
                if (j < 0) {
    
    
                    i += 256;
                } else if (j < 16) {
    
    
                    stringBuilder.append(0);
                }
                stringBuilder.append(Integer.toHexString(j));
            }
        } catch (NoSuchAlgorithmException e) {
    
    
            e.printStackTrace();
        }
        return stringBuilder.toString();
    }
}

3. Create a custom exception, skip the execution of the method body business code of the interface, and throw this exception.

package com.nscw.freshkeruyun.config.error;

/**
 * Copyright (C), 2019-2020
 * FileName: IdempotentException
 * Author:   SixJR.
 * Date:     2020年11月16日 16:07
 * Description: 幂等自定义异常处理代码
 * History:
 * <author>          <time>          <version>          <desc>
 */

public class IdempotentException extends RuntimeException {
    
    
    public IdempotentException(String message) {
    
    
        super(message);
    }

    @Override
    public String getMessage() {
    
    
        return super.getMessage();
    }
}

4. AOP intercepts our custom annotations

package com.nscw.freshkeruyun.config;
import com.nscw.freshkeruyun.config.annotation.Idempotent;
import com.nscw.freshkeruyun.config.error.IdempotentException;
import com.nscw.freshkeruyun.utils.IdempotentKeyUtil;
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.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
 * Copyright (C), 2019-2020
 * FileName: IdempotentAspect
 * Author:   SixJR.
 * Date:     2020年11月17日 10:11
 * Description: 自定义幂等aop切点
 * History:
 * <author>          <time>          <version>          <desc>
 */
@Component
@Slf4j
@Aspect
@ConditionalOnClass(RedisTemplate.class)
public class IdempotentAspect {
    
    
    private static final String KEY_TEMPLATE = "idempotent_%S";

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 切点(自定义注解)
     */
    @Pointcut("@annotation(com.nscw.freshkeruyun.config.annotation.Idempotent)")
    public void executeIdempotent() {
    
    

    }

    /**
     * 切点业务
     *
     * @throws Throwable
     */
    @Around("executeIdempotent()")
    public Object arountd(ProceedingJoinPoint jPoint) throws Throwable {
    
    
        //获取当前方法信息
        Method method = ((MethodSignature) jPoint.getSignature()).getMethod();
        //获取注解
        Idempotent idempotent = method.getAnnotation(Idempotent.class);
        //生成Key
        Object[] args = jPoint.getArgs();
        int[] custArgs = idempotent.custKeysByParameterIndexArr();

        String key = String.format(KEY_TEMPLATE, idempotent.key() + "_" + IdempotentKeyUtil.generate(method, custArgs, args));
        //https://segmentfault.com/a/1190000002870317 -- JedisCommands接口的分析
        //nxxx的值只能取NX或者XX,如果取NX,则只有当key不存在是才进行set,如果取XX,则只有当key已经存在时才进行set
        //expx expx的值只能取EX或者PX,代表数据过期时间的单位,EX代表秒,PX代表毫秒
        // key value nxxx(set规则) expx(取值规则) time(过期时间)

        //低版本`Springboot`使用如下方法
//        String redisRes = redisTemplate.execute((RedisCallback<String>) conn -> ((RedisAsyncCommands) conn).getStatefulConnection().sync().set(key, "NX", "EX", idempotent.expirMillis()));

        // Jedis jedis = new Jedis("127.0.0.1",6379);
        // jedis.auth("xuzz");
        // jedis.select(0);
        // String redisRes = jedis.set(key, key,"NX","EX",idempotent.expirMillis());
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "0", idempotent.expirMillis(), TimeUnit.SECONDS);

        if (result) {
    
    
            return jPoint.proceed();
        } else {
    
    
            log.info("数据幂等错误");
            throw new IdempotentException("幂等校验失败。key值为:" + IdempotentKeyUtil.getKeyOriginalString(method, custArgs, args));
        }
    }

}

Description:

If it is Springboot 2.X or above, its Redis uses lettuce and the above code is used. Low version of Springboot uses annotated code

String redisRes = redisTemplate.execute((RedisCallback<String>) conn -> 
((RedisAsyncCommands) conn).getStatefulConnection().sync()
.set(key, "NX", "EX", idempotent.expirMillis()));   

The return result is that the string OK is not true.

5. Use the modified annotations in the Controller method

    @PostMapping("orderStatePush2")
    @Idempotent(key = "orderStatePush2", expirMillis = 100)
    public KeruyunResultDto orderStatePush2(@RequestParam("name") String name,@RequestParam("age") int name) {
    
    
        KeruyunResultDto keruyunResultDto = new KeruyunResultDto();
        keruyunResultDto.setCode(CodeConfig.getCode0());
        keruyunResultDto.setMessage("OK");
        keruyunResultDto.setMessageUuid(UUID.randomUUID().toString());
        System.out.println("订单状态推送:" + JSONObject.toJSONString(name));
        return keruyunResultDto;
    }

Explanation The
annotation has a parameter item- custKeysByParameterIndexArr , which defines which parameters are used as the key by specifying the serial number of the parameter, 0 represents the first parameter, 1 represents the second parameter..., if the parameter item is empty, all parameters are used by default Spliced ​​as key.

@Idempotent(key = "orderStatePush2", expirMillis = 100)
//默认是把java方法orderStatePush2的所有参数name和age的值作为key来进行幂等。等同
@Idempotent(key = "orderStatePush2", custKeysByParameterIndexArr = {
    
    0,1}, expirMillis = 100)

If you only want to use the first parameter as the key, write @Idempotent(key = "orderStatePush2", custKeysByParameterIndexArr = {0}, expirMillis = 100)

Next is the test result

The first request: the idempotent check passed the
Insert picture description here
second request: the idempotent check failed, indicating that it has been requested
Insert picture description here

Guess you like

Origin blog.csdn.net/m0_43413873/article/details/109742532