好久沒有分享最新的面經了,今天分享一下北京某公司Go開發崗的面經,薪資是15K左右,看看難度如何:
為什么要用分布式事務
分布式事務的核心作用是解決跨服務、跨數據源操作的數據一致性問題。在單體應用中,數據庫本地事務(如 MySQL 的 InnoDB 事務)可通過 ACID 特性保證單庫內操作的一致性,但在分布式系統中,業務操作往往需要跨多個服務(如用戶服務、訂單服務、支付服務)或多個數據庫,此時本地事務無法覆蓋所有操作,可能出現 “部分成功、部分失敗” 的情況。
什么是事務
事務是一組操作的集合,需滿足 “要么全部成功,要么全部失敗” 的原子性要求,分為數據庫事務和分布式事務兩類:
1. 數據庫事務
指單庫內的事務(如 MySQL、PostgreSQL),核心是滿足ACID 特性:
- 原子性(Atomicity):事務中的所有操作要么全執行,要么全不執行(如轉賬時 “扣 A 的錢” 和 “加 B 的錢” 必須同時成功或同時失敗)。
- 一致性(Consistency):事務執行前后,數據從一個合法狀態切換到另一個合法狀態(如轉賬前后 A 和 B 的總金額不變)。
- 隔離性(Isolation):多個事務并發執行時,彼此的操作互不干擾(避免臟讀、不可重復讀、幻讀)。
- 持久性(Durability):事務提交后,數據修改會永久保存(即使數據庫崩潰,重啟后數據仍有效)。
以 MySQL InnoDB 引擎為例,其事務實現細節包括:
- 隔離級別:支持讀未提交(Read Uncommitted)、讀已提交(Read Committed)、可重復讀(Repeatable Read,默認)、串行化(Serializable)。
- 快照讀與當前讀:
- 快照讀(如
select * from t where id=1
):通過 MVCC(多版本并發控制)和 Read View 實現。每行數據包含隱藏列(DB_TRX_ID
:最后修改事務 ID;DB_ROLL_PTR
:回滾指針,指向歷史版本),Read View(由 “低水位”“高水位”“活躍事務 ID 列表” 組成)決定可見的版本(讀提交時每次查詢生成新 Read View,可重復讀時首次查詢生成 Read View)。 - 當前讀(如
update/delete/select ... for update
):會加鎖(行鎖、表鎖),并讀取最新數據。
- 快照讀(如
- 鎖機制:更新操作時會加 “意向鎖”(IX/IS),用于快速判斷表中是否有行鎖(避免逐行檢查鎖,提高效率);行鎖(如 Record Lock)用于鎖定單行數據,防止并發修改沖突。
2. 分布式事務
指跨多個服務或多個數據庫的事務,核心是保證跨節點操作的最終一致性(無法像本地事務一樣嚴格保證 ACID,而是通過補償機制實現 “最終一致”)。
常見實現方式包括 TCC(Try-Confirm-Cancel)、SAGA(正向 + 反向補償)、本地消息表、DTM 框架等。以 DTM 為例,其核心組件包括:
- 事務管理器(TM):負責全局事務的創建、提交、回滾,協調各子事務的執行順序,解決事務亂序、冪等性(重復提交)、空補償(未執行 Try 卻執行 Cancel)等問題(通過 “子事務屏障表” 記錄全局事務 ID、分支事務 ID 及狀態實現)。
- 資源管理器(RM):管理各服務的本地資源(如數據庫連接),負責執行子事務的 Try/Confirm/Cancel 操作,并向 TM 匯報執行結果。
分布式事務 CAP 特性
CAP 是分布式系統的三大核心特性,由 Eric Brewer 提出,三者不可同時滿足:
- 一致性(Consistency):分布式系統中,所有節點在同一時間看到的數據是一致的(如集群中所有節點的用戶余額必須相同)。
- 可用性(Availability):分布式系統中,任何節點故障時,剩余節點仍能正常響應請求(如支付系統不能因某臺服務器宕機而無法下單)。
- 分區容錯性(Partition Tolerance):當網絡分區(部分節點與其他節點斷開通信)發生時,系統仍能繼續運行(分布式系統必須滿足此特性,因為網絡故障不可避免)。
在分布式事務中,由于必須滿足分區容錯性(P),因此需在一致性(C)和可用性(A)之間權衡:
- CP 傾向:優先保證一致性,犧牲部分可用性(如 ZooKeeper 集群,leader 宕機后會暫停服務直到新 leader 選舉完成,期間不可用但數據一致)。
- AP 傾向:優先保證可用性,接受短暫的不一致(如多數業務的支付系統,允許 “用戶余額扣減后,訂單狀態延遲更新”,但最終會通過補償機制一致)。
實際業務中,分布式事務通常追求最終一致性(屬于 AP 傾向),即允許中間過程數據不一致,但通過補償操作(如 TCC 的 Cancel、SAGA 的反向流程),最終所有節點數據會達成一致。
協程了解嗎
協程(Goroutine,Go 語言中的實現)是輕量級的 “用戶態線程”,相比操作系統線程(OS Thread),其優勢在于:
- 資源占用極低:初始棧大小僅 2KB(可動態擴容至 GB 級),而線程初始棧通常為 2MB,因此單臺機器可創建數十萬甚至數百萬協程。
- 調度效率高:由 Go runtime 而非操作系統調度,減少用戶態與內核態的切換開銷。
Go 語言通過GMP 調度模型實現協程的高效調度,核心組件包括:
- G(Goroutine):協程本身,包含棧、程序計數器、狀態(如運行中、就緒、阻塞)等信息。
- M(Machine):操作系統線程,負責執行 G(真正的 “執行載體”)。
- P(Processor):處理器,是 G 和 M 的 “橋梁”,包含本地協程隊列(Local Queue)、全局隊列指針、調度器狀態等,用于管理可執行的 G。
調度流程細節:
- 初始化:程序啟動時創建初始線程 M0 和初始協程 G0(每個 M 綁定一個 G0,負責調度其他 G)。
- 協程入隊:新建的 G 優先放入當前 P 的本地隊列(容量默認 256),本地隊列滿后放入全局隊列。
- 調度策略:
- 本地隊列調度:M 優先從綁定的 P 的本地隊列獲取 G 執行。
- 全局隊列調度:本地隊列為空時,M 會從全局隊列批量獲取 G(一次取
min(全局隊列長度/num(P) + 1, 64)
個)。 - 偷取機制(Work Stealing):若本地隊列和全局隊列都為空,M 會隨機選擇其他 P 的本地隊列,偷取一半的 G 執行(避免線程空閑,提高資源利用率)。
- Hand off 機制:當 G 因系統調用(如 IO、sleep)阻塞時,M 會主動釋放綁定的 P(將 P 放入空閑 P 列表),讓其他空閑的 M 綁定該 P 并執行其本地隊列的 G;當阻塞的 G 喚醒后,M 會嘗試從空閑 P 列表獲取 P,若獲取成功則繼續執行 G,否則將 G 放入全局隊列等待調度。
介紹一下 hand off 機制
Hand off 機制是 Go 調度器中用于處理 “協程阻塞” 的協作式調度策略,核心目的是避免處理器(P)閑置,提高系統資源利用率。
觸發場景
當協程(G)因以下操作阻塞時,會觸發 hand off:
- 系統調用(如
net.Dial
、os.Read
等 IO 操作); - 同步操作(如
time.Sleep
、mutex.Lock
未獲取鎖時)。
具體流程
-
G 阻塞時:執行 G 的 M 會檢測到 G 進入阻塞狀態,此時 M 會調用
park
函數將 G 的狀態標記為 “阻塞”,并將綁定的 P 從自身解綁,放入全局 “空閑 P 列表”。 -
釋放 P 后:M 進入休眠狀態(或處理其他任務),而空閑 P 列表中的 P 可被其他空閑的 M 綁定,繼續執行 P 本地隊列中的 G(避免 P 因 M 阻塞而閑置)。
-
G 喚醒時:阻塞的 G 被喚醒(如 IO 操作完成、鎖獲取成功),此時 M 會調用
unpark
函數,嘗試從空閑 P 列表中獲取一個 P:
- 若獲取成功,M 綁定該 P,繼續執行喚醒的 G;
- 若未獲取成功,將 G 放入全局隊列,M 進入休眠,等待后續被調度。
與搶占式調度的區別
Hand off 是 “協作式調度”(G 主動讓出 P),而 Go 1.14 后引入的 “搶占式調度” 是通過信號中斷長時間運行的 G(如循環無阻塞的 G),強制其讓出 P,避免單個 G 獨占 P 導致其他 G 餓死。兩者結合,保證了 Go 協程調度的高效性。
gc 了解嗎
Go 的垃圾回收(GC)是自動管理堆內存的機制,核心目標是 “在保證程序正確的前提下,盡可能減少對業務代碼的干擾(低延遲)”。其核心實現是三色標記法 + 混合寫屏障,特點是 “并發標記、并行清掃”(大部分階段與業務代碼并發執行,僅短暫 STW)。
核心流程
-
初始標記(STW):短暫暫停所有協程(幾微秒到毫秒級),標記 “根對象”(全局變量、棧上變量直接引用的對象)為灰色,開啟寫屏障(防止標記期間對象引用關系被修改導致漏標)。
-
并發標記:恢復協程執行,GC 后臺線程從灰色對象出發,遍歷其引用的對象:
- 若引用的對象是白色,標記為灰色;
- 遍歷完的灰色對象標記為黑色(表示 “已處理”)。
此階段與業務代碼并發執行,寫屏障會實時跟蹤對象引用變化(如黑色對象引用白色對象時,通過寫屏障修正標記)。
-
最終標記(STW):再次短暫暫停協程,處理并發標記期間遺漏的對象(如根對象的新增引用),關閉寫屏障。
-
并行清掃:恢復協程執行,多個 GC 線程并行清掃所有白色對象(不可達對象),釋放其占用的堆內存。
關鍵技術:混合寫屏障
寫屏障的作用是在并發標記階段,保證對象引用關系變化時,GC 能正確跟蹤可達性,避免 “黑色對象引用白色對象” 導致的漏標(漏標會導致白色對象被誤判為垃圾回收)。
混合寫屏障結合了 “插入寫屏障” 和 “刪除寫屏障” 的優勢,規則如下:
- 當創建新對象時,直接標記為黑色(避免新對象被誤回收);
- 當修改對象引用(如a.b = c)時:
- 若舊引用
a.b
指向的對象是黑色,將其重新標記為灰色(確保其引用的對象被遍歷); - 新引用
c
指向的對象若為白色,標記為黑色(避免被誤回收)。
- 若舊引用
性能優化
Go GC 通過不斷迭代優化延遲(如 1.5 引入并發標記、1.8 優化 STW 時間、1.19 引入分代回收),目前在多數場景下 STW 時間可控制在 100 微秒以內,對業務影響極小。
函數的棧幀了解嗎
函數棧幀是函數調用時在棧上分配的內存區域,用于存儲函數的參數、局部變量、返回地址、棧基指針等信息,其生命周期與函數調用一致(函數返回時棧幀被釋放)。
棧幀結構(以 x86 架構為例)
從高地址到低地址依次為:
- 返回地址:函數執行完畢后,CPU 需跳轉回的調用處地址(如
call func
指令會將下一條指令地址壓棧)。 - 棧基指針(BP):指向當前棧幀的底部,用于定位棧幀內的參數和局部變量(函數執行時,BP 的值由上一個棧幀的 SP 決定)。
- 局部變量:函數內定義的變量(如
int a
、string s
),按聲明順序從 BP 向下分配。 - 函數參數:調用方傳遞的參數,從 BP 向上(高地址)讀取(如
func(a, b int)
中,a
在 BP+8,b
在 BP+12,取決于 CPU 位數)。
逃逸分析與堆分配
Go 編譯器會通過 “逃逸分析” 判斷變量是否需要 “逃逸” 到堆上:
- 若變量僅在函數內使用(無外部引用),則分配在棧幀上(函數返回后自動釋放,無需 GC);
- 若變量被外部引用(如返回局部變量的指針、被閉包引用),則會 “逃逸” 到堆上(由 GC 管理生命周期)。
逃逸分析的目的是減少堆分配(堆分配需 GC,棧分配更高效),提升程序性能。
歡迎關注 ?
我們搞了一個免費的面試真題共享群,互通有無,一起刷題進步。
沒準能讓你能刷到自己意向公司的最新面試題呢。
感興趣的朋友們可以私信我,備注:面試群。