帆软报表(FineReport)是企业级报表开发中最常用的工具之一,但随着数据量增长和并发上升,看板加载慢、内存溢出、查询超时等问题频发。本文从模板设计、数据准备、服务器配置、缓存策略到集群部署,全方位拆解 FineReport 性能优化方案。
目录
FineReport 性能瓶颈全景
数据准备层优化
模板设计层优化
图表与可视化优化
SQL 查询优化专题
数据集与参数优化
缓存策略深度解析
服务器与 JVM 调优
引擎配置优化
大并发场景优化
大数据量处理方案
集群部署与负载均衡
监控与诊断体系
常见反模式与避坑
实战案例集锦
一、FineReport 性能瓶颈全景 1.1 请求全链路分解 1 2 3 用户请求 → Web服务器(Tomcat) → 设计器引擎 → 数据集执行 → 数据库查询 ↓ 模板渲染 → 图表计算 → 分页处理 → HTML输出 → 浏览器渲染
每一层都可能成为瓶颈,优化需要从端到端全局分析 。
1.2 性能瓶颈分布(按影响程度排序)
优先级
瓶颈来源
占比
典型现象
P0
SQL 查询慢、数据集设计不当
45%
报表打开 10s+,数据加载中菊花转不停
P0
模板设计过于复杂
25%
大量单元格公式、行式引擎未开启
P1
JVM / 服务器配置不合理
12%
频繁 Full GC、内存溢出
P1
图表渲染数据量过大
8%
图表区域白屏/卡死
P2
未启用缓存
5%
相同查询反复访问数据库
P2
并发控制不足
3%
高峰时段全站崩溃
P3
网络与浏览器端
2%
页面加载慢但服务器负载低
1.3 快速诊断命令 1 2 3 4 5 SELECT * FROM information_schema.processlist WHERE info LIKE '%SELECT%' ;grep "数据集执行" / opt/ tomcat/ logs/ catalina.out | grep - E "[0-9]{4,} ms"
1 2 3 4 5 6 7 8 jstat -gcutil $(pgrep -f tomcat) 1000 10 curl -s http://localhost:8080/manager/status?XML=true | grep currentThreadCount grep "Cost:" /opt/tomcat/webapps/webroot/WEB-INF/log/finework.log
二、数据准备层优化 2.1 数据集设计原则 原则一:数据集只取需要的列
1 2 3 4 5 6 7 8 9 SELECT * FROM order_detail WHERE create_time >= '${start_time}' SELECT order_id, customer_name, product_name, quantity, unit_price, total_amount, create_time FROM order_detail WHERE create_time >= '${start_time}'
危害 :FineReport 会将全部列读入内存,无用的 TEXT/BLOB 字段会严重拖垮性能。
原则二:数据集数量最小化
1 2 3 4 5 6 7 8 9 10 11 12 SELECT a.field1 , a.field2 , b.field3 , b.field4 , c.field5 , c.field6 FROM table_a aLEFT JOIN table_b b ON a.id = b.a_id LEFT JOIN table_c c ON a.id = c.a_id WHERE a.create_date = '${date}'
原则三:大数据量用存储过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 DELIMITER $$ CREATE PROCEDURE sp_dashboard_data( IN p_start_date DATE , IN p_end_date DATE ) BEGIN DROP TEMPORARY TABLE IF EXISTS tmp_summary; CREATE TEMPORARY TABLE tmp_summary AS SELECT category_id, COUNT (* ) AS order_cnt, SUM (amount) AS total_amt FROM orders WHERE create_time BETWEEN p_start_date AND p_end_date GROUP BY category_id; SELECT c.category_name, t.order_cnt, t.total_amt, t.total_amt / c.target_amount * 100 AS completion_rate FROM tmp_summary t JOIN category_dim c ON t.category_id = c.id ORDER BY t.total_amt DESC ; DROP TEMPORARY TABLE IF EXISTS tmp_summary; END $$DELIMITER ;
2.2 数据源连接池配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <JNDIName > java:comp/env/jdbc/reportDB</JNDIName > <initialSize > 10</initialSize > <minIdle > 10</minIdle > <maxActive > 100</maxActive > <maxWait > 10000</maxWait > <timeBetweenEvictionRunsMillis > 60000</timeBetweenEvictionRunsMillis > <validationQuery > SELECT 1</validationQuery > <testWhileIdle > true</testWhileIdle > <testOnBorrow > false</testOnBorrow > <testOnReturn > false</testOnReturn >
2.3 数据字典与码表缓存化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 SELECT o.* , d1.name AS status_name, d2.name AS source_name FROM orders oJOIN dict_status d1 ON o.status = d1.codeJOIN dict_source d2 ON o.source = d2.code
2.4 数据预聚合与宽表设计 1 2 3 4 5 6 7 8 9 10 11 12 13 OLTP 模型(规范化) OLAP 宽表(反规范化) ┌──────────┐ ┌─────────────────────────┐ │ orders │ │ dw_order_fact │ ├──────────┤ ├─────────────────────────┤ │ id │ │ order_id │ │ user_id │──→ users │ order_no │ │ status │ │ user_name (冗余) │ │ amount │ │ city (冗余) │ │ time │ │ amount │ └──────────┘ │ status_name (冗余) │ │ category_name (冗余) │ │ year/month/day (派生列) │ └─────────────────────────┘
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 CREATE TABLE dw_dashboard_daily AS SELECT DATE (o.create_time) AS stat_date, u.city, u.user_level, c.category_name, COUNT (DISTINCT o.id) AS order_cnt, COUNT (DISTINCT o.user_id) AS user_cnt, SUM (o.amount) AS total_amount, AVG (o.amount) AS avg_amount, SUM (o.amount) / COUNT (DISTINCT o.user_id) AS arpu FROM orders oJOIN users u ON o.user_id = u.idJOIN product_sku s ON o.sku_id = s.idJOIN product_category c ON s.category_id = c.idWHERE o.create_time >= CURDATE() - INTERVAL 1 DAY GROUP BY DATE (o.create_time), u.city, u.user_level, c.category_name;
三、模板设计层优化 3.1 行式引擎 —— 大数据量的救星
这是 FineReport 最重要的性能开关之一,必须了解!
行式引擎 vs 普通引擎对比
特性
普通引擎
行式引擎
加载方式
全部数据加载到内存
逐行流式读取
内存占用
高(随数据量线性增长)
极低(近乎恒定)
适合场景
数据量 < 5000 行
数据量 ≥ 5000 行
分页支持
不支持
支持大分页
聚合函数
支持
不支持 SUM/AVG 等
单元格扩展
支持
有限支持
图表联动
支持
不支持
导出
慢
快
开启方式 :模板 → 模板属性 → 报表引擎 → 勾选「启用行式引擎」
1 2 3 4 5 数据量阈值建议: < 2000 行 → 普通引擎即可 2000-5000 → 根据复杂度决定 5000-10W → 强烈推荐行式引擎 > 10W → 必须行式引擎 + 数据库端分页
行式引擎下的注意事项 :
1 2 3 4 5 6 7 8 9 10 11 12 SELECT category, SUM(amount) AS total FROM orders GROUP BY category
3.2 单元格公式优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 SELECT order_id, amount, CASE WHEN amount > 100 THEN amount * 0.9 ELSE amount END AS discount_amount, (SELECT name FROM dict WHERE code = status) AS status_name FROM orders
公式性能排名(从快到慢) :
1 2 3 4 5 6 1. 简单运算符 (+, -, *, /) ← 最快 2. IF / SWITCH ← 快 3. INARRAY / GREPARRAY ← 中等 4. MAP / JOINARRAY ← 较慢 5. VLOOKUP / HIERARCHY ← 慢 6. SELECT() 数据集函数 ← 最慢(会重新查 DB!)
1 2 3 4 5 6 7 8 9 =sql ("datasource" , "SELECT name FROM users WHERE id = " + A1 , 1 , 1 )
3.3 分页策略 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 SELECT * FROM orders WHERE create_time >= '${start_time}' ORDER BY id LIMIT ${pageSize} OFFSET (${pageNumber} - 1) * ${pageSize}
3.4 模板拆分策略 1 2 3 4 5 6 7 8 9 10 11 12 13 单一巨型模板(❌) 拆分后的模板组(✅) ┌──────────────────┐ ┌──────────────┐ ← 主看板(关键指标) │ 主 KPI 指标 │ │ KPI 卡片区 │ │ 趋势图表 ×4 │ │ 核心指标趋势 │ │ 明细列表 5000行 │ └──────────────┘ │ 排行 TOP 10 │ ┌──────────────┐ ← 明细页(行式引擎) │ 地图分布 │ │ 明细列表 │ │ 交叉透视表 │ │ 条件筛选 │ │ 钻取子报表 │ └──────────────┘ └──────────────────┘ ┌──────────────┐ ← 图表分析页 │ 图表 ×4 │ │ 排行 ×3 │ └──────────────┘
拆分原则 :
每种查询类型独立一个模板(列表用行式引擎,图表用普通引擎)
KPI 卡片单独加载(数据量小、查询快,首屏体验好)
通过超链接或 Tab 切换关联子模板
子模板可共用数据集(通过参数传递)
3.5 懒加载与异步加载 1 2 3 4 5 6 7 8 9 10 11 12 13 setTimeout (function ( ) { var detailFrame = document .createElement ('iframe' ); detailFrame.src = '/webroot/decision/view/report?viewlet=detail.cpt&orderId=' + orderId; document .getElementById ('detail-container' ).appendChild (detailFrame); }, 500 );
四、图表与可视化优化 4.1 图表数据量控制
图表的性能瓶颈主要在前端渲染数据点数量 和数据集返回大小。
1 2 3 4 5 6 7 折线图/柱状图 → 数据点 ≤ 2000 (超出建议采样) 饼图 → 分类 ≤ 50 (超出合并为"其他" ) 散点图 → 数据点 ≤ 10000 地图 → 区域 ≤ 500 漏斗图 → 数据点 ≤ 20 仪表盘 → 无限制(数据量固定)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 SELECT * FROM ( SELECT * , ROW_NUMBER () OVER (ORDER BY time ) AS rn FROM metrics WHERE time >= '${start}' ) t WHERE rn % 10 = 0 SELECT category, total FROM ( SELECT category, total, 1 AS sort_order FROM category_stats ORDER BY total DESC LIMIT 10 UNION ALL SELECT '其他' AS category, SUM (total) AS total, 2 AS sort_order FROM category_stats WHERE category NOT IN (SELECT category FROM category_stats ORDER BY total DESC LIMIT 10 ) ) t ORDER BY sort_order, total DESC
4.2 图表数据源复用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 SELECT category, SUM (amount) AS total_amount, COUNT (*) AS order_count, AVG (amount) AS avg_amount FROM orders WHERE create_time >= '${start}' GROUP BY category
4.3 ECharts / 第三方图表集成优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 option = { series : [{ type : 'line' , sampling : 'lttb' , large : true , largeThreshold : 500 , progressive : 400 , progressiveThreshold : 3000 }], dataset : { source : dataArray } }; var worker = new Worker ('chart-data-worker.js' );worker.postMessage ({ rawData : rawData }); worker.onmessage = function (e ) { chart.setOption ({ dataset : { source : e.data } }); };
4.4 原生图表 vs 第三方图表
场景
推荐方案
理由
简单柱状/折线/饼图
FineReport 原生图表
配置快,无需额外开发
数据点 > 2000
ECharts + LTTB采样
原生图表大数据量卡顿
高度自定义样式
ECharts / Highcharts
原生图表样式自由度低
实时刷新(秒级)
ECharts + WebSocket
原生刷新慢,重渲染开销大
地图可视化
ECharts + 自定义 GeoJSON
原生地图功能有限
3D 图表
ECharts GL / Three.js
原生不支持
五、SQL 查询优化专题
详见上一篇 SQL优化完整指南 ,此处聚焦 FineReport 特有的 SQL 优化场景。
5.1 参数化查询防 SQL 注入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 SELECT * FROM orders WHERE user_id = '${user_id}' SELECT * FROM orders WHERE user_id = '${user_id}' SELECT * FROM orders WHERE user_id = ?{CALL sp_get_orders(?)}
5.2 避免在 WHERE 中使用 FineReport 函数 1 2 3 4 5 6 7 SELECT * FROM orders WHERE YEAR (create_time) = ${year }SELECT * FROM orders WHERE create_time >= '${start_date}' AND create_time <= '${end_date}'
5.3 IN 参数处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 SELECT * FROM orders WHERE city IN ('${cities}' )SELECT * FROM orders WHERE FIND_IN_SET(city, '${cities}' )SELECT * FROM orders WHERE city IN (${cities})CREATE TEMPORARY TABLE tmp_cities (city VARCHAR (50 ));SELECT o.* FROM orders o JOIN tmp_cities t ON o.city = t.city;
5.4 分页优化(配合行式引擎) 1 2 3 4 5 6 7 8 SELECT * FROM orders ORDER BY id LIMIT 10000 , 50 SELECT * FROM orders WHERE id > ${last_id} ORDER BY id LIMIT 50
六、数据集与参数优化 6.1 参数联动优化
6.2 控件默认值不触发查询
6.3 自定义参数面板 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var timer = null ;document .getElementById ('search-input' ).addEventListener ('input' , function ( ) { clearTimeout (timer); timer = setTimeout (function ( ) { _FR.doClick ('查询' ); }, 300 ); });
七、缓存策略深度解析 7.1 FineReport 缓存层级 1 2 3 4 5 6 7 8 9 10 11 ┌─────────────────────────┐ 1. 浏览器缓存 │ HTTP 304 / localStorage │ 静态资源、模板文件 ├─────────────────────────┤ │ 2. 模板结果缓存 │ 整个模板 HTML 缓存,相同参数直接返回 ├─────────────────────────┤ │ 3. 数据集缓存 │ 数据集查询结果缓存(最常用) ├─────────────────────────┤ │ 4. Redis / 外部缓存 │ 跨节点共享(集群必用) ├─────────────────────────┤ │ 5. 数据库缓存 │ MySQL Buffer Pool / 查询缓存 └─────────────────────────┘
7.2 模板结果缓存配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <cacheConfig > <cacheType > memory</cacheType > <maxEntries > 1000</maxEntries > <timeToLive > 600</timeToLive > <timeToIdle > 300</timeToIdle > </cacheConfig >
7.3 数据集缓存
7.4 Redis 缓存集成 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 redis.host=192.168.1.100 redis.port=6379 redis.password=your_password redis.database=0 redis.maxTotal=200 redis.maxIdle=50 redis.minIdle=10 redis.maxWaitMillis=5000 redis.testOnBorrow=true redis.expire.seconds=1800
1 2 3 4 5 <cacheConfig > <cacheType > redis</cacheType > <timeToLive > 1800</timeToLive > </cacheConfig >
7.5 缓存更新策略 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public ReportData getCachedReport (String templateId, Map<String, String> params) { String cacheKey = "report:" + templateId + ":" + params.hashCode(); ReportData data = redis.get(cacheKey); if (data != null ) return data; String lockKey = "lock:" + cacheKey; if (redis.setnx(lockKey, 1 , 30 )) { try { data = generateReport(templateId, params); redis.setex(cacheKey, 600 , data); } finally { redis.del(lockKey); } } else { Thread.sleep(100 ); data = redis.get(cacheKey); } return data; }
八、服务器与 JVM 调优 8.1 JVM 参数优化(最关键的服务器层配置) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 -Xms8g -Xmx8g -Xmn3g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=8m -XX:ParallelGCThreads=8 -XX:ConcGCThreads=2 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/opt/tomcat/logs/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=50M -XX:+DisableExplicitGC -XX:+AlwaysPreTouch -XX:+UseStringDeduplication -Xss512k
JVM 内存分配建议 :
服务器总内存
JVM 堆内存
说明
8G
4G - 5G
小型部署,< 50 并发
16G
8G - 10G
中型部署,50-200 并发
32G
16G - 20G
大型部署,200-500 并发
64G+
24G - 32G
注意:堆 > 32G 时指针压缩失效,需权衡
8.2 Tomcat 线程池配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <Connector port ="8080" protocol ="org.apache.coyote.http11.Http11Nio2Protocol" <!-- ====== 核心配置 ====== -- > maxThreads="500" minSpareThreads="50" connectionTimeout="20000" maxConnections="10000" compression="on" compressionMinSize="2048" compressableMimeType="text/html,text/xml,text/plain,text/css,text/javascript,application/json,application/javascript" enableLookups="false" acceptCount="200" asyncTimeout="30000" /> <Connector port ="8009" protocol ="AJP/1.3" maxThreads ="500" connectionTimeout ="20000" packetSize ="65536" />
8.3 Tomcat Session 优化 1 2 3 4 5 6 7 8 9 10 <Context > <SessionTimeout > 30</SessionTimeout > <CookieProcessor className ="org.apache.tomcat.util.http.Rfc6265CookieProcessor" sameSiteCookies ="lax" /> </Context >
1 2 3 4 5 <session-config > <session-timeout > 30</session-timeout > </session-config >
8.4 Nginx 反向代理层优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 upstream finereport_backend { ip_hash; server 192.168.1.101:8080 weight=5 max_fails=3 fail_timeout=30s ; server 192.168.1.102:8080 weight=5 max_fails=3 fail_timeout=30s ; server 192.168.1.103:8080 weight=5 max_fails=3 fail_timeout=30s ; keepalive 64 ; } server { listen 80 ; server_name report.yourcompany.com; client_max_body_size 50m ; gzip on ; gzip_min_length 1k ; gzip_comp_level 6 ; gzip_types text/plain text/css text/javascript application/json application/javascript application/xml text/xml image/svg+xml; gzip_vary on ; gzip_proxied any; location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf)$ { proxy_pass http://finereport_backend; expires 7d ; add_header Cache-Control "public, immutable" ; } location /webroot/decision/ { proxy_pass http://finereport_backend; proxy_set_header Host $host ; proxy_set_header X-Real-IP $remote_addr ; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ; proxy_set_header X-Forwarded-Proto $scheme ; proxy_connect_timeout 60s ; proxy_send_timeout 300s ; proxy_read_timeout 300s ; proxy_http_version 1 .1 ; proxy_set_header Connection "" ; proxy_buffering on ; proxy_buffer_size 16k ; proxy_buffers 32 16k ; proxy_busy_buffers_size 32k ; proxy_max_temp_file_size 256m ; } }
九、引擎配置优化 9.1 FineReport 引擎参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <runConfig > <exportTimeout > 300000</exportTimeout > <cellCalcMaxCount > 10000000</cellCalcMaxCount > <reportTimeout > 600000</reportTimeout > <maxDataRow > 100000</maxDataRow > <excelMaxRow > 200000</excelMaxRow > <csvEncode > UTF-8</csvEncode > </runConfig >
9.2 调度任务优化
9.3 平台参数配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 login.concurrent.max =200 platform.timeout =1800 export.queue.size =100 session.max =500 template.preload =true template.preload.list =/dashboard/index.cpt,/dashboard/sales.cpt
十、大并发场景优化 10.1 并发场景分类
场景
并发量
特点
策略
管理看板
5-20
高级管理者定时查看
定时预计算 + 缓存
业务报表
50-200
业务人员工作时段集中访问
限流 + 缓存 + 分组
大屏展示
1-10
长时间展示,定时刷新
缓存 + 增量更新
对外公开报表
500-5000
不可控,峰值高
CDN + 静态化 + 限流
10.2 限流策略 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 http { limit_req_zone $binary_remote_addr zone=report:10m rate=10r/s; limit_conn_zone $binary_remote_addr zone=addr:10m; server { location /webroot/decision/ { limit_req zone=report burst=20 nodelay; limit_conn addr 5 ; proxy_pass http: } } }
1 2 3 4 5 6 7 8 9 10 11 12 import com.google.common.util.concurrent.RateLimiter;public class ReportRateLimiter { private static final RateLimiter LIMITER = RateLimiter.create(100 ); public static boolean tryAcquire () { return LIMITER.tryAcquire(5 , TimeUnit.SECONDS); } }
10.3 排队机制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 function exportLargeReport ( ) { $.ajax ({ url : '/webroot/decision/view/export' , data : { reportId : 'xxx' , format : 'excel' }, success : function (res ) { if (res.status === 'queued' ) { var taskId = res.taskId ; pollExportStatus (taskId); } } }); } function pollExportStatus (taskId ) { var timer = setInterval (function ( ) { $.get ('/webroot/decision/view/export/status?taskId=' + taskId, function (res ) { if (res.status === 'completed' ) { clearInterval (timer); window .open (res.downloadUrl ); } else if (res.status === 'failed' ) { clearInterval (timer); alert ('导出失败,请重试' ); } }); }, 3000 ); }
10.4 读写分离 + 只读副本 1 2 3 4 5 6 7 8 9 10 11 12 readonly.datasource.url=jdbc:mysql:replication://master:3306,slave1:3306,slave2:3306/report_db readonly.datasource.username=report_readonly readonly.datasource.password=xxx
十一、大数据量处理方案 11.1 数据量分级策略
级别
数据量
策略
小数据量
< 1 万行
普通引擎,全量加载,交互丰富
中等数据量
1-10 万行
行式引擎 + 分页查询
大数据量
10-100 万行
行式引擎 + 数据库分页 + 游标
海量数据量
> 100 万行
预聚合 + ES/ClickHouse/StarRocks
11.2 预聚合方案 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 CREATE TABLE report_daily_stats ( stat_date DATE NOT NULL , dim1_id INT NOT NULL , dim2_id INT NOT NULL , metric1 DECIMAL (15 ,2 ), metric2 DECIMAL (15 ,2 ), metric3 INT , PRIMARY KEY (stat_date, dim1_id, dim2_id) ) ENGINE= InnoDB; INSERT INTO report_daily_statsSELECT DATE (order_time) AS stat_date, category_id AS dim1_id, city_id AS dim2_id, SUM (amount) AS metric1, COUNT (* ) AS metric2, COUNT (DISTINCT user_id) AS metric3 FROM ordersWHERE DATE (order_time) = CURDATE() - INTERVAL 1 DAY GROUP BY stat_date, dim1_id, dim2_id;
11.3 异构数据源方案 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 传统架构 优化架构 ┌─────────┐ ┌─────────────┐ │ MySQL │ ← 报表直接查询 │ MySQL │ ← 业务库、OLTP │ (OLTP) │ 大数据量 → 超时/OOM │ (OLTP) │ └─────────┘ └──────┬──────┘ │ Binlog / ETL ┌──────▼──────┐ │ ClickHouse │ ← 报表/分析查询(OLAP) │ / StarRocks │ 列式存储,查询 ms 级 └──────┬──────┘ │ ┌──────▼──────┐ │ FineReport │ │ 数据源指向 │ │ ClickHouse │ └─────────────┘
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 CREATE TABLE report_order_wide ( stat_date Date , order_id UInt64, user_name String, category_name String, city String, amount Decimal (12 , 2 ), quantity UInt32, status UInt8 ) ENGINE = MergeTree() PARTITION BY toYYYYMM(stat_date)ORDER BY (stat_date, category_name, city)SETTINGS index_granularity = 8192 ; SELECT category_name, city, SUM (amount) AS total_amount, COUNT (DISTINCT user_name) AS user_cnt FROM report_order_wideWHERE stat_date BETWEEN '2024-01-01' AND '2024-01-31' GROUP BY category_name, cityORDER BY total_amount DESC LIMIT 20 ;
11.4 导出优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public void asyncExport (String taskId, String templateId, Map<String, Object> params) { int pageSize = 5000 ; int pageNum = 1 ; int totalRows = getTotalRows(params); SXSSFWorkbook workbook = new SXSSFWorkbook (100 ); Sheet sheet = workbook.createSheet(); int rowIdx = 0 ; while ((pageNum - 1 ) * pageSize < totalRows) { List<Map<String, Object>> batch = queryPage(pageNum, pageSize, params); for (Map<String, Object> row : batch) { rowIdx++; } pageNum++; workbook.setCompressTempFiles(true ); } String ossUrl = ossClient.upload(taskId + ".xlsx" , workbook); notifyUser(taskId, ossUrl); }
十二、集群部署与负载均衡 12.1 集群架构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ┌──────────────┐ │ Nginx LB │ ← 入口 │ (SSL 终结) │ └──┬───┬───┬──┘ │ │ │ ┌───────────┼───┼───┼───────────┐ │ │ │ │ │ ┌──────▼──┐ ┌─────▼───▼──▼─────┐ │ │ Redis │ │ 共享 NAS/OSS │ │ │ Session │ │ 模板文件/附件 │ │ │ Cache │ └──────────────────┘ │ └──────▲──┘ │ │ │ ┌──────┴──────────────────────┐ │ │ MySQL (主从) │ │ └─────────────────────────────┘ │ │ │ │ │ ┌──────▼──┐ ┌───▼────┐ ┌─▼──────┐ │ │ FR 节点1│ │FR 节点2│ │FR 节点3│ │ │ Tomcat │ │Tomcat │ │Tomcat │ │ └─────────┘ └────────┘ └────────┘
12.2 集群关键配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <clusterConfig > <enable > true</enable > <nodeId > node-192-168-1-101</nodeId > <protocol > tcp</protocol > <nodes > <node > 192.168.1.101:7800</node > <node > 192.168.1.102:7800</node > <node > 192.168.1.103:7800</node > </nodes > <heartBeatInterval > 5000</heartBeatInterval > <nodeTimeout > 30000</nodeTimeout > </clusterConfig >
1 2 3 4 5 6 7 8 9 <clusterConfig > <syncTemplate > true</syncTemplate > <syncInterval > 300</syncInterval > </clusterConfig >
1 2 3 4 5 <cacheConfig > <cacheType > redis</cacheType > </cacheConfig >
12.3 集群环境下的注意事项
问题
说明
解决方案
Session 共享
不同节点登录状态不互通
Redis Session 共享 / ip_hash
模板一致性
节点间模板版本不一致
NAS 共享 / 自动同步
缓存一致性
节点 A 更新了缓存,节点 B 未感知
Redis 集中缓存
定时任务重复
多个节点同时执行同一调度
分布式锁(Redis SETNX)
文件导出
导出到节点本地,用户找不到文件
统一 OSS/MinIO
日志聚合
各节点日志分散
ELK / Loki 集中收集
十三、监控与诊断体系 13.1 FineReport 内置监控
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 grep -E "Cost: [0-9]{5,}" /opt/tomcat/logs/catalina.out grep "OutOfMemoryError" /opt/tomcat/logs/catalina.out grep "Cannot get a connection" /opt/tomcat/logs/catalina.out grep "view/report" /opt/tomcat/logs/localhost_access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -20 grep "Cost:" /opt/tomcat/logs/catalina.out | awk -F'Cost:' '{print $2}' | sort -rn | head -10
13.2 JVM 监控 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 jstat -gcutil $(pgrep -f tomcat) 1000 jmap -dump:live,format=b,file=/tmp/heap.hprof $(pgrep -f tomcat) jstack $(pgrep -f tomcat) > /tmp/thread_dump.txt jinfo -flags $(pgrep -f tomcat)
13.3 自定义监控指标 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class PerformanceFilter implements GlobalRequestFilter { @Override public void doFilter (HttpServletRequest request, HttpServletResponse response, FilterChain chain) { long start = System.currentTimeMillis(); String templateId = request.getParameter("viewlet" ); try { chain.doFilter(request, response); } finally { long cost = System.currentTimeMillis() - start; if (cost > 3000 ) { logger.warn("[SLOW] template={}, cost={}ms, params={}" , templateId, cost, request.getQueryString()); } reportLatencyHistogram.observe(cost / 1000.0 ); } } }
13.4 Prometheus + Grafana 监控看板 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 rules: - pattern: 'Catalina<type=ThreadPool, name="http-nio-8080"><>(\w+):' name: tomcat_threadpool labels: connector: http-nio-8080 - pattern: 'java.lang<type=Memory><(\w+)>(\w+)' name: jvm_memory_$1_$2 - pattern: 'com.fr<type=ReportMetrics><>(\w+): (\d+)' name: finereport_$1
十四、常见反模式与避坑 反模式 1:所有数据放在一个数据集
反模式 2:条件属性 vs 公式不分
反模式 3:填报 + 查询混在一个模板
反模式 4:每个单元格轮询数据库 1 2 3 4 5 6 7 8 =A1 + sql ("ds" , "SELECT stock FROM inventory WHERE sku_id = " + A1 , 1 , 1 ) SELECT o.*, i.stock FROM orders oLEFT JOIN inventory i ON o.sku_id = i.sku_id
反模式 5:图表不设数据上限
反模式 6:忽略模板预加载 1 2 3 4 5 6 7 8 template.preload=true template.preload.list=/dashboard/index.cpt,/dashboard/sales.cpt,/report/daily.cpt
反模式 7:Tomcat 默认参数跑生产
反模式 8:报表导出无限制
十五、实战案例集锦 案例 1:电商运营看板 —— 从 15s 到 0.3s 原始场景 :运营大屏,包含 4 个 KPI 卡片、3 个趋势图、1 个订单明细列表、1 个 TOP10 排行。
1 2 3 4 5 6 7 原始配置: - 1 个大数据集(220 列,关联 8 张表) - 全部图表和列表共享这个数据集 - 某天全量数据(150W 行) - 普通引擎,无分页 加载时间:15s(超时风险极高)
优化方案 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 SELECT SUM (amount) AS total_gmv, COUNT (DISTINCT user_id) AS uv, COUNT (* ) AS order_cnt, SUM (amount) / COUNT (DISTINCT user_id) AS arpu FROM orders WHERE DATE (create_time) = CURDATE();SELECT DATE (create_time) AS dt, SUM (amount) AS gmvFROM orders WHERE create_time >= CURDATE() - INTERVAL 30 DAY GROUP BY dt ORDER BY dt;SELECT category_name, SUM (amount) AS totalFROM order_detail WHERE DATE (create_time) = CURDATE()GROUP BY category_name ORDER BY total DESC LIMIT 10 ;SELECT id, customer_name, product_name, quantity, amountFROM orders WHERE DATE (create_time) = CURDATE()ORDER BY id LIMIT 50 ;
优化效果 :
指标
优化前
优化后
首屏加载
15s
0.3s
KPI 面板
随主查询
0.1s(缓存命中)
明细列表
一次性 150W
分页 50 行/页
内存占用
4.2G
800M
并发能力
< 10
> 100
案例 2:财务月报 —— 30s 超时到 1.2s 问题 :月报涉及多表关联(订单、退款、费用、结算),30+ 个汇总指标,执行 30 秒超时。
分析 :
4 张核心表关联(每张 200W-500W 行)
多个子查询嵌套
实时关联计算
优化方案 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 CREATE TABLE fin_monthly_summary ( stat_month CHAR (7 ) NOT NULL COMMENT '统计月份 YYYY-MM' , dept_id INT NOT NULL , biz_line_id INT NOT NULL , order_amount DECIMAL (18 ,2 ) DEFAULT 0 COMMENT '订单金额' , order_cnt INT DEFAULT 0 COMMENT '订单数' , return_amount DECIMAL (18 ,2 ) DEFAULT 0 COMMENT '退款金额' , return_cnt INT DEFAULT 0 COMMENT '退款单数' , net_amount DECIMAL (18 ,2 ) DEFAULT 0 COMMENT '净收入' , shipping_fee DECIMAL (18 ,2 ) DEFAULT 0 COMMENT '运费' , platform_fee DECIMAL (18 ,2 ) DEFAULT 0 COMMENT '平台费用' , gross_profit DECIMAL (18 ,2 ) DEFAULT 0 COMMENT '毛利润' , profit_rate DECIMAL (6 ,4 ) DEFAULT 0 COMMENT '利润率' , PRIMARY KEY (stat_month, dept_id, biz_line_id) ) ENGINE= InnoDB; INSERT INTO fin_monthly_summarySELECT ... FROM ... GROUP BY ...;
1 2 3 优化前:实时 JOIN 4 张大表 → 30s 优化后:查询预计算汇总表 → 0.8-1.2s 月度数据误差:T+1(次日更新),可接受
案例 3:大屏展示 —— 自动刷新的资源消耗 场景 :双 11 实时大屏,每 5 秒自动刷新,8 小时不间断展示。
问题 :
每次都重新计算全量数据
高峰期数据库压力巨大
浏览器内存持续增长(页面不关闭)
优化方案 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 SELECT * FROM realtime_metrics WHERE update_time >= '${last_refresh_time}' var ws = new WebSocket ('wss://report-api.yourcompany.com/ws/dashboard' );ws.onmessage = function (event ) { var delta = JSON .parse (event.data ); document .getElementById ('gmv' ).innerText = delta.gmv ; document .getElementById ('uv' ).innerText = delta.uv ; chart.setOption ({ dataset : { source : delta.chartData } }); };
优化效果 :
指标
轮询方案
WebSocket + 增量方案
每秒 DB 查询
100次
0次(按变化推送)
网络传输
100MB/h
5MB/h
浏览器内存
持续增长到 2G+
稳定在 300M
案例 4:集群环境下的数据不一致 场景 :3 节点集群,用户修改筛选条件后发现数据不一致。
问题 :
节点 A 缓存了”本月”数据
节点 B 缓存了”上月”数据
由于 ip_hash 可能路由到不同节点
解决方案 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 redis: host: 192.168 .1 .100 port: 6379 maxTotal: 200 cacheExpire: 600
附录 A:FineReport 优化速查清单 日常开发检查
上线前检查
附录 B:常用命令速查 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 jstat -gcutil $(pgrep -f tomcat) 1000 jinfo -flags $(pgrep -f tomcat) jmap -dump:live,format=b,file=/tmp/heap.hprof $(pgrep -f tomcat) jstack $(pgrep -f tomcat) > /tmp/threads.txt curl -s http://localhost:8080/manager/status?XML=true | grep -E "currentThread|currentThreadsBusy" grep "Cost:" /opt/tomcat/logs/catalina.out | sort -t: -k2 -rn | head -10 grep "view/report" /opt/tomcat/logs/localhost_access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -10 grep -E "ERROR|Exception" /opt/tomcat/logs/catalina.out | tail -50 mysql -u root -p -e " SELECT id, user, host, db, time, state, SUBSTRING(info, 1, 100) AS query_preview FROM information_schema.processlist WHERE command != 'Sleep' AND info LIKE '%SELECT%' ORDER BY time DESC " mysql -u root -p -e "SELECT * FROM information_schema.innodb_lock_waits" mysql -u root -p -e "SHOW VARIABLES LIKE 'slow_query%'"
本文基于 FineReport 10.0/11.0 版本编写,结合实际项目中上百个看板的优化经验总结而成。建议收藏作为报表开发日常参考手册,并在每个报表上线前对照检查清单逐项确认。