springboot接口限流
前言
本文使用到的技术有springboot springaop redis。
提示:以下是本篇文章正文内容,下面案例可供参考
一、引入aop以及redis依赖
<!--包含 spring-aop 和 AspectJ 来支持面向切面编程(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>
二、自定义注解
自定义注解 当然如果所有接口都要进行限流的话就直接使用aop不需要自定义注解
/**
* @author 宋中能
* 用分布式锁实现接口幂等性校验
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IdempotenceAnnotation {
/**
* key类型1user 2ip
* @return
*/
int type() default 1;
/**
* 过期时间
* @return
*/
long expiration() default 1000L;
}
自定义aop
/**
* @author szn
*/
@Aspect
@Component
//@Transactional
public class IdempotenceAnnotationImpl {
private static final Logger logger= LoggerFactory.getLogger(RedisLock.class);
@Resource
private RedisTemplate<String,Object> redisTemplate;
/**
* 定义切入点为添加了自定如注解的方法
*/
@Pointcut("@annotation(com.wenwei.sharepark.annotation.IdempotenceAnnotation)")
public void idempotence() {
}
/**
* 在方法执行之前使用redis分布式锁进行接口等幂性约束
* @param joinPoint
*/
@Around("idempotence()")
public Object redisLock(ProceedingJoinPoint joinPoint) throws Throwable {
//从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取切入点所在的方法
Method method = signature.getMethod();
StringBuffer lockKey=new StringBuffer();
IdempotenceAnnotation idempotenceAnnotation=method.getAnnotation(IdempotenceAnnotation.class);
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
.getRequestAttributes()).getRequest();
//使用username
if(idempotenceAnnotation.type()==1){
JwtUser jwtUser=(JwtUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
lockKey.append(jwtUser.getUsername());
}else {
//使用ip
lockKey.append(IpUtils.ipAddress(request));
}
lockKey.append("_");
//方法名称
lockKey.append(method.getName());
lockKey.append("_idempotence");
logger.info("redis分布式锁Key:"+lockKey.toString());
//创建redis锁对象
RedisLock redisLock=new RedisLock(redisTemplate,lockKey.toString(),false,idempotenceAnnotation.expiration());
Object result;
//获取锁
if(redisLock.lockNoRetry()){
try{
//执行方法
result=joinPoint.proceed();
} finally{
//释放锁
redisLock.unlock();
}
}else {
throw new ObjectException(250,"请勿重复提交!");
}
return result;
}
}
rdis分布式锁工具类这里只是用了lockNoRetry()这个方法 另一个lockRetry()方法可以用来做商品抢购功能
**
* @author : szn
* Description : redisLock 并发锁工具类,提供了加锁和释放锁的方法,使用方法参考RedisTest.redisLock()方法
*/
public class RedisLock {
private static final Logger logger=LoggerFactory.getLogger(RedisLock.class);
private RedisTemplate<String, Object> redisTemplate;
/**
* 重试时间
*/
private static final long DEFAULT_ACQUIRY_RETRY_MILLIS = 100L;
/**
* 锁的后缀
*/
private static final String LOCK_SUFFIX = "_redis_lock";
/**
* 锁的key
*/
private String lockKey;
/**
* 锁超时时间,防止线程在入锁以后,防止阻塞后面的线程无法获取锁
*/
private long expireMsecs = 60L * 1000L;
/**
* 线程获取锁的等待时间
*/
private long timeoutMsecs = 10L * 1000L;
/**
* 是否锁定标志
*/
private volatile boolean locked = false;
/**
* 构造器
*
* @param isAddUser 是否关联用户
* @param lockKey 锁的key
*/
public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockKey, Boolean isAddUser) {
this.redisTemplate = redisTemplate;
this.lockKey = lockKey + LOCK_SUFFIX;
}
/**
* 构造器
*
* @param redisTemplate
* @param lockKey 锁的key
* @param timeoutMsecs 获取锁的超时时间
*/
public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockKey, Boolean isAddUser, long timeoutMsecs) {
this(redisTemplate, lockKey, isAddUser);
this.timeoutMsecs = timeoutMsecs;
}
/**
* 构造器
*
* @param redisTemplate
* @param lockKey 锁的key
* @param timeoutMsecs 获取锁的超时时间
* @param expireMsecs 锁的有效期
*/
public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockKey, Boolean isAddUser, long timeoutMsecs, long expireMsecs) {
this(redisTemplate, lockKey, isAddUser, timeoutMsecs);
this.expireMsecs = expireMsecs;
}
public String getLockKey() {
return lockKey;
}
/**
* 获取值
*
* @param key
* @return
*/
private String get(final String key) {
Object obj = redisTemplate.opsForValue().get(key);
return obj != null ? obj.toString() : null;
}
/**
* 存入值
*
* @param key
* @param value
* @return
*/
private boolean setNX(final String key, final String value) {
return redisTemplate.opsForValue().setIfAbsent(key, value);
}
/**
* 设置值并返回旧值
*
* @param key
* @param value
* @return
*/
private String getSet(final String key, final String value) {
Object obj = redisTemplate.opsForValue().getAndSet(key, value);
return obj != null ? (String) obj : null;
}
/**
* 获取锁,获取失败的话过100ms重试,总超时时间 10 * 1000 ms
*
* @return 获取锁成功返回ture,超时返回false
* @throws InterruptedException
*/
public boolean lockRetry() {
long timeout = timeoutMsecs;
while (timeout >= 0) {
if(this.lock()){
return true;
}
timeout -= DEFAULT_ACQUIRY_RETRY_MILLIS;
// 延时
try {
Thread.sleep(DEFAULT_ACQUIRY_RETRY_MILLIS);
} catch (InterruptedException e) {
logger.error("延时出错",e);
}
}
return false;
}
/**
* 获取锁,不重试,只获取一次,获取不到就返回失败
*
* @return 获取锁成功返回ture,失败返回false
* @throws InterruptedException
*/
public boolean lockNoRetry() {
return this.lock();
}
/**
* 获取锁公共方法
* @return
*/
private boolean lock(){
long expires = System.currentTimeMillis() + expireMsecs + 1;
// 锁到期时间
String expiresStr = String.valueOf(expires);
if (this.setNX(lockKey, expiresStr)) {
locked = true;
return true;
}
// redis里key的时间
String currentValue = this.get(lockKey);
// 判断锁是否已经过期,过期则重新设置并获取
if (currentValue != null && Long.parseLong(currentValue) < System.currentTimeMillis()) {
// 设置锁并返回旧值
String oldValue = this.getSet(lockKey, expiresStr);
// 比较锁的时间,如果不一致则可能是其他锁已经修改了值并获取
if (oldValue != null && oldValue.equals(currentValue)) {
locked = true;
return true;
}
}
return false;
}
/**
* 释放获取到的锁
*/
public synchronized void unlock() {
if (locked) {
redisTemplate.delete(lockKey);
locked = false;
}
}
}
在接口上添加注解
//因为设置了默认值所以不用设置参数就会使用默认值
@IdempotenceAnnotation
@ApiOperation(value = "收藏和取消收藏", notes = "参数>parkId")
@PostMapping(value = "/app/favoriteAndUnFavorite")
public ObjectResult<Object> favoriteAndUnFavorite(@RequestBody SpUserCollectionPark userCollectionPark) {
userCollectionPark.setUserId(getCurrentUserId());
spUserCollectionParkService.favoriteAndUnFavorite(userCollectionPark);
return ObjectResultUtil.success("操作成功");
}
三、效果
快速点击执行
控制台输出
过期时间和rediskey的生成方式都可以自行定义由于接口执行时间不足一秒所以还没有到一秒钟 就已经释放锁了 连续点击下还是成功了几个。如果需要所有接口都进行校验就不需要自定义注解 修改aop的切入点就好了
/**
* 定义切入点为添加了自定如注解的方法
*/
// 注解方式切入@Pointcut("@annotation(com.wenwei.sharepark.annotation.IdempotenceAnnotation)")
//请自行根据路径匹配
@Pointcut("execution(public * com.wenwei.sharepark.controller.*..*(..))")
public void idempotence() {
}
有什么不懂可以在评论区探讨或者私信我