资讯动态

银行家算法详解:从死锁原理到安全状态检查与C++实现

发布时间:2026/10/21 20:12:32 来源:尧图企业网站定制
做过多线程开发的人多多少少撞上过死锁。两个线程各占了一把锁又互相等着对方手里的锁程序就这么僵着CPU风扇还在转业务却一点进度都没有。我第一次在教材里看到银行家算法这个名字时还以为是金融系统里的风控模型后来把原理啃完才发现这名字起得极其贴切银行家算法就是操作系统的贷款审批员每次资源请求进来它先模拟一遍未来能不能及时收回贷款能则批不能则拒让系统始终待在不死锁的安全区里。下面会把资源数据结构、安全性检查、请求判定三块掰开揉碎配一个完整案例推演和可以直接运行的 C 实现。读的时候建议你手边放张纸跟着演算几遍理解起来会快很多。1. 死锁现场明明有资源程序却卡死了1.1 死锁是怎么发生的死锁最典型的出场方式是两个线程互相持有对方需要的资源。比如线程 A 拿到了资源 R1正等着资源 R2线程 B 拿到了资源 R2正等着资源 R1。两边都不肯先释放自己手里的资源于是谁也别想往前走。教科书上把死锁的条件归纳为四个凑齐了才会死锁互斥、持有并等待、不可剥夺、循环等待。互斥是说一个资源同一时刻只能被一个进程占用持有并等待是说进程已经占着一些资源又去申请别的资源不可剥夺是说进程占着的资源不能被系统强行拿走只能由它自己释放循环等待则是多个进程之间形成了一个你等我、我等你的环。四个条件同时成立程序大概率就僵在那一动不动了。我在实际排查问题的时候死锁现场往往不是直观可见的。进程状态看起来都是运行中日志也在打但吞吐量就是停在某个数值上不去有时候要靠挂上调试器看线程栈才发现两个线程互相阻塞。等到定位到根因通常已经浪费了不少时间。所以死锁这件事能避免就不要等到检测出来再处理。1.2 银行家和操作系统在算同一笔账银行家算法名字里的银行家对应的是银行业务里的一种审批逻辑。银行把钱贷给企业不可能只问一句你现在缺多少它要评估企业未来的还款能力还要看同一时期其他企业还本付息之后的现金流够不够覆盖新贷款。如果所有企业的还款计划叠加起来会让银行周转不开哪怕眼前账面还有余额这笔贷款也不该放。操作系统分配资源面临一模一样的问题。进程申请资源时系统眼前可能有空闲资源但这些资源发出去之后其他进程后续的需求能不能被满足没人知道。如果照单全发碰上某个进程把资源卡住又等新资源整个系统就进入死锁。银行家算法做的事情就是让操作系统在每次分配前扮演银行信贷员按当前各进程的最大需求声明推演一下能否找到一条路径让所有进程都能顺利跑到终点。能推演出来就分配推演不出来就先拒绝让进程等一等。银行业务操作系统资源管理客户向银行声明贷款需求上限进程声明最大资源需求 Max银行统计当前可用现金系统统计可用资源 Available客户未偿还的贷款本金已分配给进程的资源 Allocation客户还需要借多少钱进程还需要的资源 Need放款前做信用和现金流推演分配前做安全性检查这段对应关系理清楚之后银行家算法为什么叫这个名字就不再是单纯的记忆点而是把整个算法的行为模式都串起来了。1.3 安全状态是整个算法的核心银行家算法反复提到安全状态和安全序列。安全状态的意思是针对当前的资源分配局面至少存在一条进程执行顺序能让所有进程都拿到各自所需的最大资源、顺利执行完毕。这条顺序就是安全序列。注意安全状态不是说系统一定不会死锁而是在最坏情况下还有退路。反过来不安全状态也不等于立即死锁只是说系统已经找不到一条能保证所有进程都完成的路径死锁风险变大。银行家算法对不安全容忍度为零一旦发现某个请求会让系统进入不安全状态立刻拒绝。我自己理解这个东西时用的一个类比是走迷宫。安全状态等于你手里还有一张完整的地图能规划出一条通向出口的路线不安全状态则是地图已经残缺你不知道哪条路会堵死。地图残了不一定这次就会死锁但继续往下走的赌博性质太强银行家的选择是宁可退回原地也不冒这个险。2. 数据结构的五个账本字段2.1 Available、Max、Allocation、Need、Request 分别是谁动手写代码之前先得把银行家算法维护的数据搞清楚。它一共会用到五个东西Available系统当前每类资源的可用实例数是一个向量。比如 (3,3,2) 表示 A 类剩 3 个、B 类剩 3 个、C 类剩 2 个。Max每个进程声明的最大资源需求是一个矩阵。行对应进程列对应资源类型。Allocation系统当前已经分配给每个进程的资源数也是一个矩阵。Need每个进程还需要的资源数等于 Max 减去 Allocation。Request进程本次请求的资源向量。Need 不是独立输入它由 Max 和 Allocation 推导出来。这个减法很有讲究它代表进程距离运行完成还差多少资源。在操作系统眼里已经分配出去的资源迟早会因为进程完成而归还安全性检查里就是靠这个归还动作来扩大可用资源池的。向量比较的语义也得提前统一。判断进程 i 的 Need 是否小于等于系统当前的 Work不是比一个数而是逐类资源比较每一类资源的需求都不能超过对应类的可用数才算满足。2.2 用一组经典数据把账本填满先给出一组教材级数据后面所有推演都基于它。系统里有 4 类进程 P0 到 P43 类资源 A、B、C。进程Max (A B C)Allocation (A B C)Need (A B C)P07 5 30 1 07 4 3P13 2 22 0 01 2 2P29 0 23 0 26 0 0P32 2 22 1 10 1 1P44 3 30 0 24 3 1当前系统可用资源 Available 为 (3,3,2)。拿到这张表之后可以先肉眼扫一遍。P3 目前的 Need 只有 (0,1,1)总量需求很小P1 的 Need 是 (1,2,2)也不大。这些就是推演安全序列时最容易率先完成的进程。2.3 为什么 Max 参数最难拿银行家算法有一个在实际工程里非常扎手的前提系统必须提前知道每个进程的最大资源需求 Max。这个参数看着简单真放到生产环境里很难拿到。一个进程的执行路径可能依赖输入数据可能依赖网络响应可能依赖其他服务的返回结果它凭什么在创建的时候就能声明一个绝对准确的最大值所以在真实系统里银行家算法要么用于资源需求相对可控的场景要么把 Max 设置得很保守。Max 一旦设得太宽可用资源很容易显得不足算法会频繁拒绝请求设得太窄进程在执行后半段发现真实需求超出声明算法会直接报错。这种既不能宽也不能窄的局面正是银行家算法没能在通用操作系统里成为主流的重要原因。后面第 6 节还会专门讲。3. 安全性检查推演放贷会不会崩3.1 算法步骤拆解安全性检查是银行家算法的看家本领。它的目的不是找出实际执行顺序而是回答一个问题在当前的资源分配局面下存不存在一条让所有进程都能完成的路。算法步骤可以写成这样设置两个临时变量Work AvailableFinish[i] false其中 i 遍历所有进程。从进程集合里找一个满足条件的进程 iFinish[i] false并且 Need[i] 的每一项都小于等于 Work 的对应项。找到之后假装这个进程可以一路运行到结束然后释放它占用的全部资源。释放动作等价于把它的 Allocation 加到 Work 上同时把 Finish[i] 置为 true。重复第 2 步直到找不到任何满足条件的进程。最后检查 Finish 数组如果所有进程的 Finish 都是 true说明系统处于安全状态否则就是不安全状态。这里的关键点在于假装进程运行完成。安全性算法不是真实调度它只是做一个压力测试先把资源借给某个急需完成的进程等它归还后系统的可用资源必然变多然后再看能不能喂饱剩下的人。如果一轮一轮喂下去能把所有进程都喂完说明系统存在一条活路。3.2 一个完整的安全序列推演拿上面那组数据手工走一遍安全性检查。初始 Work (3,3,2)。第一轮找进程P0 的 Need 是 (7,4,3)每一项都大于 Work不满足P1 的 Need 是 (1,2,2)逐项比较 (1,2,2) ≤ (3,3,2)满足。于是选中 P1等它运行完成后释放 Allocation (2,0,0)Work 更新为 (5,3,2)Finish[P1] true。继续找P3 的 Need 是 (0,1,1)(0,1,1) ≤ (5,3,2)满足。选中 P3释放 (2,1,1)Work 更新为 (7,4,3)。接着 P4 的 Need 是 (4,3,1)≤ (7,4,3)满足。释放 (0,0,2)Work 更新为 (7,4,5)。然后 P0 的 Need 是 (7,4,3)≤ (7,4,5)满足。释放 (0,1,0)Work 更新为 (7,5,5)。最后 P2 的 Need 是 (6,0,0)≤ (7,5,5)满足。释放 (3,0,2)Work 更新为 (10,5,7)。全部进程的 Finish 都置为 true系统安全。安全序列是 P1 → P3 → P4 → P0 → P2。推演过程整理成表格会更直观步骤选中进程当前 Work该进程 Need判断结果释放后 Work1P1(3,3,2)(1,2,2)满足(5,3,2)2P3(5,3,2)(0,1,1)满足(7,4,3)3P4(7,4,3)(4,3,1)满足(7,4,5)4P0(7,4,5)(7,4,3)满足(7,5,5)5P2(7,5,5)(6,0,0)满足(10,5,7)手工推演的价值在于建立资源归还的直觉。每完成一个进程Work 会变大而不是变小。进程之所以可能死锁是因为大家都在等别人手里的资源而安全性算法通过模拟某个进程先完成并归还让资源逐步充裕起来这就是所有进程都能完成的秘密。3.3 遍历顺序不会改变结论很多初学者会问我从 P0 开始找跟从 P3 开始找结果会不会不一样结论是不会。如果系统当前处于安全状态那么无论先选中哪个可满足的进程都不会把路走死。原因很简单假设有两个进程 A 和 B 都满足当前条件你先选了 A完成之后 Work 变得更大B 的 Need 并没有变所以 B 在下一轮依然可以被选中。也就是说任何现在就能满足的进程在别的进程先完成之后只会更容易满足不会更难。从代码实现角度看每次从编号最小的进程开始扫描只是一种懒人做法。它不会漏掉某个可行的进程最坏情况下只是让安全序列的具体顺序不同。安全性检查要判定的只有存在不存在安全序列而不是安全序列具体是什么所以遍历顺序无关紧要。真正要紧的是日志里别因为遍历顺序不同就误以为算法有问题。4. 收到资源请求时怎么判准批不批4.1 三步判定流程当某个进程发出资源请求 Request 时银行家算法不是马上看账上有多少钱它有严格的审批流程。第一步检查 Request 是否不超过该进程的 Need。如果某个进程申请的资源数比它当初声明的最大需求还多说明这个进程的行为已经超出预期属于非法请求应该直接报错。第二步检查 Request 是否不超过系统的当前可用资源 Available。如果当前系统根本没有这么多资源进程就得原地等待不能动用未来的资源。第三步也是最关键的一步。假设这次分配先执行下去从 Available 里减去 Request把 Request 加到 Allocation 上再从 Need 里减去 Request。然后在这个假装分配完成的新状态下跑一次安全性检查。如果新状态依然安全这次请求就获批如果新状态不安全就回滚到请求前的样子并让进程等待。第三步的原理是批准请求不能只看现在能不能给更要看给了之后系统还能不能正常走下去。这就像银行放贷不能只看客户现在工资卡里的余额还要看这笔贷款放出去之后其余客户的还款计划会不会被打乱。4.2 一个必须拒绝的请求P1 申请 (1,0,2)还是用前面那组初始数据假设 P1 发出请求 (1,0,2)。先做第一步Request (1,0,2)P1 的 Need (1,2,2)逐项比较没有超过检查通过。第二步Request ≤ Available (3,3,2)也通过。开始试分配。更新后的系统状态变成Available (2,3,0)Allocation[P1] 从 (2,0,0) 变成 (3,0,0)Need[P1] 从 (1,2,2) 变成 (0,2,0)对这个新状态跑安全性检查。Work 初始为 (2,3,0)逐个检查进程P0 的 Need (7,4,3) 不满足P1 的 Need (0,2,0) 满足选 P1释放 (3,0,0)Work 变成 (5,3,0)。接着再找P2 的 Need (6,0,0) 不满足因为 A 类资源只有 5P3 的 Need (0,1,1) 不满足因为 C 类资源只有 0P4 的 Need (4,3,1) 也不满足。找完了还有一堆进程的 Finish 是 false新状态不安全。所以 P1 这个请求必须被拒绝。就算系统账面上能给得出 (1,0,2)给了之后整个系统会进入找不到安全序列的状态后续任何进程都可能卡死。这笔资源宁可不借也不能借。4.3 一个可以同意的请求P4 申请 (0,0,1)为了对比再看一个能通过的请求。还是初始状态P4 请求 (0,0,1)。第一步Request ≤ Need[P4] (4,3,1)通过。第二步Request ≤ Available (3,3,2)通过。试分配之后Available (3,3,1)Allocation[P4] 从 (0,0,2) 变成 (0,0,3)Need[P4] 从 (4,3,1) 变成 (4,3,0)跑安全性检查。Work (3,3,1)。P3 的 Need (0,1,1) 满足释放 (2,1,1)Work 变成 (5,4,2)。接着 P1 的 Need (1,2,2) 满足释放 (2,0,0)Work 变成 (7,4,2)。接着 P4 的 Need (4,3,0) 满足释放 (0,0,3)Work 变成 (7,4,5)。后面 P0、P2 也都能完成。这个请求把系统带向了另一个安全序列P3 → P1 → P4 → P0 → P2所以可以批准。正反两个例子放在一起能很清楚地看到银行家算法的取舍标准不看你这次要得多不多而看给了你之后全局还能不能找到一条活路。4.4 试分配和回滚的代码技巧试分配这个动作写代码时最容易翻车的地方是改坏了状态没恢复。我的习惯是在真正改动前把 Available、Allocation、Need 三个数据结构备份一份。安全性检查通过就保留新状态不通过就把备份还原回去对外表现成这次请求从未发生过。备份的开销其实很小。资源种类和进程数量通常也就几十个量级复制三个二维数组是纳秒级别的事完全不必为了省这点拷贝而裸改状态。回滚逻辑放在一个函数里后面调试的时候会很舒服。如果不想备份也可以改完之后再反向操作来恢复比如 Available 加上 Request、Allocation 减去 Request、Need 加上 Request。但这样做一旦哪个加减号写反状态就彻底污染了后面的测试结果全是错的。我吃过这个亏所以宁可多写几行备份代码换一个无论如何都能还原的确定性。5. 把银行家算法写成可运行的代码5.1 核心代码结构写一个最小可运行的 C 版本类里面包含安全性检查和请求处理两个核心方法。代码用标准 C11 就能编译不依赖任何第三方库。#include iostream #include vector using namespace std; class Banker { private: int pNum; int rNum; vectorvectorint maxRes; vectorvectorint alloc; vectorvectorint need; vectorint avail; bool vec_leq(const vectorint a, const vectorint b) { for (int i 0; i rNum; i) { if (a[i] b[i]) return false; } return true; } bool safetyCheck(vectorint safeSeq) { vectorint work avail; vectorbool finish(pNum, false); safeSeq.clear(); for (int k 0; k pNum; k) { bool found false; for (int i 0; i pNum; i) { if (finish[i]) continue; if (vec_leq(need[i], work)) { finish[i] true; safeSeq.push_back(i); for (int j 0; j rNum; j) { work[j] alloc[i][j]; } found true; break; } } if (!found) return false; } return true; } public: Banker(vectorvectorint maxInput, vectorvectorint allocInput, vectorint availInput) : maxRes(maxInput), alloc(allocInput), avail(availInput) { pNum maxRes.size(); rNum maxRes[0].size(); need.resize(pNum, vectorint(rNum, 0)); for (int i 0; i pNum; i) { for (int j 0; j rNum; j) { need[i][j] maxRes[i][j] - alloc[i][j]; } } } bool isSafe() { vectorint seq; bool ok safetyCheck(seq); if (ok) { cout 安全序列: ; for (int i 0; i (int)seq.size(); i) { cout P seq[i]; if (i 1 (int)seq.size()) cout - ; } cout endl; } else { cout 当前状态不安全 endl; } return ok; } bool request(int pid, vectorint req) { if (pid 0 || pid pNum) return false; if (!vec_leq(req, need[pid])) { cout 错误请求量超过该进程的最大需求 endl; return false; } if (!vec_leq(req, avail)) { cout 系统当前可用资源不足进程需要等待 endl; return false; } auto backupAvail avail; auto backupAlloc alloc; auto backupNeed need; for (int j 0; j rNum; j) { avail[j] - req[j]; alloc[pid][j] req[j]; need[pid][j] - req[j]; } vectorint safeSeq; bool ok safetyCheck(safeSeq); if (!ok) { avail backupAvail; alloc backupAlloc; need backupNeed; cout 拒绝该请求会让系统进入不安全状态 endl; return false; } cout 同意该请求仍让系统处于安全状态 endl; cout 对应安全序列: ; for (int i 0; i (int)safeSeq.size(); i) { cout P safeSeq[i]; if (i 1 (int)safeSeq.size()) cout - ; } cout endl; return true; } void showStatus() { cout 当前可用资源: ; for (int v : avail) cout v ; cout endl; for (int i 0; i pNum; i) { cout P i Max: ; for (int v : maxRes[i]) cout v ; cout Alloc: ; for (int v : alloc[i]) cout v ; cout Need: ; for (int v : need[i]) cout v ; cout endl; } } }; int main() { vectorvectorint maxRes { {7, 5, 3}, {3, 2, 2}, {9, 0, 2}, {2, 2, 2}, {4, 3, 3} }; vectorvectorint alloc { {0, 1, 0}, {2, 0, 0}, {3, 0, 2}, {2, 1, 1}, {0, 0, 2} }; vectorint avail {3, 3, 2}; Banker banker(maxRes, alloc, avail); banker.showStatus(); cout --- 初始安全性检查 --- endl; banker.isSafe(); cout --- P1 请求 (1,0,2) --- endl; banker.request(1, {1, 0, 2}); cout --- P4 请求 (0,0,1) --- endl; banker.request(4, {0, 0, 1}); return 0; }构造函数里Need 矩阵是由 Max 和 Allocation 直接算出来的不需要额外的输入。这个设计能让外部调用方少传一个参数也降低了把 Need 数据传错的概率。5.2 代码里容易踩的细节vec_leq 这个比较函数是整个逻辑的地基。如果写成某种总需求加起来不超过总可用数的算法安全性检查就废了因为资源不是混着用的每一类资源必须单独满足。比如甲进程需要 A 类 10 个、B 类 0 个系统 A 类有 8 个、B 类有 100 个总需求 10 虽然小于总可用 108但 A 类依然不够进程照样跑不起来。所以比较必须逐分量进行。Finish 数组在每次安全性检查时都要重新初始化为 false。有些实现会把这个数组放到类成员变量里甚至静态变量里第二次调用时残留第一次的结果排查起来非常头疼。试分配时 Allocation 加、Need 减、Available 减三个更新方向要一起维护。我见过只更新 Available 和 Allocation、忘记更新 Need 的情况结果安全性检查永远在拿旧的 Need 做判断整个算法行为就乱了。5.3 跑一下代码会看到什么这段代码运行起来初始安全性检查会输出安全序列 P1 - P3 - P4 - P0 - P2。接着 P1 请求 (1,0,2)输出拒绝该请求会让系统进入不安全状态。因为请求被拒后状态回滚P4 接着请求 (0,0,1) 时系统回到最初的可用状态输出同意该请求仍让系统处于安全状态并给出新安全序列 P3 - P1 - P4 - P0 - P2。这段输出可以作为自测用例。你改完代码之后如果这三个输出跟预期不一致大概率是试分配或者安全性检查里某个细节写错了。把这段代码复制下来跑一遍比自己猜要快得多。6. 常见坑与理解误区6.1 死锁避免、预防、检测混为一谈学习银行家算法最常见的误区是把死锁避免和死锁预防弄混。死锁预防是在设计阶段直接打破四个必要条件中的一个比如要求进程一次性申请所有需要的资源或者给资源编号、强制进程按顺序申请。这样做很粗暴资源利用率偏低但实现简单。死锁避免则是在分配时刻动态判断不提前限制进程怎么申请而是每次分配前问一句这样做安全吗。死锁检测又是另一条路线允许死锁发生系统定期或按需检查资源分配图里有没有环发现死锁之后再做恢复。恢复方式包括强制撤销某个进程、回滚事务等。三者目标相同但策略和开销截然不同。银行家算法属于死锁避免它的核心动作是拒绝不安全的分配而不是阻止进程提出某种请求。6.2 银行家算法的三个先天限制银行家算法在教学里很经典但拿到真实系统里用有三个绕不开的限制。第一必须提前知道每个进程的最大资源需求。前面已经说过这在现实工程里非常难获得。进程逻辑越复杂最大需求越难以预测强行声明出来的 Max 很可能和实际运行轨迹对不上。第二系统里的进程数量和资源数量在运行期间要保持固定。银行家算法假设这些参数在系统生命周期内不变化但真实系统里进程会动态启停资源也可能热插拔或扩容。一旦参数变了之前算出来的安全序列就失去意义。第三银行家算法只解决死锁不解决饥饿。它可能保护了系统不死锁但某个低优先级的进程可能每次都被安全性检查排除在安全序列之外导致它迟迟拿不到资源永远等不到运行完成。死锁避免了饿死却可能出现这个细节在工程设计中很容易被忽略。6.3 实现层面的几个易错细节比较方向写反是最低级的错误。判断进程是否可以推进用的是 Need[i] ≤ Work拿的是进程还缺多少和系统还有多少比不是进程最多需要多少和系统还有多少比。Max 只在初始化时参与一次减法后面所有比较都要用 Need。另一个问题是可扩展性。银行家算法的时间复杂度大约是 O(n²·m)n 是进程数m 是资源类型数。进程多了之后每次请求都要跑一遍这种量级的计算对高频请求的系统来说性能不友好。如果要在真实模块里用至少要控制单次安全检查的调用频率或者降低检查时的进程集合规模。多线程环境下调用银行家算法要额外加锁。算法内部有读取状态-检查-修改状态的序列如果不加锁另一个线程在检查过程中插入一次分配整个推演就基于错误数据了。这个坑和普通并发模块的竞态问题一样只是更容易被忽略因为你往往只盯着算法逻辑本身。7. 从死锁到工程实践我的一些体会7.1 为什么通用操作系统不直接用它银行家算法听起来很理想但现代通用操作系统基本没有直接拿它做资源分配。原因就是上面说的限制太多进程数量动态变化资源种类和总量也在变最大需求声明在绝大多数应用场景下拿不到准确值再加上每次分配都要跑一次安全检查性能开销也不小。通用操作系统更常用的是组合拳把资源按类型编号要求进程按序申请从源头打破循环等待或者允许死锁发生交给上层通过超时、重启等方式恢复。银行家算法呢更多出现在教学演示、嵌入式系统设计、资源需求高度可控的专用系统里。在这些场景里进程集合和资源上限是固定的Max 可以通过静态分析拿到算法性价比就能体现出来。7.2 它的思想还藏在哪些地方虽然完整版银行家算法不常见但它的思想在很多系统里都有影子。数据库事务在申请锁的时候默认会提前规划锁的获取顺序避免两个事务互相持有对方需要的锁。这相当于在预防阶段就砍掉了循环等待和银行家算法安全第一的思路一脉相承。分布式资源调度场景里任务提交时会声明需要的 CPU、内存等资源配额调度器根据集群剩余总量决定是不是接收。这个必须预先声明需求、按总量判断能不能接纳的模式和银行家算法的 Max、Available 思想非常像。差别在于分布式环境里没有全局时钟和全局状态实现细节会更复杂。嵌入式实时系统里有些设计会在启动阶段做一次银行家式的静态分析把所有任务的资源需求汇总预先算出一套不会互相阻塞的分配方案。运行期只按这套方案执行避免动态判断的开销。这种把安全的决策前置到编译期或启动期的思路我觉得比算法本身更有借鉴价值。7.3 给正在学这个算法的读者三句话第一句先手算三遍再写代码。不要一上来就抄代码拿纸拿笔把第 3 节的推演过程走几遍重点感受 Work 是怎么一步步变大的。第二句写完代码后用第 5 节那两个请求案例做自测。P1 请求 (1,0,2) 必须被拒绝P4 请求 (0,0,1) 必须被同意。这两个输出就是算法的信号灯全对了再往下测复杂场景。第三句如果要在自己项目的并发模块里应用先想清楚 Max 从哪来。没有可靠的最大需求声明银行家算法就像一个没有信用记录的信贷员再精妙的审批流程也发挥不出作用。我踩过一次坑在一个资源管理模块里强行套银行家算法结果 Max 设得不准系统频繁拒绝合法请求最后只能换成分级资源池加超时检测的方案。所以说算法本身没有对错能不能在和业务匹配的场景里用好才是真正的考验。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑