需求
線上出現(xiàn)的問題是,一些非核心的查詢數(shù)據(jù)業(yè)務(wù),在請求超時(shí)或者錯(cuò)誤的時(shí)候,用戶會越查詢,導(dǎo)致數(shù)據(jù)庫cup飆升,拖垮核心的業(yè)務(wù)。
領(lǐng)導(dǎo)讓我做三件事,一是把這些接口做一個(gè)限流,這些限流參數(shù)是可配的,第二是這些接口可以設(shè)置開關(guān),當(dāng)發(fā)現(xiàn)問題時(shí),可以手動關(guān)閉這些接口,不至于數(shù)據(jù)庫壓力過大影響核心業(yè)務(wù)的服務(wù)。第三是做接口的熔斷,熔斷設(shè)置可以配置。
經(jīng)過確定,前兩個(gè)實(shí)現(xiàn)用redis來實(shí)現(xiàn),第三個(gè)因?yàn)槿蹟嘤懻撚X得比較復(fù)雜,決定采用我提出的用Hystrix,目前項(xiàng)目不能熱加載生效配置中心的最新的配置,所以后期推薦使用Archaius,這些網(wǎng)上查到的,具體為啥不選其他的,原因就是其他的比較復(fù)雜,上手感覺這個(gè)最快。
這篇文章說實(shí)現(xiàn),其他問題不涉及,請多多指教。
思路
接口的屏蔽:通過AOP實(shí)現(xiàn),每次訪問接口的時(shí)候,通過接口的Key值,在Redis取到接口設(shè)置開關(guān)值,如果打開繼續(xù),否在拒絕。接口限流也是基于AOP,根據(jù)接口的Key值,取到這個(gè)接口的限流值,表示多長時(shí)間,限流幾次,每次訪問都會請求加一,通過比較,如果超過限制再返回,否在繼續(xù)。
代碼
AccessLimiter接口,主要有兩類方法,是否開啟限流,取Redis中的限流值。
package com.hcfc.auto.util.limit;
import JAVA.util.concurrent.TimeUnit;
/**
* @創(chuàng)建人 peng.wang
* @描述 訪問限制器
*/
public interface AccessLimiter {
/**
* 檢查指定的key是否收到訪問限制
* @param key 限制接口的標(biāo)識
* @param times 訪問次數(shù)
* @param per 一段時(shí)間
* @param unit 時(shí)間單位
* @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);
/**
* 接口限流時(shí)間段
* @param redisKeyPer
* @return
*/
public long getPer(String redisKeyPer);
/**
* 接口的限流時(shí)間單位
* @param redisKeyUnit
* @return
*/
public TimeUnit getUnit(String redisKeyUnit);
/**
* 是否刪除接口限流
* @param redisKeyIsRefresh
* @return
*/
public boolean getIsRefresh(String redisKeyIsRefresh);
}
RedisAccessLimiter是AccessLimiter接口的實(shí)現(xiàn)類
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;
/**
* @創(chuàng)建人 peng.wang
* @描述 基于Redis的實(shí)現(xiàn)
*/
@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標(biāo)簽接口,實(shí)現(xiàn)注解方式
package com.hcfc.auto.util.limit;
import java.lang.annotation.*;
/**
* @創(chuàng)建人 peng.wang
* @描述
*/
@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Limit {}
LimitAspect 切面的實(shí)現(xiàn),實(shí)現(xiàn)接口屏蔽和限流的邏輯
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;
/**
* @創(chuàng)建人 peng.wang
* @創(chuàng)建時(shí)間 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;
}
}
}
還有一個(gè)不重要的RedisKey實(shí)現(xiàn)類GenerateRedisKey和一個(gè)錯(cuò)誤封裝類,目前沒有使用到,使用項(xiàng)目中其他的錯(cuò)誤封裝類了。
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;
/**
* @創(chuàng)建人 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();
}
}
總結(jié)
關(guān)鍵的代碼也就這幾行,訪問之前,對這個(gè)key值加一的操作,判斷是否超過限制,如果等于一這個(gè)key加一之后的值為一,說明之前不存在,則設(shè)置這個(gè)key,放在Redis數(shù)據(jù)庫中。
其實(shí)有更成熟的方案就是谷歌的Guava,領(lǐng)導(dǎo)說現(xiàn)在是咱們是分布式,不支持,還是用Redis實(shí)現(xiàn)吧,目前就這樣實(shí)現(xiàn)了。其實(shí)我是新來的,好多東西還不太明白,很多決定都是上面決定的,我只是盡力實(shí)現(xiàn)罷了。不足之處,請多多指教!
作者:Ingram--MSN
來源:
blog.csdn.net/u010843114/article/details/102695570