需求
線上出現的問題是,一些非核心的查詢數據業務,在請求超時或者錯誤的時候,用戶會越查詢,導致數據庫cup飆升,拖垮核心的業務。
領導讓我做三件事,一是把這些接口做一個限流,這些限流參數是可配的,第二是這些接口可以設置開關,當發現問題時,可以手動關閉這些接口,不至于數據庫壓力過大影響核心業務的服務。第三是做接口的熔斷,熔斷設置可以配置。
經過確定,前兩個實現用redis來實現,第三個因為熔斷討論覺得比較復雜,決定采用我提出的用Hystrix,目前項目不能熱加載生效配置中心的最新的配置,所以后期推薦使用Archaius,這些網上查到的,具體為啥不選其他的,原因就是其他的比較復雜,上手感覺這個最快。
這篇文章說實現,其他問題不涉及,請多多指教。
思路
接口的屏蔽:通過AOP實現,每次訪問接口的時候,通過接口的Key值,在Redis取到接口設置開關值,如果打開繼續,否在拒絕。接口限流也是基于AOP,根據接口的Key值,取到這個接口的限流值,表示多長時間,限流幾次,每次訪問都會請求加一,通過比較,如果超過限制再返回,否在繼續。
代碼
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實現吧,目前就這樣實現了。其實我是新來的,好多東西還不太明白,很多決定都是上面決定的,我只是盡力實現罷了。不足之處,請多多指教!
作者:Ingram--MSN
來源:
blog.csdn.net/u010843114/article/details/102695570