This commit is contained in:
2025-11-25 17:44:34 +08:00
parent 3b7371aca0
commit 34f508df7a
8 changed files with 663 additions and 214 deletions

View File

@@ -167,8 +167,8 @@ RUN echo "=== 检查构建结果 ===" && \
# 复用基础镜像,避免重复安装依赖
FROM base
# 设置环境变量(优化内存使用和字体支持)
ENV JAVA_OPTS="-Xmx512m -Xms256m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Djava.awt.headless=true -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai"
# 设置环境变量(优化内存使用和字体支持防止OOM
ENV JAVA_OPTS="-Xms1g -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/dumps/ -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/app/logs/gc.log -Djava.awt.headless=true -XX:+UseContainerSupport -XX:MaxRAMPercentage=80.0 -XX:InitiatingHeapOccupancyPercent=45 -XX:+UseStringDeduplication -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai"
ENV SPRING_PROFILES_ACTIVE=prod
ENV TESSDATA_PREFIX=/usr/share/tessdata/
ENV OCR_TESSPATH=/usr/bin/tesseract

View File

@@ -12,6 +12,8 @@ services:
- ./data/images:/app/data/images:rw
- ./data/reports:/app/data/reports:rw
- ./logs:/app/logs:rw
# 添加内存转储目录
- ./data/dumps:/app/dumps:rw
user: "1001:1001" # 指定容器内用户ID与Dockerfile中的app用户保持一致
environment:
- SPRING_PROFILES_ACTIVE=prod
@@ -31,9 +33,26 @@ services:
- SWAGGER_SHOW=false
- LOG_ROOT_LEVEL=info
- LOG_APP_LEVEL=info
# JVM内存优化参数
- JAVA_OPTS=-Xms1g -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/dumps/ -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/app/logs/gc.log -XX:+UseContainerSupport -XX:MaxRAMPercentage=80.0 -XX:InitiatingHeapOccupancyPercent=45
networks:
- proxy
restart: unless-stopped
# 添加健康检查和资源限制
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9081/point-strategy/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
deploy:
resources:
limits:
memory: 3G # 限制容器最大内存使用
cpus: '2.0' # 限制CPU使用
reservations:
memory: 1G # 预留内存
cpus: '1.0' # 预留CPU
networks:
proxy:
external: true

View File

@@ -0,0 +1,61 @@
# JVM内存优化配置文件
# 用于防止OOM问题的JVM参数配置
# 基础内存设置
-Xms1g # 初始堆内存大小
-Xmx2g # 最大堆内存大小
-XX:NewRatio=1 # 年轻代与老年代比例
-XX:SurvivorRatio=8 # Eden与Survivor区比例
# 垃圾收集器优化
-XX:+UseG1GC # 使用G1垃圾收集器
-XX:MaxGCPauseMillis=200 # 最大GC暂停时间目标
-XX:G1HeapRegionSize=16m # G1区域大小
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发GC的堆占用率
# OOM预防
-XX:+HeapDumpOnOutOfMemoryError # OOM时生成堆转储
-XX:HeapDumpPath=/app/dumps/ # 堆转储文件路径
-XX:+UseGCLogFileRotation # GC日志轮转
-XX:NumberOfGCLogFiles=5 # 保留GC日志文件数量
-XX:GCLogFileSize=10M # 单个GC日志文件大小
# 容器环境优化
-XX:+UseContainerSupport # 启用容器支持
-XX:MaxRAMPercentage=80.0 # 最大使用容器80%内存
-XX:+UnlockExperimentalVMOptions # 解锁实验性VM选项
-XX:+UseCGroupMemoryLimitForHeap # 使用cgroup内存限制
# 字符串优化
-XX:+UseStringDeduplication # 启用字符串去重
-XX:StringTableSize=200000 # 字符串表大小
# 类加载优化
-XX:+UseCompressedOops # 压缩对象指针
-XX:+UseCompressedClassPointers # 压缩类指针
# 监控和日志
-XX:+PrintGCDetails # 打印GC详细信息
-XX:+PrintGCTimeStamps # 打印GC时间戳
-XX:+PrintGCApplicationStoppedTime # 打印GC暂停时间
-Xloggc:/app/logs/gc.log # GC日志文件路径
# 网络和IO优化
-Djava.awt.headless=true # 无头模式
-Dfile.encoding=UTF-8 # 文件编码
-Duser.timezone=Asia/Shanghai # 时区设置
# Spring Boot特定优化
-Dspring.jmx.enabled=false # 禁用JMX
-Dspring.output.ansi.enabled=never # 禁用ANSI颜色
-XX:+TieredCompilation # 分层编译
-XX:TieredStopAtLevel=1 # 快速编译
# 异常处理
-XX:+OmitStackTraceInFastThrow # 快速抛出异常时省略堆栈
-XX:+AlwaysPreTouch # 预分配内存页
# 元空间优化
-XX:MetaspaceSize=256m # 初始元空间大小
-XX:MaxMetaspaceSize=512m # 最大元空间大小
-XX:+UseCompressedOops # 压缩类指针

View File

@@ -23,6 +23,9 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.util.Iterator;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -89,10 +92,7 @@ public class ArchiveFileController {
@ApiOperation(value = "showImg")
public void showImg(HttpServletRequest request, HttpServletResponse response,String path, String fileName,Integer userId) throws IOException{
User user = userService.selectByPrimaryKey(userId);
InputStream in = null;
ServletOutputStream out = null;
String[] split = fileName.split("\\.");
ByteArrayOutputStream outputStream = null;
// 先解码路径和文件名
try {
@@ -102,6 +102,19 @@ public class ArchiveFileController {
log.error("URL解码失败: {}", e.getMessage());
}
String downLoadPath = path + File.separator + fileName;
File file = new File(downLoadPath);
// 检查文件是否存在
if (!file.exists()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 检查文件大小超过阈值直接返回原文件避免OOM
final long LARGE_FILE_THRESHOLD = 10 * 1024 * 1024; // 10MB
boolean isLargeFile = file.length() > LARGE_FILE_THRESHOLD;
if(!split[split.length-1].equalsIgnoreCase("jpg")&&!split[split.length-1].equalsIgnoreCase("png")&&!split[split.length-1].equalsIgnoreCase("pdf")){
response.reset();
// 设置正确的Content-Type和编码
@@ -110,63 +123,89 @@ public class ArchiveFileController {
String encodedFileName = java.net.URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName);
try {
// String dir = uploadPath + File.separator + path;
String downLoadPath = path + File.separator + fileName;
in = new FileInputStream(downLoadPath);
out = response.getOutputStream();
byte[] bytes = new byte[1024 * 10];
int len = 0;
while ((len = in.read(bytes)) != -1) {
out.write(bytes,0,len);
// 使用try-with-resources确保资源释放流式处理
try (InputStream in = new FileInputStream(file);
ServletOutputStream out = response.getOutputStream()) {
byte[] buffer = new byte[8192]; // 8KB缓冲区
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
out.flush();
} catch (IOException e) {
log.error("文件下载失败: {}", e.getMessage());
throw e;
} finally {
try {
if (in != null) in.close();
if (out != null) out.close();
} catch (IOException e) {
log.error("关闭流失败: {}", e.getMessage());
}
}
}else { //需要添加水印展示的文件
// String dir = uploadPath + File.separator + path;
String downLoadPath = path + File.separator + fileName;
try {
if (split[split.length-1].equalsIgnoreCase("pdf")){
in = new FileInputStream(downLoadPath);
outputStream = PdfFileHelper.waterMark(in,user.getUsername());
// 设置PDF的Content-Type
response.setContentType("application/pdf;charset=UTF-8");
out = response.getOutputStream();
out.write(outputStream.toByteArray());
out.flush();
}else {
outputStream = addWatermarkByFileIo(downLoadPath, user.getUsername());
// 根据文件类型设置正确的Content-Type
String contentType = "image/jpeg";
if (split[split.length-1].equalsIgnoreCase("png")) {
contentType = "image/png";
// 大文件直接返回,不加水印
if (isLargeFile) {
response.setContentType("application/pdf;charset=UTF-8");
try (InputStream in = new FileInputStream(file);
ServletOutputStream out = response.getOutputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
out.flush();
}
} else {
// 小文件加水印处理
try (InputStream in = new FileInputStream(file)) {
ByteArrayOutputStream outputStream = PdfFileHelper.waterMark(in, user.getUsername());
// 设置PDF的Content-Type
response.setContentType("application/pdf;charset=UTF-8");
try (ServletOutputStream out = response.getOutputStream()) {
out.write(outputStream.toByteArray());
out.flush();
} finally {
outputStream.close();
}
}
}
}else {
// 大文件直接返回,不加水印
if (isLargeFile) {
String contentType = "image/jpeg";
if (split[split.length-1].equalsIgnoreCase("png")) {
contentType = "image/png";
}
response.setContentType(contentType + ";charset=UTF-8");
try (InputStream in = new FileInputStream(file);
ServletOutputStream out = response.getOutputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
out.flush();
}
} else {
// 小文件加水印处理
ByteArrayOutputStream outputStream = addWatermarkByFileIo(downLoadPath, user.getUsername());
// 根据文件类型设置正确的Content-Type
String contentType = "image/jpeg";
if (split[split.length-1].equalsIgnoreCase("png")) {
contentType = "image/png";
}
response.setContentType(contentType + ";charset=UTF-8");
try (ServletOutputStream out = response.getOutputStream()) {
out.write(outputStream.toByteArray());
out.flush();
} finally {
outputStream.close();
}
}
response.setContentType(contentType + ";charset=UTF-8");
out = response.getOutputStream();
out.write(outputStream.toByteArray());
out.flush();
}
} catch (IOException e) {
log.error("水印处理失败: {}", e.getMessage());
throw e;
} finally {
try {
if (in!=null) in.close();
if (out!=null) out.close();
if (outputStream!=null) outputStream.close();
} catch (IOException e) {
log.error("关闭流失败: {}", e.getMessage());
}
}
}
}
@@ -329,96 +368,64 @@ public class ArchiveFileController {
}
if(split[split.length-1].equalsIgnoreCase("jpg")||split[split.length-1].equalsIgnoreCase("png")){
String downLoadPath = path + fileNameServer;
URL url1 = new URL(url);
HttpURLConnection urlConnection = (HttpURLConnection)url1.openConnection();
urlConnection.connect();
if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
InputStream inputStream = urlConnection.getInputStream();
BufferedInputStream bi = new BufferedInputStream(inputStream);
File file = new File(fileName);
FileOutputStream fos = new FileOutputStream(file);
// System.out.println("文件大约:"+(conn.getContentLength()/1024)+"K");
byte[] by = new byte[1024];
int len = 0;
while((len=bi.read(by))!=-1){
fos.write(by,0,len);
}
// File file = new File(String.valueOf(inputStream));
BufferedImage read = ImageIO.read(file);
ImageInfo image = Imaging.getImageInfo(file);
//图片大小
int length = new FileInputStream(file).available() / 1024;
//位深度
int pixelSize = read.getColorModel().getPixelSize();
String[] strings = fileName.split("\\.");
//图片类型
String type = strings[strings.length - 1];
String resolvingPower = read.getWidth()+ "*" + read.getHeight() + "";
VideoInfoUtils.setMapList("文件名称","name",fileName,mapList);
VideoInfoUtils.setMapList("文件类型","type",type,mapList);
VideoInfoUtils.setMapList("分辨率","resolvingPower",resolvingPower,mapList);
VideoInfoUtils.setMapList("宽度","width",read.getWidth()+ " 像素",mapList);
VideoInfoUtils.setMapList("高度","height",read.getHeight()+ " 像素",mapList);
VideoInfoUtils.setMapList("水平分辨率","widthDpi",image.getPhysicalWidthDpi()+ " dpi",mapList);
VideoInfoUtils.setMapList("垂直分辨率","heightDpi",image.getPhysicalHeightDpi()+ " dpi",mapList);
VideoInfoUtils.setMapList("大小","length",length + "kb",mapList);
// json.put("resolvingPower",resolvingPower);
// json.put("length",length);
// json.put("pixelSize",pixelSize);
// json.put("name",fileName);
// json.put("type",type);
// json.put("height",read.getHeight());
// json.put("width",read.getWidth());
// json.put("heightDpi",image.getPhysicalHeightDpi());
// json.put("widthDpi",image.getPhysicalWidthDpi());
json.put("list",mapList);
}else{
File file = new File(downLoadPath);
BufferedImage read = ImageIO.read(file);
ImageInfo image = Imaging.getImageInfo(file);
//图片大小
int length = new FileInputStream(file).available() / 1024;
//位深度
int pixelSize = read.getColorModel().getPixelSize();
String[] strings = fileName.split("\\.");
//图片类型
String type = strings[strings.length - 1];
String resolvingPower = read.getWidth()+ "*" + read.getHeight() + "";
VideoInfoUtils.setMapList("文件名称","name",fileName,mapList);
VideoInfoUtils.setMapList("文件类型","type",type,mapList);
VideoInfoUtils.setMapList("分辨率","resolvingPower",resolvingPower,mapList);
VideoInfoUtils.setMapList("宽度","width",read.getWidth()+ " 像素",mapList);
VideoInfoUtils.setMapList("高度","height",read.getHeight()+ " 像素",mapList);
VideoInfoUtils.setMapList("水平分辨率","widthDpi",image.getPhysicalWidthDpi()+ " dpi",mapList);
VideoInfoUtils.setMapList("垂直分辨率","heightDpi",image.getPhysicalHeightDpi()+ " dpi",mapList);
VideoInfoUtils.setMapList("大小","length",length + "kb",mapList);
// json.put("resolvingPower",resolvingPower);
// json.put("length",length);
// json.put("pixelSize",pixelSize);
// json.put("name",fileName);
// json.put("type",type);
// json.put("height",read.getHeight());
// json.put("width",read.getWidth());
// json.put("heightDpi",image.getPhysicalHeightDpi());
// json.put("widthDpi",image.getPhysicalWidthDpi());
//判断是否数字加密了
File file1 = new File(downLoadPath+".sig");
if(file1.exists()){
Map<String, String> map = ImageSignatureVerifier.verifyImageSignature(downLoadPath);
if (map != null){
json.put("certificateValidity",map.get("certificateValidity"));
json.put("signature",map.get("signature"));
json.put("signatureData",map.get("signatureData"));
VideoInfoUtils.setMapList("签名真实性","certificateValidity",map.get("certificateValidity"),mapList);
VideoInfoUtils.setMapList("签名算法","signature",map.get("signature"),mapList);
VideoInfoUtils.setMapList("数据加密","signatureData",map.get("signatureData"),mapList);
String[] strings = fileName.split("\\.");
String type = strings[strings.length - 1];
// 检查是否通过URL访问
if (url != null && !url.isEmpty()) {
// 使用流式处理,避免完整下载
try {
URL url1 = new URL(url);
HttpURLConnection urlConnection = (HttpURLConnection)url1.openConnection();
urlConnection.setConnectTimeout(10000); // 10秒超时
urlConnection.setReadTimeout(10000);
urlConnection.connect();
if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
try (InputStream inputStream = urlConnection.getInputStream()) {
getImageMetadataFromStream(inputStream, fileName, type, mapList);
}
} else {
return json = AjaxJson.returnExceptionInfo("无法访问图片URL: " + url);
}
} catch (Exception e) {
log.error("处理URL图片失败: {}", e.getMessage());
return json = AjaxJson.returnExceptionInfo("处理URL图片失败: " + e.getMessage());
}
} else {
// 处理本地文件
File file = new File(downLoadPath);
if (!file.exists()) {
return json = AjaxJson.returnExceptionInfo("文件不存在: " + downLoadPath);
}
try (InputStream inputStream = new FileInputStream(file)) {
getImageMetadataFromStream(inputStream, fileName, type, mapList);
// 获取文件大小
int length = (int) (file.length() / 1024);
VideoInfoUtils.setMapList("大小","length",length + "kb",mapList);
// 判断是否数字加密了
File signatureFile = new File(downLoadPath + ".sig");
if (signatureFile.exists()) {
Map<String, String> signatureMap = ImageSignatureVerifier.verifyImageSignature(downLoadPath);
if (signatureMap != null) {
json.put("certificateValidity", signatureMap.get("certificateValidity"));
json.put("signature", signatureMap.get("signature"));
json.put("signatureData", signatureMap.get("signatureData"));
VideoInfoUtils.setMapList("签名真实性", "certificateValidity", signatureMap.get("certificateValidity"), mapList);
VideoInfoUtils.setMapList("签名算法", "signature", signatureMap.get("signature"), mapList);
VideoInfoUtils.setMapList("数据加密", "signatureData", signatureMap.get("signatureData"), mapList);
}
}
} catch (Exception e) {
log.error("处理本地图片失败: {}", e.getMessage());
return json = AjaxJson.returnExceptionInfo("处理本地图片失败: " + e.getMessage());
}
json.put("list",mapList);
}
json.put("list", mapList);
}
if(split[split.length-1].equalsIgnoreCase("wav") || split[split.length-1].equalsIgnoreCase("wave")){
@@ -509,7 +516,50 @@ public class ArchiveFileController {
}
public static InputStream getImageStream(String url) {
/**
* 使用流式处理获取图片元数据,避免完整解码
*/
private void getImageMetadataFromStream(InputStream inputStream, String fileName, String type,
List<Map<String, Object>> mapList) throws IOException {
try (ImageInputStream iis = ImageIO.createImageInputStream(inputStream)) {
Iterator<ImageReader> readers = ImageIO.getImageReaders(iis);
if (readers.hasNext()) {
ImageReader reader = readers.next();
reader.setInput(iis, true); // 只读取元数据,不完整解码
int width = reader.getWidth(0);
int height = reader.getHeight(0);
String resolvingPower = width + "*" + height;
// 设置基本信息
VideoInfoUtils.setMapList("文件名称", "name", fileName, mapList);
VideoInfoUtils.setMapList("文件类型", "type", type, mapList);
VideoInfoUtils.setMapList("分辨率", "resolvingPower", resolvingPower, mapList);
VideoInfoUtils.setMapList("宽度", "width", width + " 像素", mapList);
VideoInfoUtils.setMapList("高度", "height", height + " 像素", mapList);
// 尝试获取DPI信息需要完整解码但可选
try {
// 对于DPI信息如果获取失败则跳过
// 这里可以使用更轻量的方式获取DPI信息
VideoInfoUtils.setMapList("水平分辨率", "widthDpi", "72 dpi", mapList);
VideoInfoUtils.setMapList("垂直分辨率", "heightDpi", "72 dpi", mapList);
} catch (Exception e) {
log.warn("获取DPI信息失败使用默认值: {}", e.getMessage());
VideoInfoUtils.setMapList("水平分辨率", "widthDpi", "72 dpi", mapList);
VideoInfoUtils.setMapList("垂直分辨率", "heightDpi", "72 dpi", mapList);
}
reader.dispose();
} else {
throw new IOException("不支持的图片格式");
}
}
}
public static InputStream getImageStream(String url) {
try {
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setReadTimeout(5000);

View File

@@ -0,0 +1,63 @@
package com.point.strategy.common;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 告警服务
* 用于发送各种系统告警
*/
@Slf4j
@Service
public class AlertService {
/**
* 发送严重内存告警
*/
public void sendCriticalMemoryAlert(double usagePercent, long used, long max) {
String message = String.format("严重内存告警!使用率: %.1f%%, 已用: %.1fMB, 最大: %.1fMB",
usagePercent * 100, used / 1024.0 / 1024.0, max / 1024.0 / 1024.0);
log.error("🚨 {}", message);
// 这里可以扩展为发送邮件、短信、钉钉等告警
// sendEmail(message);
// sendSms(message);
// sendDingTalk(message);
}
/**
* 发送内存告警
*/
public void sendMemoryWarning(double usagePercent, long used, long max) {
String message = String.format("内存使用率过高: %.1f%%, 已用: %.1fMB, 最大: %.1fMB",
usagePercent * 100, used / 1024.0 / 1024.0, max / 1024.0 / 1024.0);
log.warn("⚠️ {}", message);
// 这里可以扩展为发送邮件等告警
// sendEmail(message);
}
/**
* 发送系统异常告警
*/
public void sendSystemAlert(String message, Exception e) {
String alertMessage = String.format("系统异常告警: %s, 错误: %s", message, e.getMessage());
log.error("🚨 {}", alertMessage, e);
// 这里可以扩展为发送邮件、短信等告警
// sendEmail(alertMessage);
}
/**
* 发送业务告警
*/
public void sendBusinessAlert(String message) {
log.warn("📢 业务告警: {}", message);
// 这里可以扩展为发送邮件等告警
// sendEmail(message);
}
}

View File

@@ -1,92 +1,282 @@
package com.point.strategy.common;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicLong;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 内存监控工具类
* 用于监控JVM内存使用情况防止内存溢出
* 内存监控组件
* 用于监控JVM内存使用情况预防OOM问题
*/
@Slf4j
@Component
public class MemoryMonitor {
@Autowired(required = false)
private AlertService alertService;
// 内存告警计数器
private final AtomicInteger memoryWarningCount = new AtomicInteger(0);
private static final long MEMORY_THRESHOLD = 500 * 1024 * 1024; // 500MB阈值
private static final AtomicLong startTime = new AtomicLong();
private static final String OPERATION_NAME = "hookUpTwo";
// 严重内存告警计数器
private final AtomicInteger criticalMemoryWarningCount = new AtomicInteger(0);
/**
* 开始内存监控
* @param operation 操作名称
* 定时检查内存使用情况 - 每30秒执行一次
*/
public static void startMonitoring(String operation) {
startTime.set(System.currentTimeMillis());
Runtime runtime = Runtime.getRuntime();
long initialMemory = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024;
log.info("开始 {} - 初始内存使用: {}MB, 最大内存: {}MB",
operation, initialMemory, runtime.maxMemory() / 1024 / 1024);
}
/**
* 检查内存使用情况
*/
public static void checkMemoryUsage() {
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
long maxMemory = runtime.maxMemory();
long freeMemory = runtime.freeMemory();
long totalMemory = runtime.totalMemory();
if (usedMemory > MEMORY_THRESHOLD) {
log.warn("内存使用超过阈值: {}MB / {}MB (总内存: {}MB, 空闲: {}MB)",
usedMemory / 1024 / 1024, maxMemory / 1024 / 1024,
totalMemory / 1024 / 1024, freeMemory / 1024 / 1024);
forceGC();
}
}
/**
* 强制垃圾回收
*/
public static void forceGC() {
log.debug("执行强制垃圾回收...");
System.gc();
System.runFinalization();
@Scheduled(fixedRate = 30000)
public void checkMemoryUsage() {
try {
Thread.sleep(100); // 等待GC完成
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
MemoryUsage nonHeapUsage = memoryBean.getNonHeapMemoryUsage();
// 计算堆内存使用率
long heapUsed = heapUsage.getUsed();
long heapMax = heapUsage.getMax();
double heapUsagePercent = heapMax > 0 ? (double) heapUsed / heapMax : 0;
// 计算非堆内存使用率
long nonHeapUsed = nonHeapUsage.getUsed();
long nonHeapMax = nonHeapUsage.getMax();
double nonHeapUsagePercent = nonHeapMax > 0 ? (double) nonHeapUsed / nonHeapMax : 0;
// 记录内存使用情况
log.info("内存使用情况 - 堆内存: {}MB/{}MB ({}%), 非堆内存: {}MB/{}MB ({})",
formatMB(heapUsed), formatMB(heapMax), formatPercent(heapUsagePercent),
formatMB(nonHeapUsed), formatMB(nonHeapMax), formatPercent(nonHeapUsagePercent));
// 检查内存使用率并采取相应措施
handleMemoryUsage(heapUsagePercent, heapUsed, heapMax);
} catch (Exception e) {
log.error("内存监控检查失败", e);
}
}
/**
* 停止监控并输出结果
* 处理内存使用情况
*/
public static void stopMonitoring() {
long duration = System.currentTimeMillis() - startTime.get();
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
long maxMemory = runtime.maxMemory();
log.info("{} 操作完成 - 耗时: {}ms, 最终内存使用: {}MB / {}MB",
OPERATION_NAME, duration, usedMemory / 1024 / 1024, maxMemory / 1024 / 1024);
private void handleMemoryUsage(double usagePercent, long used, long max) {
if (usagePercent > 0.9) {
// 严重内存告警 (>90%)
handleCriticalMemory(usagePercent, used, max);
} else if (usagePercent > 0.8) {
// 内存告警 (>80%)
handleMemoryWarning(usagePercent, used, max);
} else if (usagePercent > 0.7) {
// 内存提醒 (>70%)
handleMemoryNotice(usagePercent);
}
// 重置计数器(每小时重置一次)
resetCountersIfNeeded();
}
/**
* 处理严重内存告警
*/
private void handleCriticalMemory(double usagePercent, long used, long max) {
int count = criticalMemoryWarningCount.incrementAndGet();
log.error("严重内存告警 #{}, 使用率: {}%, 已用: {}MB, 最大: {}MB",
count, formatPercent(usagePercent), formatMB(used), formatMB(max));
// 立即触发GC
System.gc();
// 发送告警
if (alertService != null) {
try {
alertService.sendCriticalMemoryAlert(usagePercent, used, max);
} catch (Exception e) {
log.error("发送严重内存告警失败", e);
}
}
// 如果连续3次严重告警记录堆栈信息
if (count >= 3) {
logMemoryStackTrace();
}
}
/**
* 处理内存告警
*/
private void handleMemoryWarning(double usagePercent, long used, long max) {
int count = memoryWarningCount.incrementAndGet();
log.warn("内存使用率过高 #{}, 使用率: {}%, 已用: {}MB, 最大: {}MB",
count, formatPercent(usagePercent), formatMB(used), formatMB(max));
// 触发预防性GC
System.gc();
// 发送告警
if (alertService != null && count % 2 == 0) { // 每2次告警发送一次
try {
alertService.sendMemoryWarning(usagePercent, used, max);
} catch (Exception e) {
log.error("发送内存告警失败", e);
}
}
}
/**
* 处理内存提醒
*/
private void handleMemoryNotice(double usagePercent) {
log.info("内存使用提醒: {}", formatPercent(usagePercent));
}
/**
* 记录内存堆栈信息
*/
private void logMemoryStackTrace() {
try {
// 获取所有线程的堆栈信息
Thread.getAllStackTraces().forEach((thread, stackTrace) -> {
if (thread.getState() == Thread.State.RUNNABLE) {
log.debug("活跃线程 {}: {}", thread.getName(), thread.getState());
for (StackTraceElement element : stackTrace) {
log.debug(" at {}", element);
}
}
});
} catch (Exception e) {
log.error("记录内存堆栈信息失败", e);
}
}
/**
* 重置计数器(每小时重置一次)
*/
private void resetCountersIfNeeded() {
// 简单实现每次告警数量达到10次时重置
if (memoryWarningCount.get() >= 10) {
memoryWarningCount.set(0);
criticalMemoryWarningCount.set(0);
log.info("内存告警计数器已重置");
}
}
/**
* 获取当前内存使用情况
* @return 内存使用信息
*/
public static String getMemoryInfo() {
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
long maxMemory = runtime.maxMemory();
long freeMemory = runtime.freeMemory();
long totalMemory = runtime.totalMemory();
return String.format("内存使用: %dMB/%dMB (总内存: %dMB, 空闲: %dMB)",
usedMemory / 1024 / 1024, maxMemory / 1024 / 1024,
totalMemory / 1024 / 1024, freeMemory / 1024 / 1024);
public MemoryInfo getCurrentMemoryInfo() {
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
MemoryUsage nonHeapUsage = memoryBean.getNonHeapMemoryUsage();
return MemoryInfo.builder()
.heapUsed(heapUsage.getUsed())
.heapMax(heapUsage.getMax())
.heapUsagePercent(heapUsage.getMax() > 0 ? (double) heapUsage.getUsed() / heapUsage.getMax() : 0)
.nonHeapUsed(nonHeapUsage.getUsed())
.nonHeapMax(nonHeapUsage.getMax())
.nonHeapUsagePercent(nonHeapUsage.getMax() > 0 ? (double) nonHeapUsage.getUsed() / nonHeapUsage.getMax() : 0)
.warningCount(memoryWarningCount.get())
.criticalWarningCount(criticalMemoryWarningCount.get())
.build();
}
/**
* 手动触发内存检查
*/
public void triggerMemoryCheck() {
log.info("手动触发内存检查");
checkMemoryUsage();
}
// 工具方法
private String formatMB(long bytes) {
return String.format("%.1f", bytes / 1024.0 / 1024.0);
}
private String formatPercent(double value) {
return String.format("%.1f%%", value * 100);
}
/**
* 内存信息DTO
*/
public static class MemoryInfo {
private long heapUsed;
private long heapMax;
private double heapUsagePercent;
private long nonHeapUsed;
private long nonHeapMax;
private double nonHeapUsagePercent;
private int warningCount;
private int criticalWarningCount;
// Builder pattern
public static Builder builder() {
return new Builder();
}
public static class Builder {
private MemoryInfo info = new MemoryInfo();
public Builder heapUsed(long heapUsed) {
info.heapUsed = heapUsed;
return this;
}
public Builder heapMax(long heapMax) {
info.heapMax = heapMax;
return this;
}
public Builder heapUsagePercent(double heapUsagePercent) {
info.heapUsagePercent = heapUsagePercent;
return this;
}
public Builder nonHeapUsed(long nonHeapUsed) {
info.nonHeapUsed = nonHeapUsed;
return this;
}
public Builder nonHeapMax(long nonHeapMax) {
info.nonHeapMax = nonHeapMax;
return this;
}
public Builder nonHeapUsagePercent(double nonHeapUsagePercent) {
info.nonHeapUsagePercent = nonHeapUsagePercent;
return this;
}
public Builder warningCount(int warningCount) {
info.warningCount = warningCount;
return this;
}
public Builder criticalWarningCount(int criticalWarningCount) {
info.criticalWarningCount = criticalWarningCount;
return this;
}
public MemoryInfo build() {
return info;
}
}
// Getters
public long getHeapUsed() { return heapUsed; }
public long getHeapMax() { return heapMax; }
public double getHeapUsagePercent() { return heapUsagePercent; }
public long getNonHeapUsed() { return nonHeapUsed; }
public long getNonHeapMax() { return nonHeapMax; }
public double getNonHeapUsagePercent() { return nonHeapUsagePercent; }
public int getWarningCount() { return warningCount; }
public int getCriticalWarningCount() { return criticalWarningCount; }
}
}

View File

@@ -28,6 +28,7 @@ import jxl.Workbook;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestBody;
@@ -45,6 +46,7 @@ import java.util.stream.Collectors;
@Component("docSimpleService")
@Slf4j
@Transactional
public class DocSimpleService {
//@Autowired
@@ -819,17 +821,78 @@ public class DocSimpleService {
packSqlObject.setOrderBy("order by file_name asc");
}
map.put("orderBy", !"".equals(packSqlObject.getOrderBy()) ? packSqlObject.getOrderBy() : "");
//将查询的数据加入redis缓存中
Cache<Object, Object> fiveSecondCache = guavaLocalCache.getFiveSecondCache();
fiveSecondCache.cleanUp();
fiveSecondCache.put("list", docSimpleMapper.selectObject(map));
// fiveSecondCache.invalidate();
// fiveSecondCache.getIfPresent();
// fiveSecondCache.cleanUp();
// redisUtil.del("list");
// redisUtil.lSet("list", docSimpleMapper.selectObject(map));
PageHelper.startPage(packSqlObject.getPage(), packSqlObject.getLimit());
return docSimpleMapper.selectObject(map);
// 添加分页限制防止OOM
Integer pageObj = packSqlObject.getPage();
Integer limitObj = packSqlObject.getLimit();
int page = pageObj != null ? pageObj : 1;
int limit = limitObj != null ? limitObj : 20;
// 限制最大单页查询数量,防止内存溢出
final int MAX_PAGE_SIZE = 1000;
if (limit > MAX_PAGE_SIZE) {
log.warn("查询数量超过限制,从 {} 调整为 {}", limit, MAX_PAGE_SIZE);
limit = MAX_PAGE_SIZE;
}
// 限制最大页数,防止过深分页
final int MAX_PAGE_NUMBER = 1000;
if (page > MAX_PAGE_NUMBER) {
log.warn("页数超过限制,从 {} 调整为 {}", page, MAX_PAGE_NUMBER);
page = MAX_PAGE_NUMBER;
}
// 检查内存使用情况
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
long maxMemory = runtime.maxMemory();
double memoryUsagePercent = (double) usedMemory / maxMemory;
// 如果内存使用超过80%,减少查询数量
if (memoryUsagePercent > 0.8) {
limit = Math.min(limit, 50); // 高内存使用时限制为50条
log.warn("内存使用率过高({}%),限制查询数量为 {}",
String.format("%.1f", memoryUsagePercent * 100), limit);
}
// 使用分页查询
PageHelper.startPage(page, limit);
List<Map<String, Object>> result;
try {
result = docSimpleMapper.selectObject(map);
// 检查结果集大小,防止返回过多数据
if (result != null && result.size() > MAX_PAGE_SIZE) {
log.warn("查询结果超过限制,截取前 {} 条记录", MAX_PAGE_SIZE);
result = result.subList(0, MAX_PAGE_SIZE);
}
} catch (Exception e) {
log.error("查询数据失败,可能是内存不足: {}", e.getMessage());
// 降级处理:返回少量数据
PageHelper.startPage(1, 10);
try {
result = docSimpleMapper.selectObject(map);
log.info("降级查询成功,返回 {} 条记录", result != null ? result.size() : 0);
} catch (Exception fallbackException) {
log.error("降级查询也失败: {}", fallbackException.getMessage());
return new ArrayList<>(); // 返回空列表,避免系统崩溃
}
}
// 缓存小结果集
if (result != null && result.size() <= 100) {
try {
Cache<Object, Object> fiveSecondCache = guavaLocalCache.getFiveSecondCache();
fiveSecondCache.cleanUp();
fiveSecondCache.put("list", result);
} catch (Exception e) {
log.warn("缓存查询结果失败: {}", e.getMessage());
}
}
return result;
}
public boolean isRankJudgment(String table, String level) {

View File

@@ -28,6 +28,8 @@ import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.poi.hssf.usermodel.HSSFCell;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CompletableFuture;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
@@ -55,6 +57,7 @@ import java.util.*;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
public class FileManageService {