职业杀手
26.53M · 2026-03-25
这篇文章不是一份性能优化技术文档,而是一份AI协作指南。
我想分享的是:当面对一个复杂的性能问题时,如何与AI进行多轮对话,逐步深入,最终找到解决方案。
我的目标:通过这个真实案例,让你学会:
测试前提:本地开发环境(比生产环境慢,但可作为性能比对依据)
测试对象:一个复杂页面的预览接口
性能问题:响应时间远超预期,严重影响使用体验
影响范围:大型应用有几百个页面,发布和预览都会非常慢
应用的基本组成:
一个应用由多个页面组成
↓
页面是顶部组件
↓
应用最终的源代码 = 多个页面构造后组装的产物
页面内的组件体系:
Page (页面)
├─ QueryFields (查询条件)
├─ ResultFields (查询结果)
├─ PageButtons (页面按钮)
└─ 可引用的组件:
├─ Form (表单)
├─ MoreForm (组合表单)
├─ Dictionary (字典)
├─ CustomComponent (自定义组件)
├─ Api (接口)
└─ ...其他组件
交叉引用网络:
页面的查询条件 → 可引用 → 页面、表单、字典、接口等
页面的按钮 → 可引用 → 页面、表单、接口等
页面的结果列 → 可引用 → 字典、表单等
表单的配置项 → 可引用 → 表单、字典、接口等
组合表单的步骤 → 可引用 → 页面、表单等
...
形成了一个复杂的网状引用结构,组件之间可以相互引用。
关键理解:
这不是"组件已准备好,直接拼接"
而是"创造组件、使用组件"的动态过程
构造流程:
页面开始构造
↓
需要某个子组件 → 现场构造它
↓
子组件又需要其他组件 → 继续构造
↓
逐步展开,串联进行
↓
最终完成整个页面的构造
问题一:大量重复构造
在构造过程中,同一个组件会被多次"创造":
构造 Page-1 时需要 Form-A → 构造 Form-A
构造 Page-2 时也需要 Form-A → 又构造一次 Form-A
构造 Page-3 时还需要 Form-A → 再构造一次 Form-A
...
同一个 Form 被构造了 N 次!
问题二:循环引用导致的崩溃风险
更可怕的情况:
Page-1 → Form-A → 按钮 → Page-1
↑ │
└────────────────────────┘
循环引用!内存溢出!服务器崩溃!
而且这种问题极难排查,因为你不知道是哪个环节的引用形成了闭环。
不好的提问方式:
"这个方法执行很慢,帮我优化一下"
这样AI只能给出泛泛而谈的建议。
好的提问方式:
"这个项目是一个低代码平台,现在有个严重的性能问题:
1. 问题描述:本地测试,一个复杂页面预览需要44秒
2. 应用规模:几百个组件(页面、表单、接口等)
3. 构造过程:
- 页面是顶部组件
- 页面内包含表单、按钮、接口等子组件
- 组件之间可以相互引用
- 构造是"创造组件、使用组件"的动态过程
我发现的问题:
- 同一个组件被重复构造多次
- 可能存在循环引用,有内存溢出风险
请仔细阅读这个方法帮我分析可能的问题,并给出优化方向。"
现在我已经找到了性能问题的根源。让我给你一个详细的分析报告:
---
性能问题分析报告
调用链追踪
PageController.js(/js/{pageCode}.jsx) [第559-568行]
└── amisJsonService.genPageJson(pageCode) [第441行]
└── amisJsonService.genPageJsonById(pageId) [第446行]
├── [JSON类型] 直接返回,快速路径
├── [STM类型] genSpeedJson(),快速路径
└── [其他类型] ↓↓↓
├── pageConfigService.getCurdJson() [第1141行]
│ └── amisJsonService.packRefPage() [第390行]
│ └── 遍历 pageRefs
│ ├── [Page] getCurdJson() ← 递归!
│ ├── [Form] formService.getFormJson()
│ ├── [WizardTab] genWizardFormJson()
│ └── [TabsForm] genTabsFormJson()
│
└── loadPageTabsExt() [第1238行]
└── loadPageTabsToolbar()
└── genPageJsonById() [第1334行] ← 递归!
---
核心问题
1. 重复构造(无缓存机制)
位置: AmisJsonServiceImpl.java 第1103-1239行
每个组件引用都会完整构造一次:
// packRefPage方法中,对每个引用都重新构造
for (PageRef pageRef : page.getPageRefs()) {
if (RefType.Page.equals(refType)) {
// 每次都完整构造页面JSON,没有缓存
tabBody = pageConfigService.getCurdJson(tabPage.getCode(), ...);
}
}
影响: 如果3个页面都引用同一个表单,这个表单会被构造3次。随着引用层级加深,重复构造呈指数级增长。
2. 循环引用风险(无保护机制)
位置: 多处递归调用没有检测
// 第1334行 - 没有循环引用检测
String pageJson = genPageJsonById(ref.getRefPageCode(), null);
场景: 页面A → 页面B → 页面A → 会导致StackOverflowError
3. N+1数据库查询问题
每次构造组件都会触发多次数据库查询:
- pageService.getById() - 获取页面
- pageConfigService.queryConfigs() - 获取配置
- pageButtonService.getTableColumns() - 获取按钮
- apbApiService.getApbApi() - 获取API
几百个组件 = 数千次数据库查询
---
优化建议
┌──────────────┬────────┬─────────────────────────────────────────────┐
│ 优化项 │ 优先级 │ 说明 │
├──────────────┼────────┼─────────────────────────────────────────────┤
│ 添加结果缓存 │ 高 │ 使用Map<String, Object>缓存已构造的组件JSON │
├──────────────┼────────┼─────────────────────────────────────────────┤
│ 循环引用检测 │ 高 │ 使用Set<String>记录正在构造的组件ID │
├──────────────┼────────┼─────────────────────────────────────────────┤
│ 批量查询优化 │ 中 │ 合并数据库查询,减少往返次数 │
├──────────────┼────────┼─────────────────────────────────────────────┤
│ 延迟加载 │ 低 │ 非首屏组件可延迟构造 │
└──────────────┴────────┴─────────────────────────────────────────────┘
---
AI的分析方向是对的,但比较泛。我需要引导AI聚焦到核心问题:重复构造。
"你的分析很有道理,但我认为核心问题是重复构造。
我的观察:
构造 Page-1 时需要 Form-A → 构造 Form-A
构造 Page-2 时也需要 Form-A → 又构造一次 Form-A
同一个组件被构造了 N 次!
我的想法是使用 ThreadLocal 做一个线程级的缓存,
在同一线程内复用已经构造好的组件。
你觉得这个思路可行吗?"
你的思路完全正确,这就是问题的核心!让我分析一下:
---
你的方案分析
可行性:非常高
原因:
1. Spring MVC 一次 HTTP 请求 = 一个线程
2. 页面预览的所有组件构造都在同一线程内完成
3. ThreadLocal 天然线程安全,无需额外同步
---
但需要考虑的细节问题
1. 缓存 Key 的设计
// 太简单 - 可能覆盖不同权限的结果
String cacheKey = pageId;
// 考虑权限上下文
String cacheKey = pageId + "_" +
(authBean != null ? authBean.getFnCode() : "default") + "_" +
Thread.currentThread().getId();
2. 内存泄漏风险
// 忘记清理 - Tomcat 线程池复用会导致内存泄漏
ThreadLocal<Map<String, String>> cache = new ThreadLocal<>();
// 使用 finally 确保清理
try {
// ... 构造逻辑 ...
} finally {
cache.remove(); // 清理 ThreadLocal
}
3. 是否是最佳方案?
┌───────────────────┬────────────────────┬────────────────────┬────────────────┐
│ 方案 │ 优点 │ 缺点 │ 适用场景 │
├───────────────────┼────────────────────┼────────────────────┼────────────────┤
│ ThreadLocal │ 简单、线程安全 │ 仅单次请求有效 │ 当前问题 │
├───────────────────┼────────────────────┼────────────────────┼────────────────┤
│ Redis 缓存 │ 跨请求共享、分布式 │ 网络延迟、复杂度高 │ 高并发场景 │
├───────────────────┼────────────────────┼────────────────────┼────────────────┤
│ Caffeine 本地缓存 │ 高性能、自动过期 │ 需处理失效策略 │ 组件变化频繁时 │
└───────────────────┴────────────────────┴────────────────────┴────────────────┘
---
我的建议
ThreadLocal 是当前最佳方案,原因:
1. 实现简单,改动最小
2. 组件 JSON 通常与权限相关,跨请求缓存意义不大
3. 一次请求内的重复构造正是问题所在
---
推荐实现
让我看看现有代码结构,给你一个完整的实现方案:
⏺ Searched for 1 pattern (ctrl+o to expand)
⏺ ---
完整实现方案
在 AmisJsonServiceImpl 中添加:
@Service("amisJsonService")
@Slf4j
public class AmisJsonServiceImpl implements AmisJsonService {
// 现有代码...
// ========== 新增:线程级缓存 ==========
private static final ThreadLocal<Set<String>> CONSTRUCTING = ThreadLocal.withInitial(HashSet::new);
private static final ThreadLocal<Map<String, String>> JSON_CACHE = ThreadLocal.withInitial(HashMap::new);
@Override
public String genPageJsonById(String pageId, ApbAuthBean authBean) {
// 1. 构建缓存 Key
String cacheKey = buildCacheKey(pageId, authBean);
// 2. 检查缓存
String cached = JSON_CACHE.get().get(cacheKey);
if (cached != null) {
log.debug("缓存命中: pageId={}", pageId);
return cached;
}
// 3. 循环引用检测
if (CONSTRUCTING.get().contains(pageId)) {
log.warn("检测到循环引用: pageId={}", pageId);
return "{}";
}
try {
CONSTRUCTING.get().add(pageId);
// 4. 原有构造逻辑...
Page page = pageService.getById(pageId);
if (page == null) {
return "{}";
}
// ... [原有的完整构造逻辑] ...
String result = constructPageJson(page, authBean);
// 5. 存入缓存
JSON_CACHE.get().put(cacheKey, result);
return result;
} finally {
CONSTRUCTING.get().remove(pageId);
}
}
private String buildCacheKey(String pageId, ApbAuthBean authBean) {
String fnCode = (authBean != null && authBean.getFnCode() != null)
? authBean.getFnCode() : "default";
return pageId + "::" + fnCode;
}
// 请求结束时清理(可选,如果使用线程池建议添加)
@PreDestroy
public void cleanup() {
CONSTRUCTING.remove();
JSON_CACHE.remove();
}
}
---
预期效果
优化前:
Page-1 → Form-A (构造)
Page-2 → Form-A (又构造一次!)
Page-3 → Form-A (再构造一次!)
= 3 次重复构造
优化后:
Page-1 → Form-A (构造并存入缓存)
Page-2 → Form-A (从缓存获取)
Page-3 → Form-A (从缓存获取)
= 1 次构造 + 2 次缓存命中
---
AI确认了我的思路是可行的,但示例代码还不够通用。经过几轮对话讨论,我们最终设计出 UnifiedCacheContext 方案:
关键设计点:
(代码结构如下)
/**
* 统一缓存上下文 - 支持多种实体类型的线程级别缓存
*
* 核心功能:
* 1. 类型隔离缓存:Page、Form、ApbApi 等不同实体类型独立管理
* 2. 跨类型循环检测:Page->Form->Page 的循环引用检测
* 3. 重复查询优化:同一请求中相同实体只查询一次数据库
* 4. 线程安全:ThreadLocal 实现线程隔离
*
* 使用场景:
* - PageService.getById/getByCode
* - FormService.getById/getByCode
* - ApbApiService.getApbApi
* - 以及任何需要循环检测的实体加载场景
*
*/
@Slf4j
public class UnifiedCacheContext {
/**
* 实体类型枚举
*/
public enum EntityType {
/** 页面 */
PAGE("page"),
/** 表单 */
FORM("form"),
/** 组合表单 */
MORE_FORM("more_form"),
/** 自定义组件模块 */
CUSTOM_MODEL("custom_model"),
/** 接口 */
API("api"),
/** 应用 */
APP("app");
private final String code;
EntityType(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}
/**
* 加载状态枚举
*/
public enum LoadStatus {
/** 正在加载中 */
LOADING,
/** 已加载完成 */
LOADED
}
/**
* 缓存条目
*/
public static class CacheEntry {
private final LoadStatus status;
private final Object entity;
private final EntityType type;
private final String key;
private final long loadTime;
public CacheEntry(LoadStatus status, Object entity, EntityType type, String key) {
this.status = status;
this.entity = entity;
this.type = type;
this.key = key;
this.loadTime = System.currentTimeMillis();
}
public LoadStatus getStatus() { return status; }
public Object getEntity() { return entity; }
public EntityType getType() { return type; }
public String getKey() { return key; }
public long getLoadTime() { return loadTime; }
}
/**
* 加载帧(用于循环检测)
*/
private static class LoadingFrame {
private final EntityType type;
private final String key;
private final int depth;
LoadingFrame(EntityType type, String key, int depth) {
this.type = type;
this.key = key;
this.depth = depth;
}
@Override
public String toString() {
return type.getCode() + ":" + key;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LoadingFrame that = (LoadingFrame) o;
return type == that.type && Objects.equals(key, that.key);
}
@Override
public int hashCode() {
return Objects.hash(type, key);
}
}
/**
* 上下文数据(每个线程一个)
*/
private static class ContextData {
/** 各实体类型的缓存:EntityType -> (key -> CacheEntry) */
private final Map<EntityType, Map<String, CacheEntry>> caches;
/** 加载栈(用于跨类型循环检测) */
private final Stack<LoadingFrame> loadingStack;
/** 统计信息 */
private int totalHits = 0;
private int totalLoads = 0;
ContextData() {
this.caches = new ConcurrentHashMap<>();
this.loadingStack = new Stack<>();
// 初始化各类型的缓存容器
for (EntityType type : EntityType.values()) {
caches.put(type, new ConcurrentHashMap<>());
}
}
Map<String, CacheEntry> getCache(EntityType type) {
return caches.get(type);
}
Stack<LoadingFrame> getLoadingStack() {
return loadingStack;
}
void recordHit() { totalHits++; }
void recordLoad() { totalLoads++; }
int getTotalHits() { return totalHits; }
int getTotalLoads() { return totalLoads++; }
}
/**
* ThreadLocal 容器
*/
private static final ThreadLocal<ContextData> CONTEXT = ThreadLocal.withInitial(ContextData::new);
/**
* 最大加载深度限制
*/
private static final int MAX_DEPTH = 50;
// ==================== 公开API ====================
/**
* 开始加载实体(在查询数据库前调用)
*
* @param type 实体类型
* @param key 实体标识(id、code等)
* @return true 如果可以继续加载,false 如果检测到循环引用或深度超限
*/
public static boolean startLoading(EntityType type, String key) {
if (type == null || StringUtils.isBlank(key)) {
return false;
}
ContextData context = CONTEXT.get();
Map<String, CacheEntry> cache = context.getCache(type);
Stack<LoadingFrame> stack = context.getLoadingStack();
// 检查循环引用(跨类型检测)
LoadingFrame newFrame = new LoadingFrame(type, key, stack.size());
if (stack.contains(newFrame)) {
// 检测到循环引用!
log.info("[缓存-循环检测] 检测到循环引用: type={}, key={}, stack={}",
type.getCode(), key, formatStack(stack));
return false;
}
// 检查深度限制
if (stack.size() > MAX_DEPTH) {
log.error("[缓存-深度超限] 加载深度超过限制: type={}, key={}, depth={}",
type.getCode(), key, stack.size());
return false;
}
// 检查是否已有缓存
CacheEntry existing = cache.get(key);
if (existing != null && existing.getStatus() == LoadStatus.LOADING) {
log.warn("[缓存-重复加载] 检测到重复加载: type={}, key={}", type.getCode(), key);
return false;
}
// 标记为加载中
stack.push(newFrame);
cache.put(key, new CacheEntry(LoadStatus.LOADING, null, type, key));
log.info("[缓存-开始加载] type={}, key={}, depth={}",
type.getCode(), key, stack.size());
return true;
}
/**
* 完成加载(在查询数据库并组装完成后调用)
*
* @param type 实体类型
* @param key 实体标识
* @param entity 加载完成的实体对象
*/
public static void finishLoading(EntityType type, String key, Object entity) {
if (type == null || StringUtils.isBlank(key)) {
return;
}
ContextData context = CONTEXT.get();
Map<String, CacheEntry> cache = context.getCache(type);
Stack<LoadingFrame> stack = context.getLoadingStack();
// 更新缓存为已加载
if (entity != null) {
cache.put(key, new CacheEntry(LoadStatus.LOADED, entity, type, key));
context.recordLoad();
log.info("[缓存-加载完成] type={}, key={}, entityClass={}, depth={}",
type.getCode(), key, entity.getClass().getSimpleName(), stack.size());
} else {
// 加载失败,移除 LOADING 标记
cache.remove(key);
}
// 弹出加载栈
if (!stack.isEmpty()) {
LoadingFrame top = stack.peek();
if (top.type == type && top.key.equals(key)) {
stack.pop();
} else {
// 栈顶不匹配,说明调用顺序有问题,尝试找到并移除
log.warn("[缓存-栈不匹配] 期望[type={}, key={}], 实际[type={}, key={}]",
type.getCode(), key, top.type.getCode(), top.key);
// 使用迭代器安全删除
stack.removeIf(frame -> frame.type == type && frame.key.equals(key));
}
}
}
/**
* 获取已缓存的实体
*
* @param type 实体类型
* @param key 实体标识
* @return 缓存的实体,如果不存在或正在加载中返回 null
*/
@SuppressWarnings("unchecked")
public static <T> T get(EntityType type, String key) {
if (type == null || StringUtils.isBlank(key)) {
return null;
}
ContextData context = CONTEXT.get();
Map<String, CacheEntry> cache = context.getCache(type);
CacheEntry entry = cache.get(key);
if (entry != null && entry.getStatus() == LoadStatus.LOADED) {
context.recordHit();
log.info("[缓存-命中] type={}, key={}, entityClass={}",
type.getCode(), key, entry.getEntity().getClass().getSimpleName());
return (T) entry.getEntity();
}
return null;
}
/**
* 获取已缓存的实体(带类型转换)
*
* @param type 实体类型
* @param key 实体标识
* @param entityClass 实体类
* @return 缓存的实体
*/
public static <T> T get(EntityType type, String key, Class<T> entityClass) {
T entity = get(type, key);
if (entity != null && entityClass != null && !entityClass.isInstance(entity)) {
log.warn("缓存类型不匹配: 期望={}, 实际={}",
entityClass.getName(), entity.getClass().getName());
return null;
}
return entity;
}
/**
* 检查实体是否正在加载中
*
* @param type 实体类型
* @param key 实体标识
* @return true 如果正在加载中
*/
public static boolean isLoading(EntityType type, String key) {
if (type == null || StringUtils.isBlank(key)) {
return false;
}
ContextData context = CONTEXT.get();
Map<String, CacheEntry> cache = context.getCache(type);
CacheEntry entry = cache.get(key);
return entry != null && entry.getStatus() == LoadStatus.LOADING;
}
/**
* 检查实体是否已缓存
*
* @param type 实体类型
* @param key 实体标识
* @return true 如果已缓存
*/
public static boolean isLoaded(EntityType type, String key) {
if (type == null || StringUtils.isBlank(key)) {
return false;
}
ContextData context = CONTEXT.get();
Map<String, CacheEntry> cache = context.getCache(type);
CacheEntry entry = cache.get(key);
return entry != null && entry.getStatus() == LoadStatus.LOADED;
}
/**
* 移除指定实体的缓存
*
* @param type 实体类型
* @param key 实体标识
*/
public static void remove(EntityType type, String key) {
if (type == null || StringUtils.isBlank(key)) {
return;
}
CONTEXT.get().getCache(type).remove(key);
}
/**
* 清空当前线程的所有缓存
* 建议在请求结束时调用
*/
public static void clear() {
ContextData context = CONTEXT.get();
int totalEntries = 0;
for (Map<String, CacheEntry> cache : context.caches.values()) {
totalEntries += cache.size();
cache.clear();
}
int stackSize = context.loadingStack.size();
context.loadingStack.clear();
int hits = context.totalHits;
int loads = context.totalLoads;
CONTEXT.remove();
if (totalEntries > 0 || stackSize > 0 || hits > 0) {
log.info("[缓存-清理] entries={}, stackSize={}, hits={}, loads={}",
totalEntries, stackSize, hits, loads);
}
}
/**
* 获取当前缓存大小(用于监控)
*
* @return 各类型缓存数量
*/
public static Map<EntityType, Integer> getCacheSizes() {
Map<EntityType, Integer> sizes = new EnumMap<>(EntityType.class);
ContextData context = CONTEXT.get();
for (EntityType type : EntityType.values()) {
sizes.put(type, context.getCache(type).size());
}
return sizes;
}
/**
* 获取当前加载深度(用于监控)
*
* @return 当前加载栈的深度
*/
public static int getCurrentDepth() {
return CONTEXT.get().getLoadingStack().size();
}
/**
* 获取缓存统计信息(用于监控)
*
* @return 统计信息字符串
*/
public static String getStatistics() {
ContextData context = CONTEXT.get();
return String.format("hits=%d, loads=%d, depth=%d, sizes=%s",
context.getTotalHits(),
context.getTotalLoads(),
context.getLoadingStack().size(),
getCacheSizes());
}
/**
* 格式化加载栈(用于调试)
*/
private static String formatStack(Stack<LoadingFrame> stack) {
if (stack.isEmpty()) {
return "[]";
}
List<String> frames = new ArrayList<>();
for (LoadingFrame frame : stack) {
frames.add(frame.toString());
}
return String.join(" -> ", frames);
}
}
实现完成后,我发现了一个问题:
" 在代码类AppSchemaServieImpl中的方法createAppModel,我发现代码中使用了 parallelStream() 并行流:
appPages.parallelStream()
.map(page -> initAppPageSchema(page))
.collect(Collectors.toList());
你仔细分析该方法,然后告诉这种多线程场景下,ThreadLocal 是否失效了?"
发现潜在问题:实现后不要盲目信任,要验证和测试。
向AI确认:不是自己闷头想,而是让AI确认我的担忧。
这是个非常关键的发现!让我先看看这段代码:
⏺ Searched for 1 pattern, read 1 file (ctrl+o to expand)
⏺ 太好了,你发现了一个致命问题!
---
问题分析
看第86-95行和第103-110行:
appSchemaList = appPages.parallelStream()
.filter(item->"Tab".equals(item.getPageStyle()))
.map(page -> {
ExecutionContext.setContextMap(newContextMap);
// ...
return ApbAuthKit.loadAuthForTab(
initAppPageSchema(page,...), authBean); // 内部调用 genPageJsonById
}).collect(Collectors.toList());
parallelStream() 的工作机制:
ForkJoinPool.commonPool()
├── Thread-1 处理 page-1
├── Thread-2 处理 page-2
└── Thread-3 处理 page-3
ThreadLocal 的问题:
Thread-1 的缓存: {page-1: "json1"}
Thread-2 的缓存: {page-2: "json2"} ← 无法访问 Thread-1 的缓存
Thread-3 的缓存: {page-3: "json3"} ← 无法访问 Thread-1/2 的缓存
如果 page-1、page-2、page-3 都引用同一个 Form-A:
- Thread-1 构造 Form-A
- Thread-2 又构造一次 Form-A ← 重复!
- Thread-3 再构造一次 Form-A ← 又重复!
结论:ThreadLocal 缓存在并行流场景下完全失效!
AI列出了几个备选方案:
"关于全局缓存方案,我有这样的担忧:
时刻 T1: 用户A 预览 Project-X
→ 存入全局缓存: "page:1" = Project-X 的 Page-1
时刻 T2: 用户B 预览 Project-Y
→ 存入全局缓存: "page:1" = Project-Y 的 Page-1 ← 覆盖了!
时刻 T3: 用户A 继续处理
→ 从全局缓存获取 "page:1"
→ 拿到的是 Project-Y 的 Page-1 ← 数据错误!
所以全局缓存会导致用户数据串,且处理缓存一致性比较麻烦,不可行。
我的想法是:请求级的多线程缓存。
同一个请求内的所有线程共享缓存,但不同请求的缓存隔离。
这个方案可行吗?"
"非常好!你的分析完全正确。
请求级缓存是一个很好的方案:
1. 同一请求的所有线程可以共享缓存
2. 不同请求的缓存完全隔离
3. 请求结束时自动清理,避免内存泄漏
"
方案确认后,让AI协助完成了 RequestScopeCache 的完整实现。核心要点:
架构设计:
ThreadLocal<String> REQUEST_ID 记录当前线程的请求IDConcurrentHashMap<String, RequestCache> REQUEST_CACHES 存储所有请求的缓存setRequestId(requestId) 恢复请求上下文来共享缓存为什么不用 InheritableThreadLocal?
parallelStream() 的线程来自 ForkJoinPool,不是通过继承创建的requestId 更灵活,可以在任意子线程中恢复上下文InheritableThreadLocal 在线程池复用场景下的内存泄漏风险生命周期管理:
initRequest() 创建请求上下文cleanup() 清理请求上下文(代码量较大,这里就不展开展示了)
"我发现代码中有很多硬编码的缓存key:
put("pageCacheKey:" + id, page);
put("pageObj:" + id, page);
put("apiCacheKey:" + code, api);
这样容易拼写错误。能不能在 RequestScopeCache 类中
增加缓存key前缀枚举类型,统一管理?"
public enum CacheKeyPrefix {
PAGE_ENTITY("pageObj"),
API_ENTITY("apiObj"),
APP_ENTITY("appObj"),
FORM_ENTITY("formObj");
private final String prefix;
public String buildKey(Object... identifiers) {
StringBuilder sb = new StringBuilder(prefix);
for (Object id : identifiers) {
sb.append(":").append(id);
}
return sb.toString();
}
}
使用方式:
String cacheKey = CacheKeyPrefix.PAGE_ENTITY.buildKey(pageId);
细节很重要:好的代码是持续打磨出来的,不是一次性完成的。
"我发现所有方法都使用同一套带循环检测的缓存逻辑。
但实际上:
- Page、Form:有相互引用,需要循环检测
- Api、App:独立实体,不会被引用,不需要循环检测
是否可以提供两套API?"
简单缓存API(用于独立实体):
public static <T> T getSimple(EntityType type, String key, Class<T> clazz);
public static void putSimple(EntityType type, String key, Object entity);
完整缓存API(带循环检测):
public static boolean startLoading(EntityType type, String key);
public static void finishLoading(EntityType type, String key, Object result);
持续优化:没有最好,只有更好。实现基础功能后,继续寻找优化点。
性能提升:
效果示例(本地开发环境,复杂应用):
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 单个页面预览 | 基准值 | -50% | 2倍 |
| 缓存命中率 | 0% | 60%~80% | - |
| 重复构造次数 | 数百次 | 几乎为0 | 消除 |
| 循环引用风险 | 存在 | 已消除 | 安全 |
[RequestScopeCache] 清理: requestId=xxx, hits=450, loads=124, cacheSize=124, duration=3500ms
指标说明:
不要:
"帮我优化代码"
"这个怎么改"
要:
"我遇到了XX问题,具体情况是...
我的观察是...
我的想法是...
你觉得可行吗?"
第一轮:描述问题,获取初步分析
↓
第二轮:聚焦核心,讨论具体方案
↓
第三轮:实现方案,发现新问题
↓
第四轮:对比方案,选择最优解
↓
第五轮:完善细节,持续优化
| 原则 | 说明 | 示例 |
|---|---|---|
| 提供上下文 | 让AI理解你的场景 | "这是一个低代码平台,有几百个组件..." |
| 具体观察 | 分享你的发现 | "我发现同一个组件被构造了多次" |
| 主动思考 | 不是全盘依赖AI | "我的想法是使用ThreadLocal..." |
| 构造场景 | 帮助AI理解问题 | "T1时刻用户A,T2时刻用户B..." |
| 逐步深入 | 不是一次性解决所有问题 | 从基础功能到细节优化 |
AI不是万能的,需要判断:
AI擅长的:
️ 需要你自己判断的:
"你提到了全局缓存,能详细说说为什么这个方案可行吗?
在我的场景下会有什么问题?"
"针对这个问题,你能给出2-3个方案吗?
分别说明优缺点。"
"如果使用了全局缓存,当两个用户同时操作时会发生什么?
请帮我推演一下。"
"能不能给我一个完整的代码示例?
包括使用方式。"
"我们如何从架构层面避免这类性能问题?
在设计阶段应该考虑什么?"
"如何监控缓存的效果?
需要关注哪些指标?"
"如果未来组件数量翻倍,这个方案还能适用吗?
需要做哪些调整?"
不要把AI当成"搜索引擎"或"代码生成器"。
把AI当成你的"技术伙伴":
- 向它描述你的问题
- 与它讨论你的想法
- 让它帮助你验证
- 和它一起优化
这才是AI协作的正确方式。
本次分享基于真实项目经验整理 优化效果:性能提升50%,缓存命中率80%,彻底消除循环引用风险