???個(gè)人主頁(yè):牽著貓散步的鼠鼠?
???系列專欄:Java全棧-專欄
???個(gè)人學(xué)習(xí)筆記,若有缺誤,歡迎評(píng)論區(qū)指正?
前些天發(fā)現(xiàn)了一個(gè)巨牛的人工智能學(xué)習(xí)網(wǎng)站,通俗易懂,風(fēng)趣幽默,忍不住分享一下給大家。點(diǎn)擊跳轉(zhuǎn)到網(wǎng)站AI學(xué)習(xí)網(wǎng)站。
目錄
前言
1.導(dǎo)入Redisson
引入依賴
編寫(xiě)配置
聲明Redisson客戶端Bean
2.自定義注解
3.AOP切面編程
導(dǎo)入依賴
編寫(xiě)AOP限流代碼
4.接口使用自定義注解實(shí)現(xiàn)限流
使用自定義限流注解
綁定限流回調(diào)函數(shù)
總結(jié)
前言
在開(kāi)發(fā)高并發(fā)系統(tǒng)時(shí)有三把利器用來(lái)保護(hù)系統(tǒng):緩存、降級(jí)和限流。??限流的目的是通過(guò)對(duì)并發(fā)訪問(wèn)請(qǐng)求進(jìn)行限速或者一個(gè)時(shí)間窗口內(nèi)的的請(qǐng)求數(shù)量進(jìn)行限速來(lái)保護(hù)系統(tǒng),一旦達(dá)到限制速率則可以拒絕服務(wù)、排隊(duì)或等待
我們上次講解了如何使用Sentinel來(lái)實(shí)現(xiàn)服務(wù)限流,今天我們來(lái)講解下如何使用Redisson+AOP+自定義注解+反射優(yōu)雅的實(shí)現(xiàn)服務(wù)限流,本文講解的限流實(shí)現(xiàn)支持針對(duì)用戶IP限流,整個(gè)接口的訪問(wèn)限流,以及對(duì)某個(gè)參數(shù)字段的限流,并且支持請(qǐng)求限流后處理回調(diào)
1.導(dǎo)入Redisson
引入依賴
我們首先導(dǎo)入Redisson所需要的依賴,我們這里的springboot版本為2.7.12
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.4</version>
</dependency>
編寫(xiě)配置
# Redisson客戶端
redis:
sdk:
config:
host: redis服務(wù)IP
port: 6379
password: redis密碼,沒(méi)有可刪掉這行
pool-size: 10
min-idle-size: 5
idle-timeout: 30000
connect-timeout: 5000
retry-attempts: 3
retry-interval: 1000
ping-interval: 60000
keep-alive: true
聲明Redisson客戶端Bean
配置映射類(lèi)RedisCientConfigProperties
@Data
@ConfigurationProperties(prefix = "redis.sdk.config", ignoreInvalidFields = true)
public class RedisCientConfigProperties {
/** host:ip */
private String host;
/** 端口 */
private int port;
/** 賬密 */
private String password;
/** 設(shè)置連接池的大小,默認(rèn)為64 */
private int poolSize = 64;
/** 設(shè)置連接池的最小空閑連接數(shù),默認(rèn)為10 */
private int minIdleSize = 10;
/** 設(shè)置連接的最大空閑時(shí)間(單位:毫秒),超過(guò)該時(shí)間的空閑連接將被關(guān)閉,默認(rèn)為10000 */
private int idleTimeout = 10000;
/** 設(shè)置連接超時(shí)時(shí)間(單位:毫秒),默認(rèn)為10000 */
private int connectTimeout = 10000;
/** 設(shè)置連接重試次數(shù),默認(rèn)為3 */
private int retryAttempts = 3;
/** 設(shè)置連接重試的間隔時(shí)間(單位:毫秒),默認(rèn)為1000 */
private int retryInterval = 1000;
/** 設(shè)置定期檢查連接是否可用的時(shí)間間隔(單位:毫秒),默認(rèn)為0,表示不進(jìn)行定期檢查 */
private int pingInterval = 0;
/** 設(shè)置是否保持長(zhǎng)連接,默認(rèn)為true */
private boolean keepAlive = true;
}
Configuration
@EnableConfigurationProperties(RedisCientConfigProperties.class)
public class RedisClientConfig {
@Bean("redissonClient")
public RedissonClient redissonClient(ConfigurableApplicationContext applicationContext, RedisCientConfigProperties properties) {
Config config = new Config();
// 根據(jù)需要可以設(shè)定編解碼器;https://github.com/redisson/redisson/wiki/4.-%E6%95%B0%E6%8D%AE%E5%BA%8F%E5%88%97%E5%8C%96
// config.setCodec(new RedisCodec());
config.useSingleServer()
.setAddress("redis://" + properties.getHost() + ":" + properties.getPort())
.setPassword(properties.getPassword())
.setConnectionPoolSize(properties.getPoolSize())
.setConnectionMinimumIdleSize(properties.getMinIdleSize())
.setIdleConnectionTimeout(properties.getIdleTimeout())
.setConnectTimeout(properties.getConnectTimeout())
.setRetryAttempts(properties.getRetryAttempts())
.setRetryInterval(properties.getRetryInterval())
.setPingConnectionInterval(properties.getPingInterval())
.setKeepAlive(properties.isKeepAlive())
;
RedissonClient redissonClient = Redisson.create(config);
// 注冊(cè)消息發(fā)布訂閱主題Topic
// 找到所有實(shí)現(xiàn)了Redisson中MessageListener接口的bean名字
String[] beanNamesForType = applicationContext.getBeanNamesForType(MessageListener.class);
for (String beanName : beanNamesForType) {
// 通過(guò)bean名字獲取到監(jiān)聽(tīng)bean
MessageListener bean = applicationContext.getBean(beanName, MessageListener.class);
Class<? extends MessageListener> beanClass = bean.getClass();
// 如果bean的注解里包含我們的自定義注解RedisTopic.class,則以RedisTopic注解的值作為name將該bean注冊(cè)到bean工廠,方便在別處注入
if (beanClass.isAnnotationPresent(RedisTopic.class)) {
RedisTopic redisTopic = beanClass.getAnnotation(RedisTopic.class);
RTopic topic = redissonClient.getTopic(redisTopic.topic());
topic.addListener(String.class, bean);
ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
beanFactory.registerSingleton(redisTopic.topic(), topic);
}
}
return redissonClient;
}
static class RedisCodec extends BaseCodec {
private final Encoder encoder = in -> {
ByteBuf out = ByteBufAllocator.DEFAULT.buffer();
try {
ByteBufOutputStream os = new ByteBufOutputStream(out);
JSON.writeJSONString(os, in, SerializerFeature.WriteClassName);
return os.buffer();
} catch (IOException e) {
out.release();
throw e;
} catch (Exception e) {
out.release();
throw new IOException(e);
}
};
private final Decoder<Object> decoder = (buf, state) -> JSON.parseObject(new ByteBufInputStream(buf), Object.class);
@Override
public Decoder<Object> getValueDecoder() {
return decoder;
}
@Override
public Encoder getValueEncoder() {
return encoder;
}
}
}
2.自定義注解
我們這里自定義一個(gè)注解來(lái)作為后續(xù)AOP切面編程的切點(diǎn)
根據(jù)注解Key屬性的值,我們會(huì)有如下情況
all:針對(duì)整個(gè)接口限流
request_ip:針對(duì)各個(gè)用戶的訪問(wèn)IP限流
其他str:根據(jù)參數(shù)作為標(biāo)識(shí)符限流,比如我這里key=userid,那么我會(huì)根據(jù)參數(shù)中的userid來(lái)限流
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface AccessInterceptor {
/** 用哪個(gè)字段作為攔截標(biāo)識(shí)符,配置all則是對(duì)整個(gè)接口限流,配置request_ip,
* 則是對(duì)訪問(wèn)ip限流,配置其他str,則會(huì)到參數(shù)中尋找對(duì)應(yīng)名稱的屬性值(包括對(duì)象內(nèi)部屬性) */
String key() default "all";
/** 限制頻次(每秒請(qǐng)求次數(shù)) */
long permitsPerSecond();
/** 黑名單攔截(多少次限制后加入黑名單)0 不限制 */
double blacklistCount() default 0;
/** 攔截后的執(zhí)行方法 */
String fallbackMethod();
}
3.AOP切面編程
導(dǎo)入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
編寫(xiě)AOP限流代碼
我們doRouter切面函數(shù)以AccessInterceptor注解為切點(diǎn),根據(jù)注解的各類(lèi)配置來(lái)執(zhí)行整個(gè)限流過(guò)程。
我們通過(guò)使用Redisson的RRateLimiter限流器,基于令牌桶實(shí)現(xiàn)訪問(wèn)限流,并對(duì)已經(jīng)限流的訪問(wèn)記錄黑名單次數(shù),超過(guò)設(shè)置的黑名單閾值就會(huì)被加入黑名單中,較長(zhǎng)時(shí)間無(wú)法訪問(wèn)
代碼較長(zhǎng),為了縮短篇幅就一次性放上來(lái)了,各處已經(jīng)打上了詳細(xì)注解,若有疑問(wèn)可評(píng)論區(qū)留言。
@Slf4j
@Aspect
public class RateLimiterAOP {
// 注入我們聲明的redisson客戶端
@Resource
private RedissonClient redissonClient;
// 限流RateLimiter緩存前綴
private static final String rateLimiterName = "test:RateLimiter:";
// 黑名單原子計(jì)數(shù)器緩存前綴
private static final String blacklistPrefix = "test:RateBlockList:";
@Around("@annotation(accessInterceptor)")
public Object doRouter(ProceedingJoinPoint jp, AccessInterceptor accessInterceptor) throws Throwable {
// 獲取注解配置的字段key
String key = accessInterceptor.key();
if (StringUtils.isBlank(key)) {
log.error("限流RateLimiter注解中的 Key 屬性為空!");
throw new RuntimeException("RateLimiter注解中的 Key 屬性為空!");
}
log.info("限流攔截關(guān)鍵字為 {}", key);
// 根據(jù)key獲取攔截標(biāo)識(shí)符字段
String keyAttr = getAttrValue(key, jp.getArgs());
// 黑名單攔截,非法訪問(wèn)次數(shù)超過(guò)黑名單閾值
if (!"all".equals(keyAttr) && accessInterceptor.blacklistCount() != 0 && redissonClient.getAtomicLong(blacklistPrefix + keyAttr).get() > accessInterceptor.blacklistCount()) {
log.info("限流-黑名單攔截:{}", keyAttr);
return fallbackMethodResult(jp, accessInterceptor.fallbackMethod());
}
// 獲取限流器 -> Redisson RRateLimiter
RRateLimiter rateLimiter = redissonClient.getRateLimiter(rateLimiterName + keyAttr);
if (!rateLimiter.isExists()) {
// 創(chuàng)建令牌桶數(shù)據(jù)模型,單位時(shí)間內(nèi)產(chǎn)生多少令牌
rateLimiter.trySetRate(RateType.PER_CLIENT,1, accessInterceptor.permitsPerSecond(), RateIntervalUnit.MINUTES);
}
// 限流判斷,沒(méi)有獲取到令牌,超出頻率
if (!rateLimiter.tryAcquire()) {
// 如果開(kāi)啟了黑名單限制,那么就記錄當(dāng)前的非法訪問(wèn)次數(shù)
if (accessInterceptor.blacklistCount() != 0) {
RAtomicLong atomicLong = redissonClient.getAtomicLong(blacklistPrefix + keyAttr);
atomicLong.incrementAndGet(); // 原子自增
atomicLong.expire(24, TimeUnit.HOURS); // 刷新黑名單原子計(jì)數(shù)器器過(guò)期時(shí)間為24小時(shí)
}
log.info("限流-頻率過(guò)高攔截:{}", keyAttr);
return fallbackMethodResult(jp, accessInterceptor.fallbackMethod());
}
// 返回結(jié)果
return jp.proceed();
}
/**
* 調(diào)用用戶配置的回調(diào)方法,使用反射機(jī)制實(shí)現(xiàn)。
*/
private Object fallbackMethodResult(JoinPoint jp, String fallbackMethod) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 通過(guò)JoinPoint對(duì)象獲取方法的簽名(Signature)
Signature sig = jp.getSignature();
// 將方法簽名轉(zhuǎn)換為MethodSignature對(duì)象,以便獲取方法的詳細(xì)信息
MethodSignature methodSignature = (MethodSignature) sig;
// 獲取到具體的方法對(duì)象,通過(guò)方法名和參數(shù)(所以回調(diào)函數(shù)參數(shù)一定要和原方法一致)
Method method = jp.getTarget().getClass().getMethod(fallbackMethod, methodSignature.getParameterTypes());
// 調(diào)用目標(biāo)對(duì)象的方法,并傳入當(dāng)前對(duì)象(jp.getThis())和方法的參數(shù)(jp.getArgs())。
return method.invoke(jp.getThis(), jp.getArgs());
}
/**
* 根據(jù)JoinPoint對(duì)象獲取其所代表的方法對(duì)象
*/
private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
Signature sig = jp.getSignature();
MethodSignature methodSignature = (MethodSignature) sig;
return jp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
}
/**
* 實(shí)際根據(jù)自身業(yè)務(wù)調(diào)整,主要是為了獲取通過(guò)某個(gè)值做攔截
*/
public String getAttrValue(String attr, Object[] args) {
String filedValue = null;
for (Object arg : args) {
try {
// 找到HttpServletRequest對(duì)象來(lái)獲取請(qǐng)求IP地址(如果是根據(jù)IP攔截的話)
if ("request_ip".equals(attr) && arg instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) arg;
filedValue = IPUtils.getIpAddr(request);
}
// 找到了值,返回
if (StringUtils.isNotBlank(filedValue)) {
break;
}
// fix: 使用lombok時(shí),uId這種字段的get方法與idea生成的get方法不同,會(huì)導(dǎo)致獲取不到屬性值,改成反射獲取解決
filedValue = String.valueOf(this.getValueByName(arg, attr));
} catch (Exception e) {
log.error("獲取路由屬性值失敗 attr:{}", attr, e);
}
}
return filedValue;
}
/**
* 獲取對(duì)象的特定屬性值(反射)
*
* @param item 對(duì)象
* @param name 屬性名
* @return 屬性值
* @author tang
*/
private Object getValueByName(Object item, String name) {
try {
// 獲取指定對(duì)象中對(duì)應(yīng)屬性名的Field對(duì)象
Field field = getFieldByName(item, name);
// 獲取到的Field對(duì)象為null,表示屬性不存在,直接返回null。
if (field == null) {
return null;
}
// 將Field對(duì)象設(shè)置為可訪問(wèn),以便獲取私有屬性的值。
field.setAccessible(true);
// 獲取屬性值,并將其賦值給變量o。
Object o = field.get(item);
// 將Field對(duì)象設(shè)置為不可訪問(wèn),以保持對(duì)象的封裝性。
field.setAccessible(false);
return o;
} catch (IllegalAccessException e) {
return null;
}
}
/**
* 根據(jù)名稱獲取方法,該方法同時(shí)兼顧繼承類(lèi)獲取父類(lèi)的屬性
*
* @param item 對(duì)象
* @param name 屬性名
* @return 該屬性對(duì)應(yīng)方法
* @author tang
*/
private Field getFieldByName(Object item, String name) {
try {
Field field;
try {
// 獲取指定對(duì)象中對(duì)應(yīng)屬性名的Field對(duì)象。
field = item.getClass().getDeclaredField(name);
} catch (NoSuchFieldException e) {
// 沒(méi)有找到,拋出NoSuchFieldException異常,嘗試獲取父類(lèi)中對(duì)應(yīng)屬性名的Field對(duì)象
field = item.getClass().getSuperclass().getDeclaredField(name);
}
return field;
} catch (NoSuchFieldException e) {
// 父類(lèi)也沒(méi)找到對(duì)應(yīng)屬性名的Field對(duì)象,寄,返回null
return null;
}
}
}
以上代碼用到了自己寫(xiě)的一個(gè)工具類(lèi)IPUtils來(lái)獲取請(qǐng)求的IP地址,內(nèi)容如下
public class IPUtils {
private static Logger logger = LoggerFactory.getLogger(IPUtils.class);
private static final String IP_UTILS_FLAG = ",";
private static final String UNKNOWN = "unknown";
private static final String LOCALHOST_IP = "0:0:0:0:0:0:0:1";
private static final String LOCALHOST_IP1 = "127.0.0.1";
/**
* 獲取IP地址
* <p>
* 使用Nginx等反向代理軟件, 則不能通過(guò)request.getRemoteAddr()獲取IP地址
* 如果使用了多級(jí)反向代理的話,X-Forwarded-For的值并不止一個(gè),而是一串IP地址,X-Forwarded-For中第一個(gè)非unknown的有效IP字符串,則為真實(shí)IP地址
*/
public static String getIpAddr(HttpServletRequest request) {
String ip = null;
try {
//以下兩個(gè)獲取在k8s中,將真實(shí)的客戶端IP,放到了x-Original-Forwarded-For。而將WAF的回源地址放到了 x-Forwarded-For了。
ip = request.getHeader("X-Original-Forwarded-For");
if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Forwarded-For");
}
//獲取nginx等代理的ip
if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("x-forwarded-for");
}
if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
//兼容k8s集群獲取ip
if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
if (LOCALHOST_IP1.equalsIgnoreCase(ip) || LOCALHOST_IP.equalsIgnoreCase(ip)) {
//根據(jù)網(wǎng)卡取本機(jī)配置的IP
InetAddress iNet = null;
try {
iNet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
logger.error("getClientIp error: {}", e);
}
ip = iNet.getHostAddress();
}
}
} catch (Exception e) {
logger.error("IPUtils ERROR ", e);
}
//使用代理,則獲取第一個(gè)IP地址
if (!StringUtils.isEmpty(ip) && ip.indexOf(IP_UTILS_FLAG) > 0) {
ip = ip.substring(0, ip.indexOf(IP_UTILS_FLAG));
}
return ip;
}
}
4.接口使用自定義注解實(shí)現(xiàn)限流
使用自定義限流注解
比如我在用戶controller層的登錄接口上使用注解,key為request_ip,表示根據(jù)用戶IP限流,回調(diào)函數(shù)為fallbackMethod,每分鐘訪問(wèn)限制10次
@PostMapping(value = "/login")
@AccessInterceptor(key = "request_ip", fallbackMethod = "loginErr", permitsPerSecond = 1L, blacklistCount = 10)
public Response<String> doLogin(@RequestParam String code, HttpServletRequest request){
綁定限流回調(diào)函數(shù)
這里需要注意的是,回調(diào)函數(shù)的參數(shù)必須和你使用限流注解的方法參數(shù)一致,否則報(bào)對(duì)應(yīng)方法找不到的錯(cuò)誤(因?yàn)檫@里是通過(guò)反射機(jī)制找到回調(diào)函數(shù)執(zhí)行的)
public Response<String> loginErr(String code, HttpServletRequest request) {
System.out.println("限流觸發(fā)回調(diào),參數(shù)信息:" + code);
return Response.<String>builder()
.code(Constants.ResponseCode.FREQUENCY_LIMITED.getCode())
.info(Constants.ResponseCode.FREQUENCY_LIMITED.getInfo())
.data(code)
.build();
}
總結(jié)
以上通過(guò)Redission+自定義注解+AOP+反射實(shí)現(xiàn)了對(duì)不同標(biāo)識(shí)符的限流和黑名單攔截,并且可以綁定限流回調(diào)函數(shù)來(lái)處理限流后的邏輯,代碼篇幅較長(zhǎng),各位小伙伴也可以嘗試?yán)^續(xù)優(yōu)化一下這里的設(shè)計(jì),減少request_ip這種魔法值(實(shí)在懶得改了),感謝您的收看,萬(wàn)字長(zhǎng)文(雖然大部分是代碼),有幫助就多多支持吧文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-833024.html
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-833024.html
到了這里,關(guān)于Springboot 中使用 Redisson+AOP+自定義注解 實(shí)現(xiàn)訪問(wèn)限流與黑名單攔截的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!