SpringBoot中通过Redis的setnx和自定义注解@Idempotent实现API幂等处理

1.简述

  • 目的:一定时间内,同样的请求(业务参数相同)访问同一个接口,则只能成功一次,其余被拒绝。

2.引入redis支持

因为需要通过redissetnx确保只有一个接口能够正常访问,所以需要引入redis。

2.1.pom.xml

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
  <exclusions>
    <!-- 需要排除哪些包由具体项目觉得 -->
    <exclusion>
      <artifactId>spring-boot-starter-logging</artifactId>
      <groupId>org.springframework.boot</groupId>
    </exclusion>
  </exclusions>
</dependency>

2.2.application.properties

spring.redis.host=11.22.33.44
spring.redis.port=26379
spring.redis.database=1
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1
spring.redis.pool.max-idle=500
spring.redis.pool.min-idle=0
spring.redis.timeout=0

2.3.Redis JUnit Test Case

/**
 * @author hanchao
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisTemplateTest {

    @Resource
    private RedisTemplate<String,String > redisTemplate;

    @Test
    public void simpleTest() {
        ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
        String key = "RedisTemplateTest-simpleTest-001";
        valueOperations.set(key,key+key);
        System.out.println(valueOperations.get(key));
    }
}

3.引入幂等

3.1.幂等异常

/**
 * 用于专门处理幂等相关异常。
 * @author hanchao
 */
public class IdempotentException extends RuntimeException {

    public IdempotentException(String message) {
        super(message);
    }

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

3.2.幂等注解

/**
 * 幂等注解
 * @author wangchao
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
    /**
     * 幂等名称,作为redis缓存Key的一部分。
     */
    String value();
    
    /**
     * 幂等过期时间,即:在此时间段内,对API进行幂等处理。
     */
    long expireMillis();
}

3.3.幂等切面

/**
 * 幂等切面
 * @author wangchao
 */
@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(pers.hanchao......anno.Idempotent)")
    public void executeIdempotent() {
    }

    @Around("executeIdempotent()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
      	//获取方法
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
      	//获取幂等注解
        Idempotent idempotent = method.getAnnotation(Idempotent.class);
      	//根据 key前缀 + @Idempotent.value() + 方法签名 + 参数 构建缓存键值
      	//确保幂等处理的操作对象是:同样的 @Idempotent.value() + 方法签名 + 参数
        String key = String.format(KEY_TEMPLATE, idempotent.value() + "_" + KeyUtil.generate(method, joinPoint.getArgs()));
        //通过setnx确保只有一个接口能够正常访问
        //调用KeyUtil工具类生成key
      	String redisRes = redisTemplate.execute((RedisCallback<String>) conn -> ((JedisCommands) conn.getNativeConnection()).set(key, key, "NX", "PX", idempotent.expireMillis()));
      
        if (Objects.equals("OK", redisRes)) {
            return joinPoint.proceed();
        } else {
            LOGGER.debug("Idempotent hits, key=" + key);
            throw new IdempotentException("Idempotent hits, key=" + key);
        }
    }
}

3.4.工具类

/**
 * Key生成工具
 * @author hanchao
 */
public class KeyUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(KeyUtil.class);

    /**
     * 根据{方法名 + 参数列表}和md5转换生成key
     */
    public static String generate(Method method, Object... args) {
        StringBuilder sb = new StringBuilder(method.toString());
        for (Object arg : args) {
            sb.append(toString(arg));
        }
        return DigestUtils.md5Hex(sb.toString());
    }

    private static String toString(Object object) {
        if (object == null) {
            return "null";
        }
        if (object instanceof Number) {
            return object.toString();
        }
        //调用json工具类转换成String
        return JsonUtil.toJson(object);
    }
}

/**
 * Json格式化工具
 * @author hanchao
 */
public class JsonUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(JsonUtil.class);
    private static final ObjectMapper MAPPER = new ObjectMapper();

    static {
        MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).setSerializationInclusion(Include.NON_NULL);
    }

    /**
     * Java Object Maps To Json
     */
    public static String toJson(Object obj) {
        String result;
        if (obj == null || obj instanceof String) {
            return (String) obj;
        }
        try {
            result = MAPPER.writeValueAsString(obj);
        } catch (Exception e) {
            LOGGER.error("Java Object Maps To Json Error !");
            throw new RuntimeException("Java Object Maps To Json Error !", e);
        }
        return result;
    }
}

4.对接口标记幂等注解

@RestController
public class DemoController {
    @Resource
    private DemoService demoService;

    /**
     * @Idempotent的value值随意,一般保持与接口url一致接口。
     */
    @Idempotent(value = "/cock/alarm", expireMillis = 1000L)
    @PostMapping(value = "/cock/alarm")
    public String demo(@RequestBody DemoPo po) {
      //..
    }
}

猜你喜欢

转载自blog.csdn.net/hanchao5272/article/details/92073405