優化日志對象創建以及日志對象復用
日志對象上下文實體類
traceId
請求到達時間戳
請求完成時間戳
請求總共耗費時長
get/post/put/delete請求方式
Http狀態碼
原始請求頭中的所有鍵值對
請求體內容
響應體內容
失敗Exception信息詳細記錄
是否命中緩存
package com.kira.scaffoldmvc.CommonPool;import lombok.Data;
import lombok.experimental.Accessors;/*** RTA代理上下文:存儲請求處理全流程的關鍵信息* 設計特點:* - 線程隔離:通過ThreadLocal管理,確保每個請求獨立使用* - 鏈式調用:通過@Accessors(chain = true)支持方法鏈風格* - 全量數據:包含請求、響應、轉發和緩存等完整生命周期信息*/
@Data
@Accessors(chain = true)
public class RtaProxyContext {/*** 全局唯一請求ID(TraceID)* 用于全鏈路追蹤,關聯請求與響應日志*/private String reqXid;/*** 請求到達時間戳(毫秒)* 用于計算請求處理耗時*/private long reqTime;/*** 請求路徑*/private String url;/*** HTTP請求方法(GET/POST等)*/private String reqType;/*** 請求頭信息* 存儲原始HTTP請求頭的鍵值對*/private Object reqHeaders;/*** 請求體內容* 通常為JSON格式的廣告請求參數*/private Object reqBody;/*** 響應體內容* 最終返回給客戶端的內容*/private Object respBody;/*** HTTP響應狀態碼* 如200、403、500等*/private Integer respCode;/*** 響應完成時間戳(毫秒)* 用于計算總處理耗時:respTime - reqTime*/private long respTime;/*** 錯誤詳情* 當請求處理過程中發生異常時記錄*/private String errorDetails;/*** 請求體填充率* 廣告請求中有效流量占比,范圍0.0-1.0*/private double reqBodyFillRate;/*** 是否命中緩存標識* true表示響應數據來自緩存而非實時計算*/private Boolean cached = false;}
RtaProxyContextHolder-對象池定義與封裝
最小空閑對象數:200(系統啟動的時候預熱)
最大空閑對象數:600
最大對象數:1000
setMaxTotal(1000):池內對象的最大數量(活躍 + 空閑)
setMaxIdle(600):最大空閑對象數,超過此數量的空閑對象將被銷毀
setMinIdle(200):最小空閑對象數,池會自動維持此數量的空閑對象
package com.kira.scaffoldmvc.CommonPool;import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;//ThreadLocal結合對象池,封裝了對象池的相關操作
@Slf4j
public class RtaProxyContextHolder {private static final ThreadLocal<RtaProxyContext> CONTEXT_HOLDER = new ThreadLocal<>();public static final GenericObjectPool<RtaProxyContext> RTA_PROXY_CONTENT_POOL = new GenericObjectPool<>(new RtaProxyContextFactory(),//對象池工廠new GenericObjectPoolConfig<>() {{//對象池配置setMaxTotal(1000);//最大對象數setMaxIdle(600);//最大空閑對象數setMinIdle(200);//最小空閑對象數}});//自定義對象池工廠private static class RtaProxyContextFactory extends BasePooledObjectFactory<RtaProxyContext> {//創建新對象@Overridepublic RtaProxyContext create() {return new RtaProxyContext();}//負責將新創建或從其他地方獲取的 RtaProxyContext 對象包裝成 PooledObject<RtaProxyContext> 類型的對象// 這里使用 DefaultPooledObject 進行包裝,以便對象池進行統一管理//將其他地方的對象(例如不是對象池創建的對象)轉換成對象池對象實現復用@Overridepublic PooledObject<RtaProxyContext> wrap(RtaProxyContext context) {return new DefaultPooledObject<>(context);}@Overridepublic void destroyObject(PooledObject<RtaProxyContext> p) {// 銷毀對象的邏輯}}public static RtaProxyContext getContext() {return CONTEXT_HOLDER.get();}public static RtaProxyContext borrowContext() {RtaProxyContext rtaProxyContext;try {//先從對象池獲取對象rtaProxyContext = RTA_PROXY_CONTENT_POOL.borrowObject();} catch (Exception e) {//對象池獲取對象失敗,為了保證對象池可以成功創建,所以要new一個對象rtaProxyContext = new RtaProxyContext();log.warn("Failed to pull from pool");}//將詳細的日志對象放到ThreadLocal里面CONTEXT_HOLDER.set(rtaProxyContext);return rtaProxyContext;}public static void returnContext(RtaProxyContext context) {//將日志對象從ThreadLocal中移除CONTEXT_HOLDER.remove();//將原本的日志對象的大部分值變成空值if (context != null) {try {context.setReqXid(null);context.setReqTime(-1);context.setReqType(null);context.setReqHeaders(null);context.setReqBody(null);context.setRespBody(null);context.setRespCode(-1);context.setRespTime(-1);context.setErrorDetails(null);context.setReqBodyFillRate(-1);context.setCached(false);RTA_PROXY_CONTENT_POOL.returnObject(context);} catch (Exception e) {log.warn("Failed to return to pool");}}}//日志打印對象池的狀態public static void printPoolStatus() {log.info("Context Pool狀態, 活躍對象數 {}, 空閑對象數 {}, 等待獲取對象的線程數 {}, 對象創建總次數 {}, 對象銷毀總次數 {}.",RTA_PROXY_CONTENT_POOL.getNumActive(),RTA_PROXY_CONTENT_POOL.getNumIdle(),RTA_PROXY_CONTENT_POOL.getNumWaiters(),RTA_PROXY_CONTENT_POOL.getCreatedCount(),RTA_PROXY_CONTENT_POOL.getDestroyedCount());}// 動態調整對象池配置public static void adjustPoolConfig(int maxTotal, int maxIdle, int minIdle) {GenericObjectPoolConfig<RtaProxyContext> config = new GenericObjectPoolConfig<>();config.setMaxTotal(maxTotal);config.setMaxIdle(maxIdle);config.setMinIdle(minIdle);// 應用新配置RTA_PROXY_CONTENT_POOL.setConfig(config);log.info("對象池配置已更新: maxTotal={}, maxIdle={}, minIdle={}", maxTotal, maxIdle, minIdle);}}
RtaProxyContextHolder-請求攔截器
package com.kira.scaffoldmvc.CommonPool;import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.HandlerInterceptor;import java.util.Collections;
import java.util.stream.Collectors;/*** HTTP請求攔截器:管理請求生命周期內的上下文對象(RtaProxyContext)* 主要功能:* 1. 請求進入時從對象池獲取上下文對象并初始化* 2. 請求處理過程中通過ThreadLocal存儲上下文* 3. 請求結束后記錄日志并歸還對象到池** 線程安全機制:* - 使用ThreadLocal確保每個請求線程擁有獨立的上下文實例* - 對象池復用機制減少頻繁創建/銷毀對象的開銷*/
@Component
@RequiredArgsConstructor
public class RtaProxyContextInterceptor implements HandlerInterceptor {private final LogService logService;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {// 1. 從對象池獲取可復用的上下文對象// 若池為空則創建新對象,避免頻繁new操作帶來的GC壓力RtaProxyContext rtaProxyContext = RtaProxyContextHolder.borrowContext();// 2. 初始化上下文:設置請求基礎信息rtaProxyContext.setReqTime(System.currentTimeMillis())//請求時間戳.setReqXid(request.getHeader("traceId"))// 全局唯一請求ID(traceId).setReqType(request.getMethod())// HTTP方法(GET/POST等).setReqHeaders(Collections.list(request.getHeaderNames())//請求頭信息.stream().collect(Collectors.toMap(name -> name, request::getHeader))).setUrl(request.getRequestURI());//請求url// 3. 放行請求繼續處理鏈return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {// 1. 從當前線程獲取上下文對象// 確保與preHandle中設置的是同一個實例RtaProxyContext rtaProxyContext = RtaProxyContextHolder.getContext();// 2. 補充響應信息:狀態碼、響應時間、異常詳情rtaProxyContext.setRespCode(response.getStatus()) // HTTP響應狀態碼.setRespTime(System.currentTimeMillis()) // 響應完成時間.setErrorDetails(ex != null ? ex.getMessage() : null); // 異常信息(若有)// 3. 針對特定API路徑記錄詳細日志// 注意:此處硬編碼路徑需根據實際業務調整if ("/api/v1/aff_rta".equals(request.getRequestURI())) {//因為該日志是打印對象信息,所以封裝了對應的實現類logService.logRtaAccess(RtaAccessLog.from(rtaProxyContext)); // 記錄訪問日志}// 4. 關鍵資源回收步驟:// - 從ThreadLocal中移除引用,防止內存泄漏// - 重置對象狀態并歸還到對象池供后續請求復用RtaProxyContextHolder.returnContext(rtaProxyContext);}
}
定時任務-打印對象池狀態
package com.kira.scaffoldmvc.CommonPool;import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;@Component
public class SysMonitorConfig {@Scheduled(fixedRateString = "${sys.monitor.pool.rate:300000}")public void monitorContextPool() {RtaProxyContextHolder.printPoolStatus();}}
定時任務-自適應調整對象池參數
package com.kira.scaffoldmvc.CommonPool;import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;import java.util.ArrayList;
import java.util.concurrent.ConcurrentLinkedQueue;import static com.kira.scaffoldmvc.CommonPool.RtaProxyContextHolder.RTA_PROXY_CONTENT_POOL;@Component
public class SysMonitorConfig {// 滑動窗口配置private final ArrayList<Integer> list = new ArrayList<>();//最大修改范圍是原來對象池范圍的兩倍或三倍private final Integer minIdle = 600;private final Integer maxIdle = 1800;private final Integer maxTotal = 3000;//每五分鐘打印一次對象池狀態,并將當前活躍對象數放進List中@Scheduled(cron = "0 0/5 * * * *")public void monitorContextPool() {RtaProxyContextHolder.printPoolStatus();list.add(RTA_PROXY_CONTENT_POOL.getNumActive());}//每半小時統計后5個滑動窗口里的對象數,如果平均活躍對象數大于當前對象池中配置的最大空閑對象數,則更新@Scheduled(cron = "0 0/30 * * * *")public void adjustContextPool() {Integer averageIdle = 0;Integer sum = 0;for (int i = list.size() - 1, j = 0; j < 6; i--, j++) {sum += list.get(i);}averageIdle = sum / 6;if(averageIdle>RTA_PROXY_CONTENT_POOL.getMaxIdle() && RTA_PROXY_CONTENT_POOL.getMaxIdle()!=maxIdle){//平均活躍數大于對象池最大空閑對象數,則進行更新,如果對象池最大空閑對象數已經更新到了可達到的最大值,那么就沒必要更新int minSize = 200;if(averageIdle/2>RTA_PROXY_CONTENT_POOL.getMaxIdle()){//如果平均活躍數/2大于最小活躍數,則直接更新成允許范圍的最大值minSize=minIdle;}int maxSize = RTA_PROXY_CONTENT_POOL.getMaxIdle();while(maxSize<averageIdle){maxSize += 600;}if(maxSize>maxIdle){maxSize=maxIdle;}//更新對象池配置RtaProxyContextHolder.adjustPoolConfig(maxTotal,maxSize,minSize);}}}
如何分析響應速度
響應速度:拿出之前沒使用對象池時url的平均時間和使用對象池后url的平均時間進行對比
總結
因為不同的業務要用到不同的信息,所以將一個請求的信息封裝到對象里面,這樣子可以應付許多不同的日志打印場景或其他需要使用請求信息的業務
例如我們把
traceId
請求到達時間戳
請求完成時間戳
請求總共耗費時長
get/post/put/delete請求方式
Http狀態嗎
原始請求頭中的所有鍵值對
請求體內容
響應體內容
失敗Exception信息詳細記錄
是否命中緩存
封裝到對象里面,日志信息非常詳細,可以更加方便排查問題,操作對象也可以使獲取信息更簡單
定時任務,每五分鐘打印對象對象池的狀態
定時任務,每30分鐘檢查之前的平均活躍對象數是否大于對象池中的最大空閑對象數,如果大于則通過自適應策略對對象池容量進行一個自適應更新
通過對比不同接口使用對象池前后的用時時間,計算出提高了多少的響應時間