⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 cnblogs.com/youzhibing/p/15354706.html 「青石路」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

需求

线上出现的问题是,一些非核心的查询数据业务,在请求超时或者错误的时候,用户会越查询,导致数据库cup飙升,拖垮核心的业务。

领导让我做三件事,一是把这些接口做一个限流,这些限流参数是可配的,第二是这些接口可以设置开关,当发现问题时,可以手动关闭这些接口,不至于数据库压力过大影响核心业务的服务。第三是做接口的熔断,熔断设置可以配置。

经过确定,前两个实现用redis来实现,第三个因为熔断讨论觉得比较复杂,决定采用我提出的用Hystrix,目前项目不能热加载生效配置中心的最新的配置,所以后期推荐使用Archaius,这些网上查到的,具体为啥不选其他的,原因就是其他的比较复杂,上手感觉这个最快。

这篇文章说实现,其他问题不涉及,请多多指教。

思路

接口的屏蔽:通过AOP实现,每次访问接口的时候,通过接口的Key值,在Redis取到接口设置开关值,如果打开继续,否在拒绝。接口限流也是基于AOP,根据接口的Key值,取到这个接口的限流值,表示多长时间,限流几次,每次访问都会请求加一,通过比较,如果超过限制再返回,否在继续。

代码

img

AccessLimiter接口,主要有两类方法,是否开启限流,取Redis中的限流值

package com.hcfc.auto.util.limit;

import java.util.concurrent.TimeUnit;

/**
* @创建人 peng.wang
* @描述 访问限制器
*/
public interface AccessLimiter {
/**
* 检查指定的key是否收到访问限制
* @param key 限制接口的标识
* @param times 访问次数
* @param per 一段时间
* @param unit 时间单位
* @return
*/
public boolean isLimited(String key, long times, long per, TimeUnit unit);

/**
* 移除访问限制
* @param key
*/
public void refreshLimited(String key);

/**
* 接口是否打开
* @return
*/
public boolean isStatus(String redisKey);

/**
* 接口的限流大小
* @param redisKeyTimes
* @return
*/
public long getTimes(String redisKeyTimes);

/**
* 接口限流时间段
* @param redisKeyPer
* @return
*/
public long getPer(String redisKeyPer);

/**
* 接口的限流时间单位
* @param redisKeyUnit
* @return
*/
public TimeUnit getUnit(String redisKeyUnit);

/**
* 是否删除接口限流
* @param redisKeyIsRefresh
* @return
*/
public boolean getIsRefresh(String redisKeyIsRefresh);
}

RedisAccessLimiter是AccessLimiter接口的实现类

package com.hcfc.auto.util.limit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
* @创建人 peng.wang
* @描述 基于Redis的实现
*/
@Component
public class RedisAccessLimiter implements AccessLimiter {

private static final Logger LOGGER = LoggerFactory.getLogger(RedisAccessLimiter.class);

@Autowired
private RedisTemplate redisTemplate;

@Override
public boolean isLimited(String key, long times, long per, TimeUnit unit) {
Long curTimes = redisTemplate.boundValueOps(key).increment(1);
LOGGER.info("curTimes {}",curTimes);
if(curTimes > times) {
LOGGER.debug("超频访问:[{}]",key);
return true;
} else {
if(curTimes == 1) {
LOGGER.info(" set expire ");
redisTemplate.boundValueOps(key).expire(per, unit);
return false;
} else {
return false;
}
}
}

@Override
public void refreshLimited(String key) {
redisTemplate.delete(key);
}

@Override
public boolean isStatus(String redisKey) {
try {
return (boolean)redisTemplate.opsForValue().get(redisKey+"IsOn");
}catch (Exception e){
LOGGER.info("redisKey is not find or type mismatch, key: ", redisKey);
return false;
}
}

@Override
public long getTimes(String redisKeyTimes) {
try {
return (long)redisTemplate.opsForValue().get(redisKeyTimes+"Times");
}catch (Exception e){
LOGGER.info("redisKey is not find or type mismatch, key: ", redisKeyTimes);
return 0;
}
}

@Override
public long getPer(String redisKeyPer) {
try {
return (long)redisTemplate.opsForValue().get(redisKeyPer+"Per");
}catch (Exception e){
LOGGER.info("redisKey is not find or type mismatch, key: ", redisKeyPer);
return 0;
}
}

@Override
public TimeUnit getUnit(String redisKeyUnit) {
try {
return (TimeUnit) redisTemplate.opsForValue().get(redisKeyUnit+"Unit");
}catch (Exception e){
LOGGER.info("redisKey is not find or type mismatch, key: ", redisKeyUnit);
return TimeUnit.SECONDS;
}
}

@Override
public boolean getIsRefresh(String redisKeyIsRefresh) {
try {
return (boolean)redisTemplate.opsForValue().get(redisKeyIsRefresh+"IsRefresh");
}catch (Exception e){
LOGGER.info("redisKey is not find or type mismatch, key: ", redisKeyIsRefresh);
return false;
}
}
}

Limit标签接口,实现注解方式

package com.hcfc.auto.util.limit;

import java.lang.annotation.*;

/**
* @创建人 peng.wang
* @描述
*/
@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Limit {}

LimitAspect 切面的实现,实现接口屏蔽和限流的逻辑

package com.hcfc.auto.util.limit;

import com.hcfc.auto.vo.response.ResponseDto;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
* @创建人 peng.wang
* @创建时间 2019/10/11
* @描述
*/
@Slf4j
@Aspect
@Component
public class LimitAspect {
private static final Logger logger = LoggerFactory.getLogger(LimitAspect.class);

@Autowired
private AccessLimiter limiter;

@Autowired
GenerateRedisKey generateRedisKey;

@Pointcut("@annotation(com.hcfc.auto.util.limit.Limit)")
public void limitPointcut() {}

@Around("limitPointcut()")
public Object doArround(ProceedingJoinPoint joinPoint) throws Throwable {
String redisKey = generateRedisKey.getMethodUrlConvertRedisKey(joinPoint);
long per = limiter.getPer(redisKey);
long times = limiter.getTimes(redisKey);
TimeUnit unit = limiter.getUnit(redisKey);
boolean isRefresh =limiter.getIsRefresh(redisKey);
boolean methodLimitStatus = limiter.isStatus(redisKey);
String bindingKey = genBindingKey(joinPoint);
if (methodLimitStatus) {
logger.info("method is closed, key: ", bindingKey);
return ResponseDto.fail("40007", "method is closed, key:"+bindingKey);
//throw new OverLimitException("method is closed, key: "+bindingKey);
}
if(bindingKey !=null){
boolean isLimited = limiter.isLimited(bindingKey, times, per, unit);
if(isLimited){
logger.info("limit takes effect: {}", bindingKey);
return ResponseDto.fail("40006", "access over limit, key: "+bindingKey);
//throw new OverLimitException("access over limit, key: "+bindingKey);
}
}
Object result = null;
result = joinPoint.proceed();
if(bindingKey!=null && isRefresh) {
limiter.refreshLimited(bindingKey);
logger.info("limit refreshed: {}", bindingKey);
}
return result;
}

private String genBindingKey(ProceedingJoinPoint joinPoint){
try{
Method m = ((MethodSignature) joinPoint.getSignature()).getMethod();
return joinPoint.getTarget().getClass().getName() + "." + m.getName();
}catch (Throwable e){
return null;
}
}
}

还有一个不重要的RedisKey实现类GenerateRedisKey和一个错误封装类,目前没有使用到,使用项目中其他的错误封装类了

GenerateRedisKey

package com.hcfc.auto.util.limit;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;

import java.lang.reflect.Method;

/**
* @创建人 peng.wang
* @描述
*/
@Component
public class GenerateRedisKey {

public String getMethodUrlConvertRedisKey(ProceedingJoinPoint joinPoint){
StringBuilder redisKey =new StringBuilder("");
Method m = ((MethodSignature)joinPoint.getSignature()).getMethod();
RequestMapping methodAnnotation = m.getAnnotation(RequestMapping.class);
if (methodAnnotation != null) {
String[] methodValue = methodAnnotation.value();
String dscUrl = diagonalLineToCamel(methodValue[0]);
return redisKey.append("RSK:").append("interfaceIsOpen:").append(dscUrl).toString();
}
return redisKey.toString();
}
private String diagonalLineToCamel(String param){
char UNDERLINE='/';
if (param==null||"".equals(param.trim())){
return "";
}
int len=param.length();
StringBuilder sb=new StringBuilder(len);
for (int i = 1; i < len; i++) {
char c=param.charAt(i);
if (c==UNDERLINE){
if (++i<len){
sb.append(Character.toUpperCase(param.charAt(i)));
}
}else{
sb.append(c);
}
}
return sb.toString();
}
}

总结

关键的代码也就这几行,访问之前,对这个key值加一的操作,判断是否超过限制,如果等于一这个key加一之后的值为一,说明之前不存在,则设置这个key,放在Redis数据库中。

其实有更成熟的方案就是谷歌的Guava,领导说现在是咱们是分布式,不支持,还是用Redis实现吧,目前就这样实现了。其实我是新来的,好多东西还不太明白,很多决定都是上面决定的,我只是尽力实现罢了。不足之处,请多多指教!

img

文章目录
  1. 1. 需求
  2. 2. 思路
  3. 3. 代码
  4. 4. 总结