偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

MySQL 事務(wù)兩階段提交原理簡(jiǎn)析

數(shù)據(jù)庫(kù) MySQL
MySQL 中的日志非常重要,包括實(shí)例內(nèi)的事務(wù)以及實(shí)例間的主從復(fù)制均基于日志實(shí)現(xiàn)。

引言

MySQL 中的日志非常重要,包括實(shí)例內(nèi)的事務(wù)以及實(shí)例間的主從復(fù)制均基于日志實(shí)現(xiàn)。

計(jì)劃通過(guò)多篇文章分析多種日志,從而串聯(lián)日志、事務(wù)、復(fù)制三個(gè)模塊之間的關(guān)系,本文是第一篇文章,介紹兩階段提交。

其中首先介紹為什么需要兩階段提交,然后簡(jiǎn)單分析兩階段提交的實(shí)現(xiàn),期間介紹相關(guān)知識(shí)點(diǎn),包括分布式事務(wù)與崩潰恢復(fù)。

概念

兩份日志

MySQL 中最重要的兩份日志是 redo log 與 binlog。

為什么會(huì)有兩份日志,原因是使用場(chǎng)景不同。

其中:

  • redo log 用于實(shí)現(xiàn)事務(wù)的持久性,具體是通過(guò) crash-safe 能力;
  • binlog 用于實(shí)現(xiàn)主從復(fù)制與數(shù)據(jù)恢復(fù)。

兩份日志主要有以下三點(diǎn)不同;

  • redo log 是 InnoDB 存儲(chǔ)引擎層實(shí)現(xiàn)的特有的日志,binlog 是 Server 層實(shí)現(xiàn)的通用的日志;
  • redo log 是物理日志,binlog 是邏輯日志;
  • redo log 是循環(huán)寫入,binlog 是追加寫入。

兩階段提交

為了保證兩份日志之間的邏輯一致,也就是數(shù)據(jù)與備份的一致性,引入兩階段提交(two-phase commit protocol,2PC)。

為什么需要兩階段提交,那么如果沒(méi)有兩階段提交,會(huì)發(fā)生什么呢?

由于 redo log 和 binlog 是兩個(gè)獨(dú)立的邏輯,如果不用兩階段提交,要么就是先寫完 redo log 再寫 binlog,或者采用反過(guò)來(lái)的順序。

假設(shè)執(zhí)行 update,將值從 1 改為 2。

假設(shè):

  • 先寫 redo log 后寫 binlog,如果 redo log 寫完后 MySQL 進(jìn)程異常重啟,redo log 崩潰恢復(fù)后值為 2,但是基于 binlog 備份恢復(fù)值為 1,并導(dǎo)致備份恢復(fù)少了一個(gè)事務(wù);
  • 先寫 binlog 后寫 redo log,如果 binlog 寫完后 MySQL 進(jìn)程異常重啟,基于 binlog 備份恢復(fù)值為 2,但是 redo log 還沒(méi)寫因此崩潰恢復(fù)后事務(wù)無(wú)效,值為 1,并導(dǎo)致備份恢復(fù)多了一個(gè)事務(wù)。

顯然,如果沒(méi)有兩階段提交,無(wú)法保證數(shù)據(jù)與日志的一致性。

那么,有兩階段提交時(shí)會(huì)怎么樣呢?

首先,介紹下兩階段提交的過(guò)程,其中將 redo log 的提交拆分為兩個(gè)步驟,包括 prepare 與 commit,期間寫入 binlog。

因此,如果在兩階段提交的不同時(shí)刻,MySQL 異常重啟會(huì)發(fā)生什么呢?

  • 如果在時(shí)刻 A 重啟,也就是 redo log prepare 之后,寫入 binlog 之前,崩潰恢復(fù)時(shí)發(fā)現(xiàn) redo log 沒(méi)有 commit,因此回滾。binlog 還沒(méi)寫,因此不會(huì)傳到備庫(kù),數(shù)據(jù)與日志保持一致;
  • 如果在時(shí)刻 B 重啟,也就是寫入 binlog 之后,redo log commit 之前,崩潰恢復(fù)時(shí)發(fā)現(xiàn) redo log 雖然沒(méi)有 commit,但是 redo log 有完整的 prepare,且對(duì)應(yīng)的事務(wù) binlog 完整,因此提交事務(wù)。binlog 寫入,因此會(huì)傳到備庫(kù),數(shù)據(jù)與日志保持一致。

崩潰恢復(fù)

從前一節(jié)的描述中可以發(fā)現(xiàn)崩潰恢復(fù)時(shí)根據(jù)兩階段提交的進(jìn)度進(jìn)行處理。

參考 MySQL 45 講,崩潰恢復(fù)(crash-recovery)時(shí)的完整判斷邏輯為:

  • 如果 redo log 里面的事務(wù)完整,也就是已經(jīng)有了 commit 標(biāo)識(shí),直接提交;
  • 如果 redo log 里面的事務(wù)只有完整的 prepare,進(jìn)一步判斷對(duì)應(yīng)的事務(wù) binlog 是否存在且完整:
  • 如果是,提交事務(wù);
  • 否則,回滾事務(wù)。

因此,redo log prepare 后 commit 前崩潰恢復(fù)時(shí)可能發(fā)生回滾或提交,具體與 binlog 的完整性有關(guān)。

顯然,時(shí)刻 B 發(fā)生 crash 的情況對(duì)應(yīng) redo log prepare 完整,且 binlog 完整的場(chǎng)景,因此事務(wù)提交。

這里可以提出以下兩個(gè)問(wèn)題:

1)如何判斷 binlog 完整

2)如何根據(jù) redo log 定位對(duì)應(yīng)的 binlog

接下來(lái)分別回答這兩個(gè)問(wèn)題。

1)如何判斷 binlog 完整

判斷 binlog 的完整性有以下兩種方式:

  • 在事務(wù)提交時(shí)記錄 XID event 到 binlog 中以標(biāo)記事務(wù)的結(jié)束。這個(gè)機(jī)制確保了事務(wù)的完整性和一致性,無(wú)論使用哪種復(fù)制格式;
  • 在 MySQL 5.6.2 版本以后,還引入了 binlog-checksum 參數(shù),用于驗(yàn)證 binlog 內(nèi)容的正確性。通過(guò)為 binlog 中的每個(gè)事件添加校驗(yàn)和(checksum),MySQL 能夠檢測(cè)到寫入 binlog 時(shí)由于磁盤錯(cuò)誤等原因?qū)е碌臄?shù)據(jù)損壞。

如下所示,測(cè)試顯示 row 與 statement 兩種 bnlog 格式中事務(wù)的最后一個(gè) event 都是 XID event。

2)如何根據(jù) redo log 定位對(duì)應(yīng)的 binlog

redo log 與 binlog 有一個(gè)共同的數(shù)據(jù)字段,稱為 XID。

崩潰恢復(fù)的時(shí)候,會(huì)按順序掃描 redo log:

  • 如果碰到既有 prepare、又有 commit 的 redo log,就直接提交;
  • 如果碰到只有 parepare、而沒(méi)有 commit 的 redo log,就拿著 XID 去 binlog 找對(duì)應(yīng)的事務(wù)。

其中:

  • redo log 掃描的起點(diǎn)是 InnoDB 最后一次 checkpoint 操作的 lsn(last_checkpoint_lsn)。
  • XID 與分布式事務(wù)有關(guān),下一節(jié)中介紹。

這里可以提出另一個(gè)問(wèn)題,根據(jù)事務(wù)的持久性,到什么進(jìn)度后事務(wù)將無(wú)法回滾?

理論上 MySQL 中通過(guò) redo log 實(shí)現(xiàn)事務(wù)的持久性,因此 redo log 刷盤后就可以保證對(duì)數(shù)據(jù)庫(kù)的修改是永久性的,即使發(fā)生崩潰也不會(huì)丟失,當(dāng)然也不會(huì)回滾。

不過(guò)根據(jù)事務(wù)的兩階段提交協(xié)議,binlog 寫入代表事務(wù)提交,同樣不可能發(fā)生回滾。

因此,事務(wù)無(wú)法回滾的關(guān)鍵點(diǎn)是事務(wù)的提交,而不是單純的 redo log 或 binlog 的寫入。在事務(wù)提交的過(guò)程中,兩階段提交機(jī)制確保了 redo log 和 binlog 的一致性,這個(gè)提交過(guò)程標(biāo)志著事務(wù)從可回滾轉(zhuǎn)變?yōu)椴豢苫貪L。

XA 事務(wù)

分布式事務(wù)是一種跨多個(gè)獨(dú)立的數(shù)據(jù)庫(kù)、系統(tǒng)或網(wǎng)絡(luò)區(qū)域的事務(wù)處理方法。

XA 事務(wù)是一種遵循 XA 規(guī)范的分布式事務(wù),因此 XA 事務(wù)是分布式事務(wù)的一種實(shí)現(xiàn)。

XA 事務(wù)依賴兩階段提交(2PC)協(xié)議實(shí)現(xiàn)分布式事務(wù)的一致性和原子性。

兩階段提交是最常見(jiàn)的分布式事務(wù)協(xié)議,用于保證分布式事務(wù)的原子性,顯然并不是 MySQL 獨(dú)有的。

根據(jù) XA 規(guī)范,兩階段提交的實(shí)現(xiàn)過(guò)程中包括兩個(gè)角色:

  • 資源管理器(Resource Manager),可以稱為執(zhí)行器,用于管理分布式數(shù)據(jù)庫(kù)的一個(gè)本地事務(wù);
  • 事務(wù)管理器(Transaction Manager),可以稱為協(xié)調(diào)器,用于協(xié)調(diào)事務(wù)的提交、回滾、崩潰恢復(fù)。

兩階段提交中將提交操作分為兩個(gè)階段:

  • prepare 階段,協(xié)調(diào)器詢問(wèn)所有執(zhí)行器,是否可以提交事務(wù),如果任何一個(gè)執(zhí)行器的本地事務(wù)無(wú)法提交時(shí),分布式事務(wù)都需要通知所有執(zhí)行器進(jìn)行回滾操作;
  • commit 階段,協(xié)調(diào)器在收到每一個(gè)執(zhí)行器的提交確認(rèn)后,通知執(zhí)行器各自提交自己的本地事務(wù)。

MySQL 中的 XA 事務(wù)分為外部 XA 與內(nèi)部 XA。其中:

  • 外部 XA,MySQL 服務(wù)器作為執(zhí)行器,連接服務(wù)器的客戶端程序作為協(xié)調(diào)器,對(duì)應(yīng)多個(gè)支持分布式事務(wù)的數(shù)據(jù)庫(kù)實(shí)例,比如多套 MySQL(使用分庫(kù)分表中間件)、Oracle + MySQL;
  • 內(nèi)部 XA,對(duì)應(yīng)單個(gè) MySQL 實(shí)例,分為以下兩種場(chǎng)景:
  • 沒(méi)有開(kāi)啟 binlog,SQL 語(yǔ)句涉及一個(gè)或多個(gè)支持事務(wù)的存儲(chǔ)引擎;

開(kāi)啟 binlog,SQL 語(yǔ)句涉及一個(gè)或多個(gè)支持事務(wù)的存儲(chǔ)引擎。

其中,由于 binlog 與存儲(chǔ)引擎是獨(dú)立單元,可以將 binlog 也看作一個(gè)存儲(chǔ)引擎,因此需要通過(guò) XA 事務(wù)實(shí)現(xiàn) binlog 與存儲(chǔ)引擎的數(shù)據(jù)一致性和原子性,從而保證全部操作要么全部提交,要么全部回滾。

在分布式事務(wù)中,XID作為全局事務(wù)的唯一標(biāo)識(shí)符,用于跟蹤和協(xié)調(diào)不同數(shù)據(jù)庫(kù)實(shí)例中的事務(wù)部分。這個(gè)標(biāo)識(shí)符在事務(wù)的所有參與者之間是共享的,以確保事務(wù)的一致性和完整性。

因此在 XA 事務(wù)中,XID用于在多個(gè)數(shù)據(jù)庫(kù)實(shí)例之間協(xié)調(diào)事務(wù)。

在 MySQL 中,XID(Transaction Identifier)是事務(wù)的唯一標(biāo)識(shí)符,用于標(biāo)記事務(wù)的提交。

binlog 中一個(gè)事務(wù)由一系列事件(event)組成,這個(gè)序列由 BEGIN 事件開(kāi)始,以 XID 事件結(jié)束(對(duì)于提交的事務(wù))。

因此如果事務(wù)被回滾,不會(huì)記錄 XID 事件,而是記錄一個(gè) ROLLBACK 事件。

參考 chatgpt,XID 與 GTID 的主要區(qū)別包括:

  • XID:是事務(wù)的標(biāo)識(shí)符,用于標(biāo)記事務(wù)的結(jié)束,主要用于事務(wù)的恢復(fù)和復(fù)制過(guò)程中確定事務(wù)邊界。對(duì)于分布式事務(wù),所有 MySQL 實(shí)例使用相同的 XID 來(lái)提交事務(wù);
  • GTID(全局事務(wù)標(biāo)識(shí)符):是 MySQL 5.6 及更高版本中引入的,用于唯一標(biāo)識(shí)每個(gè)事務(wù)。每個(gè) GTID 都是全局唯一的,即使在不同的 MySQL 實(shí)例中也是如此。GTID 使得跟蹤和復(fù)制事務(wù)變得更加簡(jiǎn)單和可靠。

實(shí)現(xiàn)

prepare

參考文章 MySQL 事務(wù)二階段提交 與 MySQL 核心模塊揭秘 | 07 期 | 二階段提交 (1) prepare 階段,prepare 階段做的事情分為兩類:

  • binlog prepare,對(duì)應(yīng) binlog_prepare 函數(shù),什么都不做;
  • InnoDB prepare,對(duì)應(yīng) innobase_xa_prepare 函數(shù),具體做五件事情:
  • 把分配給事務(wù)的所有 Undo segment 的狀態(tài) TRX_UNDO_STATE 從 TRX_UNDO_ACTIVE 修改為 TRX_UNDO_PREPARED;
  • 把事務(wù) XID 寫入所有 Undo segment 中當(dāng)前提交事務(wù)的 Undo Log Segment Header;
  • 把內(nèi)存中的事務(wù)對(duì)象狀態(tài)從 TRX_STATE_ACTIVE 修改為 TRX_STATE_PREPARED,標(biāo)識(shí)事務(wù)已經(jīng)進(jìn)入二階段提交的 prepare 階段;
  • 如果當(dāng)前提交事務(wù)的隔離級(jí)別是讀未提交(READ-UNCOMMITTED)或讀已提交(READ-COMMITTED),InnoDB 會(huì)釋放事務(wù)給記錄加的共享、排他 GAP 鎖;
  • 調(diào)用 trx_flush_logs(),處理 redo log 刷盤的相關(guān)邏輯,其中實(shí)際上并不會(huì)將 redo log 刷盤,也就是同樣什么都不做。

其中 undo log 非常重要,原因是:

  • TRX_UNDO_STATE 用于崩潰恢復(fù)過(guò)程中,標(biāo)記哪些事務(wù)需要恢復(fù),哪些事務(wù)不用恢復(fù)。
  • XID 用于崩潰恢復(fù)過(guò)程中,決定數(shù)據(jù)庫(kù)崩潰時(shí)處于 prepared 階段的事務(wù),是要回滾還是要提交。

參考文章 XA事務(wù)與兩階段提交。

Undo頁(yè)面鏈表的第一個(gè)頁(yè)面的結(jié)構(gòu)見(jiàn)下圖,其中記錄了一些關(guān)于這個(gè)事務(wù)的一些屬性。

其中 Undo Log Segment Header 結(jié)構(gòu)見(jiàn)下圖,其中 TRX_UNDO_STATE 字段表示事務(wù)所處的狀態(tài)。

其中 Undo Log Header 結(jié)構(gòu)見(jiàn)下圖。

其中:

  • TRX_UNDO_XID_EXISTS:表示有沒(méi)有 XID 信息;
  • XID信息:表示具體的 XID 是什么。

TRX_UNDO_STATE 的取值包括:

  • TRX_UNDO_ACTIVE:活躍狀態(tài),也就是一個(gè)活躍的事務(wù)正在往這個(gè)段里邊寫入 undo log;
  • TRX_UNDO_CACHED:被緩存的狀態(tài)。處在該狀態(tài)的 Undo 頁(yè)面鏈表等待著之后被其他事務(wù)重用;
  • TRX_UNDO_TO_FREE:對(duì)于 insert undo 鏈表來(lái)說(shuō),如果在它對(duì)應(yīng)的事務(wù)提交之后,該鏈表不能被重用,那么就會(huì)處于這種狀態(tài)。Undo 頁(yè)面鏈表可以被馬上清理;
  • TRX_UNDO_TO_PURGE:對(duì)于 update undo 鏈表來(lái)說(shuō),如果在它對(duì)應(yīng)的事務(wù)提交之后,該鏈表不能被重用,那么就會(huì)處于這種狀態(tài)。Undo 頁(yè)面鏈表不可以被馬上清理,而是加入 History 鏈表用于 MVCC,等待 purge 線程清理;
  • TRX_UNDO_PREPARED:包含處于 prepare 階段(這個(gè)階段是在分布式事務(wù)中會(huì)出現(xiàn))的事務(wù)產(chǎn)生的 undo log。

commit

commit 階段做的事情同樣分為兩類:

  • binlog 刷盤,對(duì)應(yīng) flush 函數(shù),將事務(wù)執(zhí)行過(guò)程中產(chǎn)生的 binlog 寫入硬盤;
  • InnoDB commit,對(duì)應(yīng) innobase_commit 函數(shù),完成存儲(chǔ)引擎層面的事務(wù)提交。

具體 commit 階段的實(shí)現(xiàn)與組提交有關(guān),計(jì)劃下一篇文章中介紹。

因此,在客戶端執(zhí)行 commit 語(yǔ)句或自動(dòng) commit 時(shí),MySQL 開(kāi)啟內(nèi)部 XA 事務(wù),分兩階段完成 XA 事務(wù)的提交。

崩潰恢復(fù)

崩潰恢復(fù)全過(guò)程分為多個(gè)階段,其中與事務(wù)兩階段提交有關(guān)的階段包括:

  • 恢復(fù)數(shù)據(jù)頁(yè),通過(guò) doublewrite buffer 修復(fù)部分頁(yè)寫入(partial page write)導(dǎo)致的數(shù)據(jù)頁(yè)損壞;
  • 讀取 redo log,從 last_checkpoint_lsn 開(kāi)始讀取 redo log;
  • 應(yīng)用 redo log 到數(shù)據(jù)頁(yè),將沒(méi)有寫入數(shù)據(jù)頁(yè)的日志重做一遍,從而保證事務(wù)的持久性;
  • 初始化事務(wù)子系統(tǒng),從 undo 表空間文件讀取未完成的事務(wù);
  • 處理未完成事務(wù),其中:
  • 如果事務(wù) XID 對(duì)應(yīng) binlog 已寫入文件,事務(wù)提交;
  • 如果事務(wù) XID 對(duì)應(yīng) binlog 未寫入文件,事務(wù)回滾。
  • 清理已提交事務(wù),對(duì)應(yīng) TRX_STATE_COMMITTED_IN_MEMORY,包括 DDL 與 DML;
  • 回滾未提交事務(wù),對(duì)應(yīng) TRX_STATE_ACTIVE,包括 DDL 與 DML;
  • 處理 prepare 事務(wù),對(duì)應(yīng) TRX_STATE_PREPARED,其中:

未完成事務(wù)的狀態(tài)可能是以下三種之一:

  • TRX_STATE_ACTIVE,表示事務(wù)還沒(méi)有進(jìn)入提交階段。
  • TRX_STATE_PREPARED,表示事務(wù)已經(jīng)提交了,但是只完成了二階段提交的 PREPARE 階段,還沒(méi)有完成 COMMIT 階段。
  • TRX_STATE_COMMITTED_IN_MEMORY,表示事務(wù)已經(jīng)完成了二階段提交的 2 個(gè)階段,還剩一些收尾工作沒(méi)做,這種狀態(tài)的事務(wù)修改的數(shù)據(jù)已經(jīng)可以被其它事務(wù)看見(jiàn)了。

其中未提交事務(wù) TRX_STATE_ACTIVE 對(duì)應(yīng) redo log 已經(jīng)刷盤的未提交事務(wù),包括以下三種場(chǎng)景:

  • 后臺(tái)線程定時(shí)將 redo log buffer 中的日志刷盤時(shí)將事務(wù)執(zhí)行中間過(guò)程的 redo log 持久化到磁盤;
  • redo log buffer 占用的空間即將達(dá)到 innodb_log_buffer_size 一半時(shí),后臺(tái)線程會(huì)主動(dòng)寫盤,即使事務(wù)并沒(méi)有提交;
  • 并行的事務(wù)提交時(shí),順帶將這個(gè)事務(wù)的 redo log buffer 持久化到磁盤。假設(shè)一個(gè)事務(wù) A 執(zhí)行到一半,已經(jīng)寫了一些 redo log 到 buffer 中,這時(shí)候有另外一個(gè)線程的事務(wù) B 提交,如果 innodb_flush_log_at_trx_commit 設(shè)置的是 1,那么按照這個(gè)參數(shù)的邏輯,事務(wù) B 要把 redo log buffer 里的日志全部持久化到磁盤。這時(shí)候,就會(huì)帶上事務(wù) A 在 redo log buffer 里的日志一起持久化到磁盤。

因此,為了保證事務(wù)的原子性,需要在崩潰恢復(fù)時(shí)將這些未提交事務(wù)回滾,而找到這些未提交事務(wù)依賴 undo log。

結(jié)論

MySQL 通過(guò)事務(wù)的兩階段提交實(shí)現(xiàn)數(shù)據(jù)與日志的一致性。

其中數(shù)據(jù)指 redo log,日志指 binlog,可以認(rèn)為是兩個(gè)不同的存儲(chǔ)引擎,因此基于分布式事務(wù)的 XID 協(xié)議實(shí)現(xiàn)一致性。

具體實(shí)現(xiàn)中將 redo log 的提交拆分為兩個(gè)步驟,包括 prepare 與 commit,期間寫入 binlog。

因此,寫入的不同階段異常重啟時(shí):

  • redo log commit crash,binlog 完整,因此事務(wù)提交;
  • binlog crash,redo log 沒(méi)有 commit,且沒(méi)有寫入 binlog,因此事務(wù)回滾。

具體是在崩潰恢復(fù)過(guò)程中基于兩階段提交保證事務(wù)的一致性。

其中:

  • redo log application 階段用于將沒(méi)有寫入數(shù)據(jù)頁(yè)的日志重做一遍,把系統(tǒng)恢復(fù)到崩潰前的狀態(tài),其中都是提交,沒(méi)有回滾;
  • 初始化事務(wù)子系統(tǒng)階段從表空間中找到各個(gè) Undo 頁(yè)面鏈表的首個(gè)頁(yè)面的頁(yè)號(hào),然后根據(jù)事務(wù)的狀態(tài)處理未完成事務(wù)。其中:
  • TRX_STATE_ACTIVE,表明是未提交事務(wù),因此回滾事務(wù);
  • TRX_STATE_PREPARED,進(jìn)一步判斷 XID 對(duì)應(yīng) binlog 是否存在,如果有,提交事務(wù),否則回滾事務(wù);
  • TRX_STATE_COMMITTED_IN_MEMORY,表明是已提交事務(wù),因此提交事務(wù),具體是清理已提交事務(wù)。

因此,可以將崩潰恢復(fù)過(guò)程中使用的日志的順序理解為 redo log、undo log、binlog。

責(zé)任編輯:華軒 來(lái)源: 丹柿小院
相關(guān)推薦

2022-12-21 19:04:35

InnoDBMySQL

2023-07-26 09:24:03

分布式事務(wù)分布式系統(tǒng)

2022-03-28 10:44:51

MySQL日志存儲(chǔ)

2025-06-19 08:03:03

2025-06-10 08:02:15

2025-05-16 07:46:11

分布式事務(wù)服務(wù)

2024-01-26 08:18:03

2024-12-06 07:10:00

2017-08-30 18:15:54

MySql

2018-10-29 08:44:29

分布式兩階段提交事務(wù)

2023-11-29 07:47:58

DDIA兩階段提交

2023-12-05 09:33:08

分布式事務(wù)

2023-01-18 10:35:49

MySQL數(shù)據(jù)庫(kù)

2025-04-07 03:00:00

MySQLDDLonline

2022-07-27 08:52:10

MySQL二階段提交

2020-02-03 12:12:28

MySQL數(shù)據(jù)庫(kù)SQL

2010-04-20 20:46:01

負(fù)載均衡

2024-07-22 08:57:58

2024-03-26 16:24:46

分布式事務(wù)2PC3PC

2015-05-13 10:36:43

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)