高并发系统基石:从手写限流算法到秒杀系统设计

4 阅读4分钟

你有没有遇到过这种崩溃时刻:

  • 写毕业论文时,导师说:“你得先做完实验才能写数据分析,但实验数据又得等开题报告通过。”
  • 运行 npm run build 时,Webpack 报错:“Module A depends on Module B, but Module B hasn't been compiled yet.”

这些看似无关的烦恼,本质上都是同一个算法问题:拓扑排序 (Topological Sort)

1. 什么是拓扑排序?

简单来说,就是把一个有向无环图 (DAG)  中的所有节点排成一个线性序列,使得对于图中的每一条有向边 u -> v,节点 u 在序列中都出现在节点 v 的前面。

人话翻译:  如果任务 A 必须在任务 B 之前完成,那么在最终的执行清单里,A 必须排在 B 前面。

2. 核心逻辑:寻找“零入度”节点

想象一下你的待办事项清单:

  1. 入度 (In-degree) :有多少个前置任务指向它。
  2. 出度 (Out-degree) :它完成后能解锁多少个后续任务。

算法步骤(Kahn 算法):

  1. 找出所有入度为 0 的节点(没有前置依赖的任务),放入队列。
  2. 从队列取出一个节点,将其加入结果序列。
  3. 删除该节点及其发出的所有边(即把它的后继节点的入度减 1)。
  4. 如果某个后继节点的入度变成了 0,说明它的前置任务都搞定了,把它也加入队列。
  5. 重复直到队列为空。

3. 代码实现:模拟任务调度系统

class TopologicalSorter {
    constructor(numTasks) {
        this.numTasks = numTasks;
        this.graph = new Array(numTasks).fill(0).map(() => []);
        this.inDegree = new Array(numTasks).fill(0);
    }

    // 添加依赖关系:taskA 必须在 taskB 之前完成 (A -> B)
    addDependency(taskA, taskB) {
        this.graph[taskA].push(taskB);
        this.inDegree[taskB]++;
    }

    sort() {
        const queue = [];
        const result = [];

        // 1. 找到所有入度为 0 的起点
        for (let i = 0; i < this.numTasks; i++) {
            if (this.inDegree[i] === 0) {
                queue.push(i);
            }
        }

        while (queue.length > 0) {
            const current = queue.shift();
            result.push(current);

            // 2. 处理当前任务的后继
            for (const neighbor of this.graph[current]) {
                this.inDegree[neighbor]--;
                // 3. 如果后继任务的依赖都满足了,加入队列
                if (this.inDegree[neighbor] === 0) {
                    queue.push(neighbor);
                }
            }
        }

        // 如果结果数量不等于总任务数,说明存在环(死锁)
        if (result.length !== this.numTasks) {
            throw new Error("检测到循环依赖!无法完成所有任务。");
        }

        return result;
    }
}

// 场景:毕业论文流程
// 0: 选题, 1: 开题报告, 2: 实验, 3: 数据分析, 4: 论文撰写, 5: 答辩
const sorter = new TopologicalSorter(6);
sorter.addDependency(0, 1); // 选题 -> 开题
sorter.addDependency(1, 2); // 开题 -> 实验
sorter.addDependency(2, 3); // 实验 -> 数据分析
sorter.addDependency(3, 4); // 数据分析 -> 撰写
sorter.addDependency(4, 5); // 撰写 -> 答辩

console.log("推荐执行顺序:", sorter.sort()); 
// 输出: [0, 1, 2, 3, 4, 5]

4. 工业界应用:无处不在的调度

  1. 前端工程化 (Webpack/Vite)

    • 模块之间存在 import/export 依赖。构建工具必须通过拓扑排序确定编译顺序,否则会出现“变量未定义”的错误。
  2. 数据库迁移 (Migration)

    • 表 B 的外键引用了表 A,那么创建表 A 的脚本必须先于表 B 执行。
  3. 课程安排系统

    • 必须先修完《高等数学》才能修《机器学习》,教务系统用它来检测选课冲突。

5. 进阶:如何处理“循环依赖”?

如果在图中出现了 A -> B -> C -> A,拓扑排序将无法进行(队列会提前变空,但还有节点没处理)。

解决方案:

  • 静态分析:在代码编译阶段直接报错,提示开发者解耦。
  • 运行时降级:在某些微服务架构中,如果检测到循环调用,可能会采用异步消息队列来打破同步依赖链。

6. 总结与面试考点

特性说明
适用场景任务调度、依赖解析、编译顺序
前提条件图必须是有向无环图 (DAG)
时间复杂度O(V + E),V 是节点数,E 是边数

面试官常问:

  1. “如何判断一个图中是否存在环?” (答:尝试进行拓扑排序,如果最后输出的节点数少于总节点数,则存在环。)
  2. “DFS 也能做拓扑排序吗?” (答:可以。利用 DFS 的回溯过程,将节点压入栈中,最后出栈的顺序即为拓扑序。)

下期预告:  任务调度完了,我们来看看如何在海量数据中快速找到“第 K 大”的元素。堆 (Heap)  和 快速选择 (Quick Select)  谁是性能之王?

如果你觉得这篇关于“秩序”的文章对你有帮助,欢迎点赞收藏!🚀


📢 欢迎关注我的公众号:《Lee 的成长日记》 

💡 福利:关注公众号回复“算法”,获取本教程的 PDF 完整版及 LeetCode 刷题清单。