请别在问怎么做接口幂等性了 | Eddie'Blog
请别在问怎么做接口幂等性了

请别在问怎么做接口幂等性了

eddie 449 2021-08-26

目录

两套通过redis+aop来控制

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

(一)通过自定义注解

1.1 按标题所说,定义注解

import java.lang.annotation.*;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * @author eddie.lee
 */
@Documented
@Retention(RUNTIME)
@Target({METHOD})
public @interface RejectReqSubmit {

    long timeout() default 5;

}

1.2 新建切面类

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.BoundValueOperations;
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.util.concurrent.TimeUnit;

/**
 * @author eddie.lee
 */
@Aspect
@Component
public class RejectReqSubAspect {

    /**
     * 请求时候带过来 </p>
     * 比如: "token: 123456"
     */
    private static String REQUEST_HEADER = "token";

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * @execution:用于匹配方法执行的连接点
     * @annotation:用于匹配当前执行方法持有指定注解的方法
     */
    @Pointcut("@annotation(com.eddc.usercenter.reject.RejectReqSubmit)")
    public void annotationPointCut() {
    }

    @Around("annotationPointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Object proceed = joinPoint.proceed();
        // 获取 req 对象
        ServletRequestAttributes attributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
        HttpServletRequest request = attributes == null ? null : attributes.getRequest();
        // 如果不是有效的api接口请求则不处理
        if (null == request) {
            proceed = joinPoint.proceed(joinPoint.getArgs());
            return proceed;
        }
        // 获取传入属性
        String token = request.getHeader(REQUEST_HEADER);
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // 先获取redis对value的操作对象,需要先设定key
        // 使用.set(key,value)方法进行设置,get(key)方法用来获取
        BoundValueOperations redis = redisTemplate.boundValueOps(token + request.getRequestURI());
        // 获取字符串所有值
        if (redis.get() != null) {
//            throw new SecurityException("你已经按过好多次了哦~~ 不如休息下?");
            System.err.println("你已经按过好多次了哦~~ 不如休息下?");
        }
        long timeout = methodSignature.getMethod().getAnnotation(RejectReqSubmit.class).timeout();
        // 设置value的超时时间,timeout为数字,unit为单位,例如天,小时等
        redis.set("eddie", timeout, TimeUnit.SECONDS);
//        System.out.println("redis:" + redis.get());
        redisDelete(token, request);
        return proceed;
    }

    /**
     * 最后删除Key感觉很多余吗? TIPS: 留意最后的图
     */
    private void redisDelete(String token, HttpServletRequest request) {
        redisTemplate.delete(token + request.getRequestURI());
    }

}

1.3 单元测试

就调用一个普通查库接口

@RestController
public class TestController {

    @Autowired
    private UserMapper userMapper;

    /**
     * 根据 @RejectReqSubmit 控制需要做幂等的接口
     */
    @RejectReqSubmit
    @GetMapping("/test-get")
    public User query(@RequestParam Integer id) {
//        TimeUnit.SECONDS.sleep(1);
        System.out.println("-------------------");
        User user = userMapper.selectByPrimaryKey(id);
        return user;
    }
}

单元测试

import com.alibaba.fastjson.JSONObject;
import com.itmuch.UserCenterApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.util.concurrent.CountDownLatch;

import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = UserCenterApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserCenterApplicationTests {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void testUser() throws InterruptedException {
        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
        headers.add("token", "eddie");
        CountDownLatch countDownLatch = new CountDownLatch(100);
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 10; j++) {
                    // 模拟请求 /test-get?id=1 接口
                    ResponseEntity responseEntity = restTemplate.exchange("/test-get?id=1", HttpMethod.GET, new HttpEntity<Object>(headers), JSONObject.class);
                    System.out.println(Thread.currentThread().getName() + ": "+ responseEntity.getBody());
                    assertThat(HttpStatus.OK, equalTo(responseEntity.getStatusCode()));
                    countDownLatch.countDown();
                }
            }).start();
        }
        countDownLatch.await();
    }

}

图片.png


(二)通过指定操作类Mapping注解

上面方案改动指定 PostMapping、DeleteMapping、PutMapping,因为GetMapping做肯定是多余的

2.1 新建切面类

import org.apache.commons.lang3.StringUtils;
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.BoundValueOperations;
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.util.concurrent.TimeUnit;

/**
 * @author eddie.lee
 */
@Aspect
@Component
public class RejectReqSubAspect2 {

    /**
     * 请求时候带过来 </p>
     * 比如: "token: 123456"
     */
    private static String REQUEST_HEADER = "token";

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * @execution:用于匹配方法执行的连接点
     * @annotation:用于匹配当前执行方法持有指定注解的方法
     */
    @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)" +
            "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)" +
            "|| @annotation(org.springframework.web.bind.annotation.PutMapping)")
    public void annotationPointCut() {
    }

    @Around("annotationPointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Object proceed = joinPoint.proceed();
        // 获取 req 对象
        ServletRequestAttributes attributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
        HttpServletRequest request = attributes == null ? null : attributes.getRequest();
        // 如果不是有效的api接口请求则不处理
        if (null == request) {
            proceed = joinPoint.proceed(joinPoint.getArgs());
            return proceed;
        }
        // 获取传入属性
        String token = request.getHeader(REQUEST_HEADER);
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // 先获取redis对value的操作对象,需要先设定key
        // 使用.set(key,value)方法进行设置,get(key)方法用来获取
        BoundValueOperations redis = redisTemplate.boundValueOps(token + request.getRequestURI());
        // 获取字符串所有值
        if (redis.get() != null) {
//            throw new SecurityException("你已经按过好多次了哦~~ 不如休息下?");
            System.err.println("你已经按过好多次了哦~~ 不如休息下?");
        }
        long timeout = 5;
        // 设置value的超时时间,timeout为数字,unit为单位,例如天,小时等
        redis.set("eddie", timeout, TimeUnit.SECONDS);
        System.out.println("redis:" + redis.get());
        redisDelete(token, request);
        return proceed;
    }

    private void redisDelete(String token, HttpServletRequest request) {
        redisTemplate.delete(token + request.getRequestURI());
    }
}

2.2 单元测试

import com.alibaba.fastjson.JSONObject;
import com.itmuch.UserCenterApplication;
import com.itmuch.usercenter.dao.user.UserMapper;
import com.itmuch.usercenter.domain.entity.user.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.util.concurrent.CountDownLatch;

import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = UserCenterApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserCenterApplicationTests {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testPostUser() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(100);
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 10; j++) {
                    // 设置头信息
                    HttpHeaders headers = new HttpHeaders();
                    headers.setContentType(MediaType.APPLICATION_JSON);
                    headers.set("token","eddie");

                    // 懒的一个一个set,直接查库
                    User user = userMapper.selectByPrimaryKey(1);
                    user.setId(null);

                    ResponseEntity responseEntity = restTemplate.exchange("/test-add", HttpMethod.POST, new HttpEntity<Object>(user), JSONObject.class);
                    System.out.println(Thread.currentThread().getName() + ": "+ responseEntity.getBody());
                    assertThat(HttpStatus.OK, equalTo(responseEntity.getStatusCode()));
                    countDownLatch.countDown();
                }
            }).start();
        }
        countDownLatch.await();
    }

}

图片.png


总结

以上都是Redis + AOP 实现的,还有很多种代替Redis的方式:

  • zk锁
  • Redisson锁 (也是Redis,只是利用锁)
  • ConcurrentHashMap

Mark 或许你感觉这样子多次一举,怎么不利用数据库的唯一性。 But 那么你是做接口的一致性还是业务数据的一致性?


# AOP # Spring