🤖 用AI解决复杂性能问题:一场真实的人机协作实录

0 阅读20分钟

教你如何与Claude高效对话,通过真实案例学习人机协作


💡 写在前面

这篇文章不是一份性能优化技术文档,而是一份AI协作指南

我想分享的是:当面对一个复杂的性能问题时,如何与AI进行多轮对话,逐步深入,最终找到解决方案。

我的目标:通过这个真实案例,让你学会:

  1. 如何向AI准确描述问题
  2. 如何引导AI给出可行的方案
  3. 如何在AI的基础上深入思考
  4. 如何判断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-1Form-A → 按钮 → Page-1
     ↑                        │
     └────────────────────────┘
循环引用!内存溢出!服务器崩溃!

而且这种问题极难排查,因为你不知道是哪个环节的引用形成了闭环。


🗣️ 第一轮对话:问题提出

我的Prompt策略

❌ 不好的提问方式

"这个方法执行很慢,帮我优化一下"

这样AI只能给出泛泛而谈的建议。

✅ 好的提问方式

"这个项目是一个低代码平台,现在有个严重的性能问题:
​
1. 问题描述:本地测试,一个复杂页面预览需要44秒
2. 应用规模:几百个组件(页面、表单、接口等)
3. 构造过程:
   - 页面是顶部组件
   - 页面内包含表单、按钮、接口等子组件
   - 组件之间可以相互引用
   - 构造是"创造组件、使用组件"的动态过程
​
我发现的问题:
- 同一个组件被重复构造多次
- 可能存在循环引用,有内存溢出风险
​
请仔细阅读这个方法帮我分析可能的问题,并给出优化方向。"

AI的初步分析

现在我已经找到了性能问题的根源。让我给你一个详细的分析报告:
                                                                                                                                                                  
  ---             
  性能问题分析报告                                                                                                                                                
   
  调用链追踪                                                                                                                                                      
                  
  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. 提供具体观察:让AI理解问题的本质
  2. 提出自己的想法:不是让AI从零开始,而是讨论具体方案
  3. 征求AI的意见:让AI评估我的思路

AI的回应

 你的思路完全正确,这就是问题的核心!让我分析一下:                                                                                                              
​
  ---                                                                                                                                                             
  你的方案分析
                                                                                                                                                                  
  ✅ 可行性:非常高
                                                                                                                                                                  
  原因:                                                                                                                                                          
  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-1Form-A (构造)
  Page-2Form-A (又构造一次!)
  Page-3Form-A (再构造一次!)
  = 3 次重复构造
​
  优化后:
  Page-1Form-A (构造并存入缓存)
  Page-2Form-A (从缓存获取) ✅
  Page-3Form-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确认我的担忧。

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列出了几个备选方案:

  1. 全局缓存 - 跨请求共享,但有数据混乱风险
  2. InheritableThreadLocal - 子线程继承父线程缓存,但复杂度高
  3. 放弃并行 - 改用串行,简单但失去并行优势

🤔 第四轮对话:方案对比与选择

我的思考与提问

"关于全局缓存方案,我有这样的担忧:
​
时刻 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的基础上继续完善

AI的确认

"非常好!你的分析完全正确。
​
请求级缓存是一个很好的方案:
1. 同一请求的所有线程可以共享缓存
2. 不同请求的缓存完全隔离
3. 请求结束时自动清理,避免内存泄漏
"

方案确认后,让AI协助完成了 RequestScopeCache 的完整实现。核心要点:

架构设计

  • 使用 ThreadLocal<String> REQUEST_ID 记录当前线程的请求ID
  • 使用 ConcurrentHashMap<String, RequestCache> REQUEST_CACHES 存储所有请求的缓存
  • 子线程通过 setRequestId(requestId) 恢复请求上下文来共享缓存

为什么不用 InheritableThreadLocal?

  • parallelStream() 的线程来自 ForkJoinPool,不是通过继承创建的
  • 手动传递 requestId 更灵活,可以在任意子线程中恢复上下文
  • 避免了 InheritableThreadLocal 在线程池复用场景下的内存泄漏风险

生命周期管理

  • 在拦截器中 initRequest() 创建请求上下文
  • 在拦截器中 cleanup() 清理请求上下文
  • 提供统计日志,监控缓存效果

(代码量较大,这里就不展开展示了)


🛠️ 第五轮对话:细节优化

缓存Key的问题

"我发现代码中有很多硬编码的缓存key:
​
put("pageCacheKey:" + id, page);
put("pageObj:" + id, page);
put("apiCacheKey:" + code, api);
​
这样容易拼写错误。能不能在 RequestScopeCache 类中
增加缓存key前缀枚举类型,统一管理?"

AI的实现(部分代码)

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?"

AI的优化

简单缓存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%
  • 缓存命中率达到 60%~80%
  • 彻底消除了循环引用导致的崩溃风险

效果示例(本地开发环境,复杂应用):

指标优化前优化后提升
单个页面预览基准值-50%2倍 ⚡
缓存命中率0%60%~80%-
重复构造次数数百次几乎为0消除 ✅
循环引用风险存在已消除安全 ✅

缓存统计日志示例

[RequestScopeCache] 清理: requestId=xxx, hits=450, loads=124, cacheSize=124, duration=3500ms

指标说明

  • hits: 450(缓存命中次数)
  • loads: 124(从数据库加载并缓存的数量)
  • cacheSize: 124(缓存中的条目数)
  • 命中率 = 450 / (450 + 124) × 100% = 78.4%

效果总结

  • 这只是预览一个页面的效果
  • 页面越复杂,组件越多,性能提升越明显
  • 对于有几百个组件的应用,提升会更加显著
  • 同时解决了循环引用导致的内存溢出风险

🎓 总结:如何与AI高效协作

1. 提问的艺术

❌ 不要

"帮我优化代码"
"这个怎么改"

✅ 要

"我遇到了XX问题,具体情况是...
我的观察是...
我的想法是...
你觉得可行吗?"

2. 对话的节奏

第一轮:描述问题,获取初步分析
  ↓
第二轮:聚焦核心,讨论具体方案
  ↓
第三轮:实现方案,发现新问题
  ↓
第四轮:对比方案,选择最优解
  ↓
第五轮:完善细节,持续优化

3. 关键原则

原则说明示例
提供上下文让AI理解你的场景"这是一个低代码平台,有几百个组件..."
具体观察分享你的发现"我发现同一个组件被构造了多次"
主动思考不是全盘依赖AI"我的想法是使用ThreadLocal..."
构造场景帮助AI理解问题"T1时刻用户A,T2时刻用户B..."
逐步深入不是一次性解决所有问题从基础功能到细节优化

4. 判断AI建议的方法

AI不是万能的,需要判断:

✅ AI擅长的

  • 代码实现
  • 模式识别
  • 多方案对比
  • 细节完善

⚠️ 需要你自己判断的

  • 业务场景的理解
  • 方案的可行性
  • 实际的应用环境

💡 实战技巧

技巧1:让AI解释它的建议

"你提到了全局缓存,能详细说说为什么这个方案可行吗?
在我的场景下会有什么问题?"

技巧2:让AI给出多个方案

"针对这个问题,你能给出2-3个方案吗?
分别说明优缺点。"

技巧3:构造反例验证

"如果使用了全局缓存,当两个用户同时操作时会发生什么?
请帮我推演一下。"

技巧4:要求代码示例

"能不能给我一个完整的代码示例?
包括使用方式。"

🚀 进阶:从解决问题到预防问题

与AI讨论架构设计

"我们如何从架构层面避免这类性能问题?
在设计阶段应该考虑什么?"

与AI讨论监控方案

"如何监控缓存的效果?
需要关注哪些指标?"

与AI讨论扩展性

"如果未来组件数量翻倍,这个方案还能适用吗?
需要做哪些调整?"

📚 结语

核心要点

  1. AI是协作者:不是替代你思考,而是帮助你思考
  2. 问题要具体:好的提问是成功的一半
  3. 对话要深入:多轮对话比单次提问更有效
  4. 验证要落实:AI的建议需要验证和测试
  5. 优化要持续:没有最好,只有更好

最重要的建议

不要把AI当成"搜索引擎"或"代码生成器"。
​
把AI当成你的"技术伙伴":
- 向它描述你的问题
- 与它讨论你的想法
- 让它帮助你验证
- 和它一起优化
​
这才是AI协作的正确方式。

本次分享基于真实项目经验整理 优化效果:性能提升50%,缓存命中率80%,彻底消除循环引用风险