某大厂的电商系统被拆成 1000 个微服务:订单服务调用库存服务,库存服务调用风控服务,风控服务又要回头看订单服务的数据……
某天凌晨,线上的构建系统突然报错:「检测到循环依赖,无法确定启动顺序!」工程师们对着 30000 条调用关系发呆。
更糟的是,运维同学还发现另一个问题:只要风控服务一挂,整个交易链路就全线瘫痪;而日志服务挂掉却几乎没人受影响。
为什么有的服务是「命门」,有的服务却「可有可无」?
能不能把「互相依赖的一坨服务」直接打包成一个整体,再画出它到底卡在哪儿?
答案是肯定的。把服务看成点、调用关系看成有向边,这个系统就是一张有向图:
• 互相可达的一组服务,就是图中的强连通分量(SCC)——它们要么一起启动成功,要么一起失败,可以缩成一个点;
• 缩点之后,图变成一张有向无环图(DAG),启动顺序(拓扑序)立刻就有了;
• 而像「风控服务」这种删掉就让图断开的点,就是割点;删掉就让连通性下降的边,就是割边。
Tarjan 算法:一次 O(n+m) 的 DFS,同时解决上面全部三个问题。
💡 故事背后的算法思想
第一层直觉:时间戳。DFS 会给每个点打上「第几个被访问到」的时间戳 dfn。同一次 DFS 中,dfn 越大代表越「晚被看见」。
第二层直觉:回路。如果从 u 出发能走回一个比自己更早访问的点(dfn 更小),说明绕了一圈又回来了,这条回路上的点在同一个 SCC 里。
第三层直觉:low 值。low[u] 记录「从 u 的子树出发,最多能绕回到多大的祖先」。low[u] 越小,说明 u 所在的位置越「往上勾得住」。当 low[u] == dfn[u] 时,u 就是这一坨 SCC 的「最高点」,可以出栈结算了。
结论:同一份 dfn/low 数据,有向图上判 SCC(看回边是否还在栈里),无向图上判割点(看 low[v] ≥ dfn[u])、判割边(看 low[v] > dfn[u]),核心代码只差一两行。