【结论】拆壳顺序、深度上限、绝对数告警这几条我都认,没有异议。我再补三个容易出错的细节:拆壳循环不能只靠深度计数;outbox 抢占除了加索引,还要控制隔离级别和事务边界;退避策略不要硬编码在扫描器里。
【展开】
拆壳循环:深度上限 10 只能拦住“无限向下”,拦不住“同一层打转”。建议加一个 IdentityHashMap<String,Boolean>(或 Set<Throwable>,注意用 == 而非 equals),记录已经拆过的对象,一旦命中就直接判定为环并打点上报。包前缀先 unwrap 再匹配这个顺序,我在实际项目里也遇到过;顺着这条思路再加一个指标:每次拆壳记录 shellDepth,正常业务调用应该稳定在 0-2 层;如果某条链路长期是 3 层以上,多半是代理配置或异常转换写错了,这比断裂率更早发出信号。Error 单开基线我同意;另外拆到 Error 时,建议直接跳过后续统计,不要在 handler 里对 OutOfMemoryError 做任何字符串拼接(构造 message 本身又可能继续分配内存)。
告警规范:分子绝对数触发告警、比率只观察趋势,这个分法是对的。但比率的分母我建议画两条:断裂数 / 5xx 总数 和 断裂数 / 业务请求总量。前者看“5xx 质量”,后者才能发现“5xx 总量本身在涨,但都被拆壳掩盖了”这种情况。你提到的业务量翻倍掩盖问题,第二条曲线能兜住。
outbox:(status, id) 联合索引确实是前提;另外还有两点:① 抢占式 UPDATE ... ORDER BY id LIMIT 在 REPEATABLE READ 下的间隙锁范围比 READ COMMITTED 大得多。建议把扫描事务显式设为 READ COMMITTED,并且“认领后立即提交、业务处理放到事务外”,否则一批 100 行会锁住整段索引;② 除了 updated_at,还要有 attempts 和 next_run_at 两个字段,扫描时带上 next_run_at <= NOW(),索引利用和退避就能同时解决。批量方式不要硬编码 100,做成可配置项,退避间隔按 source_type 从一张策略表读取(或者直接用 task_type 加一个配置 map),这样你提到的来源标记才真正起作用,而不只是多一个字段。
【结尾】还有一个容易漏的点:回收扫描和认领扫描如果共用同一张表、同一个频率,回收逻辑在高峰期会和正常认领争抢同一批索引。可以给回收单独配一个低频率 Cron,别和主扫描绑在同一个任务里。
正式会员
认证极客






