計(jì)安全事件告警任務(wù)調(diào)度系統(tǒng):時(shí)間輪與分布式鎖實(shí)戰(zhàn))
1. 面試定位這道系統(tǒng)開(kāi)發(fā)一到底在考什么奇安信的服務(wù)端開(kāi)發(fā)工程師面試和互聯(lián)網(wǎng)公司常見(jiàn)的 CRUD 型面試不太一樣。安全公司的服務(wù)端承接的是海量安全設(shè)備的接入、告警事件的處理、策略配置的下發(fā)這套體系對(duì)系統(tǒng)的穩(wěn)定性、實(shí)時(shí)性、一致性要求都很高。換句話說(shuō)他們考察的不是你會(huì)不會(huì)用 Spring Boot 寫(xiě)個(gè)接口而是你有沒(méi)有能力構(gòu)建一個(gè)能扛住千萬(wàn)級(jí)設(shè)備上報(bào)、毫秒級(jí)響應(yīng)的底層服務(wù)系統(tǒng)。系統(tǒng)開(kāi)發(fā)一這個(gè)題目從命名來(lái)看屬于一輪技術(shù)面中的核心編碼與設(shè)計(jì)題。結(jié)合奇安信的業(yè)務(wù)場(chǎng)景這類題目大概率會(huì)圍繞以下幾個(gè)方面展開(kāi)任務(wù)調(diào)度、事件處理管道、分布式數(shù)據(jù)一致性、或者某個(gè)具體基礎(chǔ)組件的設(shè)計(jì)與實(shí)現(xiàn)。我在實(shí)際面試中遇到的版本是要設(shè)計(jì)并實(shí)現(xiàn)一個(gè)安全事件采集與告警通知的任務(wù)調(diào)度系統(tǒng)。下面我按這個(gè)方向把完整的思路、代碼實(shí)現(xiàn)和踩坑過(guò)程拆開(kāi)講。這道題的隱含考察點(diǎn)其實(shí)有四個(gè)層次第一層是看你能不能把模糊需求轉(zhuǎn)化為清晰的技術(shù)方案第二層是看你的編碼基本功邊界條件處理得好不好第三層是看你對(duì)分布式場(chǎng)景的理解比如冪等性、超時(shí)、重試、并發(fā)控制第四層是看你的系統(tǒng)思維有沒(méi)有考慮監(jiān)控、日志、容量規(guī)劃這些生產(chǎn)環(huán)境才關(guān)心的東西。只停留在第一二層基本就掛了。2. 系統(tǒng)整體設(shè)計(jì)與技術(shù)選型2.1 需求拆解先從一句模糊描述開(kāi)始面試官給的原始需求通常非常簡(jiǎn)短比如實(shí)現(xiàn)一個(gè)安全事件采集與告警任務(wù)調(diào)度系統(tǒng)支持任務(wù)定時(shí)執(zhí)行、失敗重試、并發(fā)控制。這句話信息量很大但你需要先做的是把它拆成具體的功能模塊任務(wù)模型一個(gè)任務(wù)包含任務(wù) ID、類型、執(zhí)行時(shí)間、執(zhí)行參數(shù)、狀態(tài)、重試次數(shù)等字段。任務(wù)調(diào)度根據(jù)任務(wù)的執(zhí)行時(shí)間決定何時(shí)分發(fā)到執(zhí)行器。任務(wù)執(zhí)行執(zhí)行器接收任務(wù)后跑具體的采集或告警邏輯。失敗重試執(zhí)行失敗后按策略進(jìn)行有限次數(shù)的重試。并發(fā)控制同一類型的任務(wù)不能并發(fā)執(zhí)行同一任務(wù)的多個(gè)實(shí)例也不能同時(shí)跑。在實(shí)際面試中我建議先不要急著寫(xiě)代碼而是把上面的模塊畫(huà)出來(lái)和面試官對(duì)齊邊界。這本身就是加分項(xiàng)因?yàn)榻^大多數(shù)候選人上來(lái)就埋頭寫(xiě)結(jié)果方向全偏。我當(dāng)時(shí)在紙上畫(huà)的模塊劃分是調(diào)度器Scheduler、任務(wù)隊(duì)列Task Queue、執(zhí)行器Executor、狀態(tài)存儲(chǔ)State Store。這四個(gè)組件的職責(zé)單一耦合度低后續(xù)擴(kuò)展也比較容易。2.2 方案選型為什么不用現(xiàn)成框架很多人第一反應(yīng)是用 Quartz、ElasticJob 或者 XXL-JOB。但在面試?yán)锶绻阒苯诱f(shuō)我用 XXL-JOB 就行面試官大概率會(huì)追問(wèn)你那它的底層原理是什么如果你答不上來(lái)反而暴露短板。我的建議是可以提你會(huì)用這些框架但更要展示你能徒手實(shí)現(xiàn)一個(gè)簡(jiǎn)化版的核心機(jī)制。這樣既證明你有工程視野又證明你有底層能力。我也對(duì)比過(guò)幾種常見(jiàn)的實(shí)現(xiàn)方案方案優(yōu)點(diǎn)缺點(diǎn)適用場(chǎng)景Quartz成熟穩(wěn)定支持 cron 表達(dá)式不支持分布式需要額外引入數(shù)據(jù)庫(kù)鎖單機(jī)任務(wù)調(diào)度ElasticJob支持分布式分片有運(yùn)維界面依賴 Zookeeper部署較重大規(guī)模分片任務(wù)自研簡(jiǎn)化版輕量、無(wú)外部依賴、原理可控功能有限不再造輪子面試展示、中小規(guī)模系統(tǒng)我最終選擇自研一個(gè)基于時(shí)間輪Time Wheel加延遲隊(duì)列的組合方案。時(shí)間輪負(fù)責(zé)高效的定時(shí)任務(wù)觸發(fā)延遲隊(duì)列負(fù)責(zé)處理任務(wù)的等待和重試。這個(gè)方案的好處是不依賴任何外部中間件純 JDK 就能實(shí)現(xiàn)而且在面試中非常有聊頭——時(shí)間輪算法本身就是一個(gè)很經(jīng)典的考察點(diǎn)。2.3 核心原理時(shí)間輪為什么高效時(shí)間輪算法的本質(zhì)是個(gè)環(huán)形數(shù)組數(shù)組的每個(gè)槽位代表一個(gè)時(shí)間刻度。一個(gè)指針每隔固定時(shí)間比如 1 秒跳動(dòng)一格指針指向的槽位中掛著的任務(wù)就是當(dāng)前時(shí)刻需要執(zhí)行的任務(wù)。舉個(gè)例子假設(shè)時(shí)間輪的刻度是 1 秒一圈有 60 個(gè)槽位。一個(gè)任務(wù)需要 5 秒后執(zhí)行就把它掛在當(dāng)前指針往后數(shù)第 5 個(gè)槽位的鏈表中。當(dāng)指針走到那個(gè)槽位就把鏈表里的任務(wù)取出來(lái)分發(fā)。這種設(shè)計(jì)的查找復(fù)雜度是 O(1)插入復(fù)雜度也是 O(1)比使用優(yōu)先隊(duì)列PriorityQueue的 O(log n) 插入要高效得多。當(dāng)然時(shí)間輪也有局限性如果任務(wù)需要延遲的時(shí)間超過(guò)一圈的刻度范圍就需要記錄圈數(shù)rounds或者使用多層時(shí)間輪。我在實(shí)現(xiàn)時(shí)采用的方案是每個(gè)任務(wù)記錄一個(gè)剩余輪數(shù)每走完一圈就減一減到零才觸發(fā)。這樣實(shí)現(xiàn)簡(jiǎn)單也能覆蓋絕大多數(shù)定時(shí)場(chǎng)景。3. 代碼實(shí)現(xiàn)與關(guān)鍵環(huán)節(jié)解析3.1 任務(wù)模型的屬性設(shè)計(jì)寫(xiě)代碼之前先定義清楚任務(wù)的數(shù)據(jù)結(jié)構(gòu)。這個(gè)類是所有邏輯的基石字段設(shè)計(jì)得不好后面每一步都會(huì)難受。我在面試中給出的版本是這樣的public class Task { private String taskId; // 全局唯一任務(wù)ID private String type; // 任務(wù)類型如 collect、alert private long executeTime; // 計(jì)劃執(zhí)行時(shí)間毫秒時(shí)間戳 private MapString, Object params; // 執(zhí)行參數(shù) private int maxRetryCount; // 最大重試次數(shù) private int currentRetryCount; // 當(dāng)前已重試次數(shù) private TaskStatus status; // 任務(wù)狀態(tài)枚舉 public enum TaskStatus { PENDING, // 等待調(diào)度 RUNNING, // 執(zhí)行中 SUCCESS, // 執(zhí)行成功 FAILED, // 執(zhí)行失敗 RETRY_NOT_NEEDED // 失敗且不需要重試 } }這里有幾個(gè)需要注意的細(xì)節(jié)。第一taskId必須全局唯一這是分布式環(huán)境下做冪等控制的基礎(chǔ)第二status字段在并發(fā)場(chǎng)景下要處理可見(jiàn)性問(wèn)題建議用volatile修飾或通過(guò)狀態(tài)機(jī)統(tǒng)一管理第三params用Map而不是固定字段是為了適配不同類型任務(wù)的差異化參數(shù)。3.2 時(shí)間輪的實(shí)現(xiàn)要點(diǎn)時(shí)間輪的核心數(shù)據(jù)結(jié)構(gòu)是數(shù)組加鏈表。數(shù)組的每個(gè)槽位是一個(gè)LinkedListTask指針用AtomicInteger或簡(jiǎn)單的int加鎖保護(hù)。關(guān)鍵代碼如下public class TimeWheel { private final int tickDuration; // 每個(gè)刻度的時(shí)間間隔單位毫秒 private final int wheelSize; // 槽位數(shù)量 private final AtomicInteger currentIndex new AtomicInteger(0); private final ListLinkedListTask slots; public TimeWheel(int tickDuration, int wheelSize) { this.tickDuration tickDuration; this.wheelSize wheelSize; this.slots new ArrayList(wheelSize); for (int i 0; i wheelSize; i) { slots.add(new LinkedList()); } } public void addTask(Task task) { long delay task.getExecuteTime() - System.currentTimeMillis(); if (delay 0) { // 立即執(zhí)行 executor.submit(() - executeTask(task)); return; } int ticks (int) (delay / tickDuration); int targetIndex (currentIndex.get() ticks) % wheelSize; // 記錄剩余輪數(shù) int rounds ticks / wheelSize; task.setRounds(rounds); synchronized (slots.get(targetIndex)) { slots.get(targetIndex).add(task); } } public void advance() { int index currentIndex.getAndUpdate(i - (i 1) % wheelSize); LinkedListTask bucket slots.get(index); synchronized (bucket) { IteratorTask iterator bucket.iterator(); while (iterator.hasNext()) { Task task iterator.next(); if (task.getRounds() 0) { task.setRounds(task.getRounds() - 1); } else { iterator.remove(); executor.submit(() - executeTask(task)); } } } } }這里要注意的坑是advance()方法必須由一個(gè)固定頻率的調(diào)度線程驅(qū)動(dòng)不能靠任務(wù)自己觸發(fā)否則會(huì)出現(xiàn)時(shí)間漂移。我在實(shí)現(xiàn)時(shí)用了一個(gè)ScheduledExecutorService每隔tickDuration毫秒調(diào)用一次advance()。3.3 任務(wù)隊(duì)列與執(zhí)行器協(xié)作時(shí)間輪負(fù)責(zé)觸發(fā)但觸發(fā)之后任務(wù)并不一定馬上執(zhí)行。如果同一個(gè)時(shí)間點(diǎn)有大量任務(wù)同時(shí)到期直接全部丟進(jìn)線程池可能導(dǎo)致瞬時(shí)壓力過(guò)大。我在時(shí)間輪和執(zhí)行器之間加了一層內(nèi)存隊(duì)列做緩沖同時(shí)引入信號(hào)量做并發(fā)控制。執(zhí)行器的核心邏輯如下public class TaskExecutor { private final ExecutorService workerPool; private final Semaphore semaphore; // 控制最大并發(fā)數(shù) public TaskExecutor(int poolSize, int maxConcurrent) { this.workerPool Executors.newFixedThreadPool(poolSize); this.semaphore new Semaphore(maxConcurrent); } public void execute(Task task) { try { semaphore.acquire(); workerPool.submit(() - { try { task.setStatus(Task.TaskStatus.RUNNING); TaskResult result doExecute(task); if (result.isSuccess()) { task.setStatus(Task.TaskStatus.SUCCESS); } else { handleRetry(task); } } catch (Exception e) { log.error(task execute failed, e); handleRetry(task); } finally { semaphore.release(); } }); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }doExecute方法根據(jù)任務(wù)的type字段分發(fā)到不同的業(yè)務(wù)處理器。這里可以預(yù)先維護(hù)一個(gè)MapString, TaskHandler的注冊(cè)表避免一堆if-else的判斷。在面試中加上這個(gè)設(shè)計(jì)點(diǎn)能讓面試官覺(jué)得你有代碼潔癖而且是那種有工程經(jīng)驗(yàn)才會(huì)有的潔癖。3.4 冪等性設(shè)計(jì)與 Redis 鎖的應(yīng)用安全事件采集和告警場(chǎng)景中最忌諱的是重復(fù)執(zhí)行。比如一條告警消息因?yàn)榫W(wǎng)絡(luò)超時(shí)被重試結(jié)果發(fā)送了兩次用戶就會(huì)收到重復(fù)告警。解決這個(gè)問(wèn)題的標(biāo)準(zhǔn)做法是引入分布式鎖。我在任務(wù)執(zhí)行前加一個(gè)加鎖操作以taskId為 key嘗試獲取分布式鎖可以用 Redis 的SETNX也可以用 ZooKeeper。只有獲取成功的節(jié)點(diǎn)才執(zhí)行任務(wù)其他節(jié)點(diǎn)直接忽略。同時(shí)鎖要設(shè)置過(guò)期時(shí)間防止持有鎖的節(jié)點(diǎn)崩潰導(dǎo)致死鎖。public boolean tryLock(String taskId, long timeoutMs) { String lockKey task:lock: taskId; String requestId UUID.randomUUID().toString(); boolean locked redis.setIfAbsent(lockKey, requestId, timeoutMs, TimeUnit.MILLISECONDS); return locked; } public void unlock(String taskId, String requestId) { String lockKey task:lock: taskId; // 使用Lua腳本保證判斷和刪除的原子性 String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; redis.execute(script, Arrays.asList(lockKey), requestId); }這里有一個(gè)非常隱蔽的坑釋放鎖時(shí)不能直接del必須先校驗(yàn)requestId是否匹配。否則可能出現(xiàn)一種情況——線程 A 持有的鎖過(guò)期了線程 B 獲取了同一把鎖然后線程 A 執(zhí)行完直接del把線程 B 的鎖刪掉了。用 Lua 腳本保證了 get 和 del 的原子性才能避免這個(gè)經(jīng)典的誤刪問(wèn)題。4. 系統(tǒng)高可用設(shè)計(jì)與擴(kuò)展方案4.1 單機(jī)節(jié)點(diǎn)故障如何保證任務(wù)不丟如果只有單機(jī)部署進(jìn)程崩潰后內(nèi)存中的時(shí)間輪和任務(wù)隊(duì)列都會(huì)丟失。在面試中面試官一定會(huì)追問(wèn)這個(gè)問(wèn)題。我的回答是分兩步做狀態(tài)持久化第一任務(wù)入庫(kù)MySQL 中維護(hù)一張task_meta表記錄任務(wù)的所有元數(shù)據(jù)第二任務(wù)狀態(tài)變化時(shí)同步更新數(shù)據(jù)庫(kù)調(diào)度器啟動(dòng)時(shí)從數(shù)據(jù)庫(kù)恢復(fù)未完成的任務(wù)。CREATE TABLE task_meta ( task_id VARCHAR(64) PRIMARY KEY, task_type VARCHAR(32) NOT NULL, execute_time BIGINT NOT NULL, params TEXT, max_retry_count INT DEFAULT 3, current_retry_count INT DEFAULT 0, status VARCHAR(20) NOT NULL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );恢復(fù)邏輯也很簡(jiǎn)單啟動(dòng)時(shí)查詢status IN (PENDING, RUNNING)的任務(wù)重新放入時(shí)間輪。這里要注意RUNNING狀態(tài)的任務(wù)需要特殊處理——因?yàn)楣?jié)點(diǎn)崩潰時(shí)任務(wù)可能只執(zhí)行了一半恢復(fù)后要重新執(zhí)行所以需要在執(zhí)行器里保證業(yè)務(wù)邏輯的冪等性或者引入執(zhí)行記錄表來(lái)做去重。4.2 多節(jié)點(diǎn)部署任務(wù)分片與一致性哈希單機(jī)容量有限生產(chǎn)環(huán)境必然會(huì)多節(jié)點(diǎn)部署。多節(jié)點(diǎn)之后要解決兩個(gè)問(wèn)題任務(wù)如何分配重復(fù)調(diào)度如何避免我采用的方案是一致性哈希分片。每個(gè)節(jié)點(diǎn)的唯一標(biāo)識(shí)比如 IP端口映射到哈希環(huán)上任務(wù) ID 哈希后落在某個(gè)節(jié)點(diǎn)上只有該節(jié)點(diǎn)負(fù)責(zé)這個(gè)任務(wù)的調(diào)度。這樣每個(gè)任務(wù)在任意時(shí)刻只有一個(gè)調(diào)度者從源頭避免了重復(fù)調(diào)度。節(jié)點(diǎn)增減時(shí)只需要遷移少量任務(wù)即可。關(guān)鍵代碼如下public class ConsistentHashRouter { private final TreeMapInteger, String ring new TreeMap(); private final int virtualNodeCount; // 每個(gè)物理節(jié)點(diǎn)對(duì)應(yīng)虛擬節(jié)點(diǎn)數(shù) public ConsistentHashRouter(ListString nodes, int virtualNodeCount) { this.virtualNodeCount virtualNodeCount; for (String node : nodes) { addNode(node); } } public void addNode(String node) { for (int i 0; i virtualNodeCount; i) { int hash hash(node # i); ring.put(hash, node); } } public String route(String taskId) { int hash hash(taskId); SortedMapInteger, String tailMap ring.tailMap(hash); Integer key tailMap.isEmpty() ? ring.firstKey() : tailMap.firstKey(); return ring.get(key); } private int hash(String key) { // 使用MD5后取32位哈希避免String.hashCode的碰撞問(wèn)題 MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(key.getBytes()); return ((digest[3] 0xFF) 24) | ((digest[2] 0xFF) 16) | ((digest[1] 0xFF) 8) | (digest[0] 0xFF); } }引入虛擬節(jié)點(diǎn)的原因是為了均衡負(fù)載。如果只有少量物理節(jié)點(diǎn)直接用節(jié)點(diǎn)本身的哈希很容易出現(xiàn)數(shù)據(jù)傾斜虛擬節(jié)點(diǎn)可以打散分布。實(shí)際生產(chǎn)環(huán)境中虛擬節(jié)點(diǎn)數(shù)量設(shè)置在 100~200 之間比較合理太小了分布不均太大了內(nèi)存開(kāi)銷增大。4.3 任務(wù)積壓怎么辦動(dòng)態(tài)擴(kuò)容與優(yōu)先級(jí)隊(duì)列一個(gè)非常容易在實(shí)際中踩到的坑是某類任務(wù)執(zhí)行時(shí)間過(guò)長(zhǎng)導(dǎo)致后面的任務(wù)積壓。比如告警通知依賴外部 HTTP 接口如果接口響應(yīng)慢每個(gè)任務(wù)要等 3 秒那 100 個(gè)任務(wù)就要 300 秒完全不能接受。我在設(shè)計(jì)的時(shí)候做了兩個(gè)應(yīng)對(duì)措施第一按任務(wù)類型區(qū)分線程池不同類型之間互不干擾第二任務(wù)隊(duì)列支持優(yōu)先級(jí)緊急告警類任務(wù)優(yōu)先執(zhí)行。線程池的飽和策略也要設(shè)置合理——我用的CallerRunsPolicy當(dāng)線程池滿時(shí)由調(diào)用線程直接執(zhí)行雖然會(huì)阻塞調(diào)度線程但至少不會(huì)丟任務(wù)。另一個(gè)優(yōu)化方向是批量執(zhí)行。如果同一類型、同一目標(biāo)的任務(wù)在同一時(shí)間段內(nèi)大量積壓可以合并成一次批量請(qǐng)求。比如五分鐘內(nèi)同一設(shè)備的 50 條告警合并成一條匯總告警發(fā)出。這在安全場(chǎng)景下也是更合理的產(chǎn)品策略因?yàn)橛脩舨恍枰诙虝r(shí)間內(nèi)收到大量重復(fù)告警。5. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄5.1 任務(wù)重復(fù)執(zhí)行冪等方案在哪里生效這是我實(shí)際遇到的最多的問(wèn)題。排查重復(fù)執(zhí)行的思路是先看鎖有沒(méi)有生效再看任務(wù)狀態(tài)有沒(méi)有更新。我曾經(jīng)遇到過(guò)一種情況兩個(gè)節(jié)點(diǎn)同時(shí)從數(shù)據(jù)庫(kù)恢復(fù)了一批PENDING狀態(tài)的任務(wù)因?yàn)閿?shù)據(jù)庫(kù)隔離級(jí)別默認(rèn)是REPEATABLE_READ兩個(gè)節(jié)點(diǎn)都查詢到了同一條記錄然后各自調(diào)度。雖然是按一致性哈希路由的但恢復(fù)階段的LOAD操作沒(méi)有走路由規(guī)則。解決辦法是把恢復(fù)邏輯改成一個(gè)分布式鎖保護(hù)的流程只有搶到恢復(fù)鎖的節(jié)點(diǎn)才執(zhí)行LOAD操作其他節(jié)點(diǎn)等待。或者用數(shù)據(jù)庫(kù)的SELECT ... FOR UPDATE對(duì)恢復(fù)的記錄加行鎖保證只有一個(gè)節(jié)點(diǎn)能讀到。5.2 時(shí)間輪精度漂移如何校準(zhǔn)時(shí)間輪在多線程環(huán)境下如果advance()的調(diào)用間隔不穩(wěn)定會(huì)出現(xiàn)時(shí)間漂移。我一開(kāi)始是用ScheduledExecutorService.scheduleAtFixedRate驅(qū)動(dòng)但發(fā)現(xiàn)它在 GC 停頓的時(shí)候會(huì)跳過(guò)一次調(diào)度。后來(lái)改成了scheduleWithFixedDelay雖然效率略低但保證每次執(zhí)行完成后才等下一個(gè)周期不會(huì)出現(xiàn)累積誤差。另一個(gè)校準(zhǔn)思路是每次advance()時(shí)不依賴 tickDuration 的累計(jì)而是直接用當(dāng)前系統(tǒng)時(shí)間計(jì)算應(yīng)該走到哪個(gè)槽位。如果發(fā)現(xiàn)實(shí)際時(shí)間比預(yù)期晚了好幾個(gè)刻度就一次性把中間所有的槽位都掃一遍。這個(gè)方案的容錯(cuò)性更強(qiáng)但實(shí)現(xiàn)復(fù)雜度更高面試中能主動(dòng)提出這一點(diǎn)會(huì)很加分。5.3 線程池耗盡排查線程棧的實(shí)戰(zhàn)方法任務(wù)量上來(lái)之后最常見(jiàn)的問(wèn)題就是線程池被打滿。有一次排查中我發(fā)現(xiàn)執(zhí)行線程池的隊(duì)列長(zhǎng)度一直在漲但 CPU 使用率并不高。當(dāng)時(shí)用jstack抓了一下線程棧發(fā)現(xiàn)大量線程阻塞在 HTTP 調(diào)用上——外部告警接口的 DNS 解析超時(shí)了。這屬于下游依賴出問(wèn)題導(dǎo)致的線程堆積而不是系統(tǒng)本身代碼 bug。這個(gè)案例的教訓(xùn)是所有對(duì)下游的調(diào)用必須設(shè)置超時(shí)時(shí)間而且超時(shí)時(shí)間要有上限。很多 HTTP 客戶端默認(rèn)是不超時(shí)的等于把系統(tǒng)的命脈交到了別人手里。我后來(lái)的做法是外部調(diào)用統(tǒng)一走一個(gè)包裝類強(qiáng)制設(shè)置連接超時(shí)、讀取超時(shí)和重試次數(shù)并且把超時(shí)時(shí)間做成配置文件可以動(dòng)態(tài)調(diào)整的參數(shù)。5.4 數(shù)據(jù)庫(kù)連接池耗盡隱藏的坑另一個(gè)和線程池類似的坑是數(shù)據(jù)庫(kù)連接池溢出。任務(wù)執(zhí)行時(shí)需要更新task_meta表的狀態(tài)如果更新邏輯里的事務(wù)沒(méi)控制好長(zhǎng)時(shí)間占用連接不釋放連接池就會(huì)被打滿。我的排查方法是查看數(shù)據(jù)庫(kù)的SHOW PROCESSLIST如果有大量的Sleep線程就是連接泄漏了。代碼層面檢查下來(lái)發(fā)現(xiàn)是一個(gè)try-catch里忘記在finally中關(guān)閉連接導(dǎo)致的JDK7 之后用 try-with-resources 能從根本上避免這個(gè)問(wèn)題。6. 面試復(fù)盤與進(jìn)階建議這道系統(tǒng)開(kāi)發(fā)一雖然看起來(lái)是一個(gè)設(shè)計(jì)題但實(shí)際上考察的是候選人完整的工程能力鏈條。從需求拆解到技術(shù)選型從代碼實(shí)現(xiàn)到高可用設(shè)計(jì)再到問(wèn)題排查每一條線都是可以深挖的。我在面試后的復(fù)盤中發(fā)現(xiàn)真正讓我通過(guò)這輪面試的不是時(shí)間輪算法本身而是我在講方案的時(shí)候主動(dòng)暴露了問(wèn)題第一次主動(dòng)暴露是討論單機(jī)方案時(shí)我自己提出單機(jī)進(jìn)程崩潰任務(wù)會(huì)丟然后引導(dǎo)到持久化和恢復(fù)機(jī)制第二次是討論執(zhí)行可靠性時(shí)提出分布式環(huán)境下要防重復(fù)調(diào)度然后引出哈希分片和分布式鎖。這種自己挖坑自己填的節(jié)奏會(huì)讓面試官看到你的系統(tǒng)思維方式而不是被動(dòng)等提問(wèn)。給準(zhǔn)備類似面試的朋友一個(gè)建議不要只刷題多動(dòng)手實(shí)現(xiàn)一些基礎(chǔ)組件比如消息隊(duì)列的簡(jiǎn)化版、分布式鎖的封裝、限流器、任務(wù)調(diào)度器。實(shí)現(xiàn)的深度不要求達(dá)到生產(chǎn)級(jí)別但關(guān)鍵機(jī)制、并發(fā)控制、邊界條件一定要自己想明白。面試官考察的不是你會(huì)用某個(gè)框架而是你有沒(méi)有能力從零構(gòu)建一個(gè)可靠的系統(tǒng)而這個(gè)能力只能靠多動(dòng)手踩坑練出來(lái)。如果你時(shí)間有限優(yōu)先把三件事做到位第一掌握一個(gè)定時(shí)任務(wù)調(diào)度算法的實(shí)現(xiàn)時(shí)間輪或優(yōu)先隊(duì)列第二搞明白分布式環(huán)境下冪等性的各種實(shí)現(xiàn)方式第三能有條理地講清楚一個(gè)系統(tǒng)的故障排查過(guò)程。這三點(diǎn)到了面試現(xiàn)場(chǎng)任何一支都能成為你的加分項(xiàng)。