当设计或修改 REST API 响应结构、处理 API 返回值,或生成 Excel/CSV/PDF/对账文件等下游产物时触发。防止 API 设计缺陷导致的字段错位、类型歧义,以及生成产物时关键字段缺失但静默成功的问题。
The English summary is being prepared. Showing the original description.
当 API/任务可能执行超过 10 秒(批量数据处理、远程 API 批量调用、全表扫描、跨租户聚合)时触发。防止同步接口被网关 30s 超时切断、用户重复点击触发并发、状态缓存内存泄漏等问题。提供异步任务状态机标准模板。
The English summary is being prepared. Showing the original description.
Review the instructions the agent will receive before you install.
当一个操作可能跑超过 10 秒,必须改成 triggerAsync() + getStatus() 的异步状态机模式。
不要让用户盯着一个会超时的 loading 转圈,更不要让他点第二下。
与
query-performance-safety配合:先用后者把同步性能压到能跑完的范围;只有"再优化也压不到 10s 以内"时,才走本规范。
只要满足任一条件,就属于本 skill 的覆盖范围:
场景: 6000 条商品查重,同步接口 30s 超时;前端无回执,用户重复点击。
// ❌ HTTP 请求线程内串行跑 N 次远程调用 + JPA 写库
@PostMapping("/duplicates/check")
public ApiResponse<Map<String, List<ProductDTO>>> checkDuplicates() {
return ApiResponse.success(productService.findDuplicateProducts());
// 6000 条 × 4 次隐藏查询 = 24000 次 SQL,必然 > 30s
}
后果:
@Service
public class DuplicateChecker {
private static final long FINISHED_STATE_TTL_MINUTES = 60;
private final Executor executor;
private final ProductService productService;
// 关键:状态缓存按租户隔离,永远不要让它无界增长
private final Map<Long, CheckState> tenantStates = new ConcurrentHashMap<>();
public CheckState triggerAsync() {
evictExpiredStates();
Long tenantId = TenantContext.getTenantId();
// 同租户已 RUNNING:返回当前状态,不重复启动
CheckState existing = tenantStates.get(tenantId);
if (existing != null && existing.getStatus() == Status.RUNNING) {
return existing.snapshot();
}
CheckState state = new CheckState();
state.setStatus(Status.RUNNING);
state.setStartedAt(LocalDateTime.now());
tenantStates.put(tenantId, state);
executor.execute(() -> {
try {
TenantContext.setTenantId(tenantId); // 子线程必须恢复上下文
run(tenantId, state);
} finally {
TenantContext.clear();
}
});
return state.snapshot();
}
public CheckState getStatus() {
evictExpiredStates();
Long tenantId = TenantContext.getTenantId();
CheckState state = tenantStates.get(tenantId);
if (state == null) {
CheckState idle = new CheckState();
idle.setStatus(Status.IDLE);
return idle;
}
return state.snapshot();
}
private void evictExpiredStates() {
LocalDateTime threshold = LocalDateTime.now().minusMinutes(FINISHED_STATE_TTL_MINUTES);
tenantStates.entrySet().removeIf(entry -> {
CheckState s = entry.getValue();
return s.getStatus() != Status.RUNNING
&& s.getFinishedAt() != null
&& s.getFinishedAt().isBefore(threshold);
});
}
public enum Status { IDLE, RUNNING, SUCCESS, FAILED }
}
// Controller 立即返回
@PostMapping("/duplicates/check")
public ApiResponse<CheckState> trigger() {
return ApiResponse.success("已触发", duplicateChecker.triggerAsync());
}
@GetMapping("/duplicates/check/status")
public ApiResponse<CheckState> status() {
return ApiResponse.success(duplicateChecker.getStatus());
}
根因:ConcurrentHashMap<Long, State> 作为长生命周期缓存,每个进入过的租户都留下一条记录,FINISHED 后也不清理。
// ❌ 错误:永远不清理
private final Map<Long, RefreshState> tenantStates = new ConcurrentHashMap<>();
// 多租户 SaaS 跑 3 个月后:1000 个租户 × 平均 2KB state = 内存只增不减
// 若 state 里还带着完整结果集(duplicateGroups Map<String,List>)→ 单租户就可能 MB 级
任选其一(推荐第 1 个,最简单):
| 策略 | 实现 | 适用 |
|---|---|---|
| 入口清理 + TTL(推荐) | 每次 triggerAsync/getStatus 时遍历清理 finishedAt < now - 1h 的非 RUNNING 状态 | 状态数量 < 1000,访问频率高 |
| 定时任务清理 | @Scheduled(fixedRate = 600000) 每 10 分钟扫描 | 状态数量大,访问频率低 |
| LRU 限容 | Caffeine.newBuilder().maximumSize(500).expireAfterWrite(1, HOURS) | 状态超大(含完整结果) |
Map<X, State> 作为成员变量时,是否有 TTL/LRU 策略异步执行后 ThreadLocal 默认不传递。如果业务依赖 TenantContext,子线程拿不到 → 跨租户数据串、空指针。
// ❌ 子线程拿不到 tenantId
executor.execute(() -> run(state));
// ✅ 在子线程入口手动恢复
executor.execute(() -> {
try {
TenantContext.setTenantId(tenantId);
run(tenantId, state);
} finally {
TenantContext.clear();
}
});
同样适用:
SecurityContextHolder、MDC(日志上下文)、自定义请求级 ThreadLocal。
用户点了"刷新"按钮没反应,再点一下 → 后端起了两个并发任务,互相覆盖状态。
public State triggerAsync() {
State existing = states.get(key);
if (existing != null && existing.getStatus() == Status.RUNNING) {
return existing.snapshot(); // 关键:直接返回,不要重新启动
}
// ... 启动新任务
}
前端配合:按钮在 RUNNING 状态下 disabled,文案变 "回填中 N%",让任何用户进来都看见同一任务在跑。
mount 时用 setTimeout 递归 polling,状态变 RUNNING 时又起 setInterval —— 两套并存 → 每 3s 触发 2 次接口;状态变化让第二个 effect 重新创建 interval → 可能爆出 N×N 个定时器。
只能有一套 polling 机制:mount 时只做一次初始 fetch,polling 完全由依赖 status 的 effect 接管。
完整错误/正确代码示例见
frontend-devskill - 陷阱 #3「useEffect 双轮询冲突」
| 项 | 规范 |
|---|---|
| 触发按钮 | 立即变 loading={true} disabled,文案 "回填中 N%" |
| 进度可见 | 顶部加 <Alert> + <Progress>,显示 processed / total |
| 任意用户进来都能看见 | 进入页面先 getStatus(),RUNNING 时自动启动 polling |
| 切换页面再回来 | polling 依赖 status,重新 mount 自动接续 |
| RUNNING → 完成 | message.success/error + 自动 loadData() 刷新主列表 |
| FAILED 显示原因 | state.errorMessage 透传到 message |
| 关闭浏览器再开 | 后端任务不受影响(在 Executor 里跑),下次进来仍能查 |
新增异步任务时挨条核:
errorMessage 透传给前端log.info 便于排查(开始 / 进度 / 结束)| 反模式 | 为什么不行 |
|---|---|
启动新线程而非 Executor | 不受池化管理,OOM 风险 |
| 把结果塞进 HTTP Session | 多实例部署直接失效 |
| 把状态写文件 | 多实例部署、容器重启都丢 |
用 @Async 但不管返回值 | 异常被吞,用户不知道失败 |
前端轮询用 setInterval 但不 cleanup | 路由切换后还在跑,反复触发 setState 报警 |
同步接口加 @Async 注解(self-invocation) | 不走代理,注解不生效 → 见 java-dev skill 「Spring self-invocation 陷阱」 |
任务规模继续增长时的演进方向:
| 当前规模 | 推荐方案 |
|---|---|
| 单租户单进程,< 1 小时 | 本 skill 描述的 ConcurrentHashMap 状态机 |
| 多实例部署,需共享状态 | 状态存 Redis(TTL 1h)+ 分布式锁 |
| 长任务(> 1 小时)或需要重试 | 上专门的任务队列(Spring Batch / XXL-Job / RocketMQ) |
| 工作流编排 | Temporal / Activiti |
不要过早上重型方案。ConcurrentHashMap + Executor 能覆盖 80% 的"同步太慢"场景。
> 📋 本回复遵循:`async-task-pattern` - [章节]
No reviews yet. Tell others what you thought.
No login needed. Share how it worked for you, ask a question, or note where it did not run.
No comments yet. Be the first to write one.
当设计或修改 REST API 响应结构、处理 API 返回值,或生成 Excel/CSV/PDF/对账文件等下游产物时触发。防止 API 设计缺陷导致的字段错位、类型歧义,以及生成产物时关键字段缺失但静默成功的问题。
The English summary is being prepared. Showing the original description.
当设计或修改 REST API 响应结构、处理 API 返回值时触发。防止 API 设计缺陷导致的字段错位、类型歧义等问题。
The English summary is being prepared. Showing the original description.
网关/代理/WAF/CDN 中间件的安全关键词匹配实现规范,防止纯子串匹配误判正常响应内容中的技术术语(如 Cloudflare、502、error)
The English summary is being prepared. Showing the original description.
当用户操作 .sh、Dockerfile、Makefile、.yml、.yaml 文件,或在 Markdown 中编写 bash 代码块时触发。提供 Bash 编写规范。
The English summary is being prepared. Showing the original description.
Bash 脚本与系统命令规范。禁止行尾注释,强制使用 tee/heredoc 写入,提升命令执行的可维护性。
The English summary is being prepared. Showing the original description.
Bash 编写规范。当用户操作 .sh、Dockerfile、Makefile、.yml、.yaml 文件, 或在 Markdown 中编写 bash/shell 代码块时触发。 包含注释规范、文件写入方式、Heredoc 引号规则、权限路径、脚本规范等。
The English summary is being prepared. Showing the original description.