目录
两套通过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();
}
}
(二)通过指定操作类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();
}
}
总结
以上都是Redis + AOP 实现的,还有很多种代替Redis的方式:
- zk锁
- Redisson锁 (也是Redis,只是利用锁)
- ConcurrentHashMap
Mark 或许你感觉这样子多次一举,怎么不利用数据库的唯一性。 But 那么你是做接口的一致性还是业务数据的一致性?