基於AOP和Redis實現對介面呼叫情況的介面及IP限流
阿新 • • 發佈:2019-12-31
[toc]
需求描述
- 專案中有許多介面,現在我們需要實現一個功能對介面呼叫情況進行統計,主要功能如下:
- 需求一:實現對每個介面,每天的呼叫次數做記錄;
- 需求二:如果某次呼叫丟擲了異常資訊,則記錄下異常資訊;
- 需求三:限流,限制單個IP一天內對一個介面的呼叫次數。
概要設計
-
因為需要對每個介面的呼叫情況進行統計,所以選擇AOP來實現,將Controller層抽象為一個切面
-
@Before 執行業務操作前進行限流判斷;
-
@AfterReturn 如果正常返回則呼叫次數加1;
-
@AfterThrowing 如果丟擲異常則記錄異常資訊。
如果將這些資訊寫入資料庫的話會對每個介面帶來額外的操作資料庫的開銷,影響介面響應時間,且此類記錄資訊較多,所以此處選擇Redis將這些資訊快取下來。
-
-
Redis設計
- 對於需求一,我們需要記錄三個資訊:1、呼叫的介面名;2、呼叫的日期(精確到天);3、呼叫次數。所以此處Redis的key使用Hash結構,資料結構如下:key = 介面URI、key = 呼叫日期(到天)、value = 呼叫次數(初始值為1,沒一次呼叫後自增1)。
- 對於需求二,需要記錄的資訊有:1、呼叫的介面名;2、異常發生時間(精確到毫秒);3、異常資訊。因為需求一的key已經設定成了介面URI,所以此處選擇使用URI + 字尾“_exception”的形式來代表異常資訊的key。所以此需求Redis的資料結構設計如下(仍然使用Hash結構):key = URI + “_exception”、key = 異常發生時間(精確到毫秒)、value = 異常資訊。
- 對於需求三,我們需要記錄的資訊有:1、呼叫的介面名;2、ip地址;3、呼叫時間;4、呼叫次數。此需求需要記錄的資訊較多,但是我們可以將資訊1、資訊2、資訊3組合起來拼接成一個唯一的key即可,將呼叫時間的維度精確到天且設定key的過期時間為一天,這樣的一個key即可代表單個IP一天時間內訪問了哪些介面。所以Redis的資料結構設計如下:key = URI + ip +date(精確到天)、value = 呼叫次數。
程式碼實現
/**
* 介面呼叫情況監控
* 1、監控單個介面一天內的呼叫次數
* 2、如果丟擲異常,則記錄異常資訊及發生時間
* 3、對單個IP進行限流,每天對每個介面的呼叫次數有限
*
* @author csh
* @date 2019/10/30
*/
@Aspect
@Component
public class ApiCallAdvice {
@Resource
private RedisTemplate redisTemplate;
@Resource
private StringRedisTemplate stringRedisTemplate;
private static final String FORMAT_PATTERN_DAY = "yyyy-MM-dd";
private static final String FORMAT_PATTERN_MILLS = "yyyy-MM-dd HH:mm:ss:SSS";
/**
* 真正執行業務操作前先進行限流的驗證
* 限制維度為:一天內單個IP的訪問次數
* key = URI + IP + date(精確到天)
* value = 呼叫次數
*/
@Before("execution(* com.pagoda.erp.platform.controller.*.*(..))")
public void before() {
// 接收到請求,記錄請求內容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//獲取請求的request
HttpServletRequest request = attributes.getRequest();
String uri = request.getRequestURI();
String date = dateFormat(FORMAT_PATTERN_DAY);
String ip = getRequestIp(request);
if (StringUtils.isEmpty(ip)) {
throw new BusinessException("IP不能為空。");
}
// URI+IP+日期 構成以天為維度的key
String ipKey = uri + "_" + ip + "_" + date;
if (redisTemplate.hasKey(ipKey)) {
if (Integer.parseInt(redisTemplate.opsForValue().get(ipKey).toString()) > 10000) {
throw new BusinessException("訪問失敗,已超過訪問次數。");
}
redisTemplate.opsForValue().increment(ipKey,1);
} else {
stringRedisTemplate.opsForValue().set(ipKey,"1",1L,TimeUnit.DAYS);
}
}
/**
* 如果有返回結果,代表一次呼叫,則對應介面的呼叫次數加一,統計維度為天
* (Redis使用Hash結構)
* key = URI
* key = date (精確到天)
* value = 呼叫次數
*/
@AfterReturning("execution(* com.pagoda.erp.platform.controller.*.*(..))")
public void afterReturning() {
// 接收到請求,記錄請求內容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//獲取請求的request
HttpServletRequest request = attributes.getRequest();
String uri = request.getRequestURI();
String date = dateFormat(FORMAT_PATTERN_DAY);
if (redisTemplate.hasKey(uri)) {
redisTemplate.boundHashOps(uri).increment(date,1);
} else {
redisTemplate.boundHashOps(uri).put(date,1);
}
}
/**
* 如果呼叫丟擲異常,則快取異常資訊(Redis使用Hash結構)
* key = URI + “_exception”
* key = time (精確到毫秒的時間)
* value = exception 異常資訊
*
* @param ex 異常資訊
*/
@AfterThrowing(value = "execution(* com.pagoda.erp.platform.controller.*.*(..))",throwing = "ex")
public void afterThrowing(Exception ex) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String uri = request.getRequestURI() + "_exception";
String time = dateFormat(FORMAT_PATTERN_MILLS);
String exception = ex.getMessage();
redisTemplate.boundHashOps(uri).put(time,exception);
}
private String getRequestIp(HttpServletRequest request) {
// 獲取請求IP
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)) {
ip = "" + request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)) {
ip = "" + request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)) {
ip = "" + request.getRemoteAddr();
}
return ip;
}
private String dateFormat(String pattern) {
SimpleDateFormat dateFormat = new SimpleDateFormat(pattern);
return dateFormat.format(new Date());
}
}
複製程式碼