优化日志对象创建以及日志对象复用
日志对象上下文实体类
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> {
//创建新对象
@Override
public RtaProxyContext create() {
return new RtaProxyContext();
}
//负责将新创建或从其他地方获取的 RtaProxyContext 对象包装成 PooledObject<RtaProxyContext> 类型的对象
// 这里使用 DefaultPooledObject 进行包装,以便对象池进行统一管理
//将其他地方的对象(例如不是对象池创建的对象)转换成对象池对象实现复用
@Override
public PooledObject<RtaProxyContext> wrap(RtaProxyContext context) {
return new DefaultPooledObject<>(context);
}
@Override
public 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;
@Override
public 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;
}
@Override
public 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分钟检查之前的平均活跃对象数是否大于对象池中的最大空闲对象数,如果大于则通过自适应策略对对象池容量进行一个自适应更新
通过对比不同接口使用对象池前后的用时时间,计算出提高了多少的响应时间