您現在的位置是:首頁 > 人文
新同事上來就用Kafka,瑟瑟發抖……
a hippo怎麼讀
「來源: |51CTO技術棧 ID:blog51cto」
關注
51CTO技術棧
,悅享技術,成就 CTO 夢想
“
事務是一個程式執行單元,裡面的所有操作要麼全部執行成功,要麼全部執行失敗。
圖片來自 Pexels
RocketMQ、Kafka 和 Pulsar 都是當今業界應用十分廣泛的開源訊息佇列(MQ)元件。
筆者在工作中遇到關於 MQ 選型相關的內容,瞭解到關於“事務訊息”這個概念在不同的 MQ 元件裡有不同內涵。
故藉此文,試著淺析一番這三種訊息佇列(MQ)的事務訊息有何異同,目的是形成關於訊息佇列事務訊息的全景檢視,給有類似業務需求的同學提供一些參考和借鑑。
訊息佇列演化
訊息佇列(Message Queue,簡稱 MQ),是指在訊息的傳輸中儲存訊息的容器或服務,是一種非同步的服務間通訊方式,適用於無伺服器和微服務架構,是分散式系統實現高效能、高可用、可伸縮等高階特效的重要元件。
常見的主流訊息佇列有 ActiveMQ、RabbitMQ、ZeroMQ、Kafka、MetaMQ、RocketMQ、Pulsar 等。
而在公司內有 TubeMQ、Ckafka、TDMQ、CMQ、CDMQ、Hippo 等。
①Kafka
Apache Kafka 是由 Apache 軟體基金會開發的一個開源訊息系統專案,由 Scala 寫成。
Kafka 最初是由 LinkedIn 開發,並於 2011 年初開源。2012 年 10 月從 Apache Incubator 畢業。該專案的目標是為處理實時資料提供一個統一、高通量、低等待的平臺。
Kafka 是一個分散式的、分割槽的、多複本的日誌提交服務。它透過一種獨一無二的設計提供了一個訊息系統的功能。
其整體架構圖如下所示:
②RocketMQ
Apache RocketMQ 是一個分散式訊息和流媒體平臺,具有低延遲、強一致、高效能和可靠性、萬億級容量和靈活的可擴充套件性。它有借鑑 Kafka 的設計思想,但不是 Kafka 的複製。
其整體架構圖如下所示:
③Pulsar
Apache Pulsar 是 Apache 軟體基金會頂級專案,是下一代雲原生分散式訊息流平臺。
它集訊息、儲存、輕量化函式式計算為一體,採用計算與儲存分離架構設計,支援多租戶、持久化儲存、多機房跨區域資料複製,具有強一致性、高吞吐、低延時及高可擴充套件性等流資料儲存特性,被看作是雲原生時代實時訊息流傳輸、儲存和計算最佳解決方案。
其整體架構圖如下所示:
背景知識
①什麼是事務?
事務:
是一個程式執行單元,裡面的所有操作要麼全部執行成功,要麼全部執行失敗。
一個事務有四個基本特性,也就是我們常說的(ACID):
Atomicity(原子性):
事務是一個不可分割的整體,事務內所有操作要麼全做成功,要麼全失敗。
Consistency(一致性):
事務執行前後,資料從一個狀態到另一個狀態必須是一致的(A 向 B 轉賬,不能出現 A 扣了錢,B 卻沒收到)。
Isolation(隔離性):
多個併發事務之間相互隔離,不能互相干擾。
Durablity(永續性):
事務完成後,對資料的更改是永久儲存的,不能回滾。
分散式事務:
是指事務的參與者、支援事務的伺服器、資源伺服器以及事務管理器分別位於不同的分散式系統的不同節點之上。分散式事務通常用於在分散式系統中保證不同節點之間的資料一致性。
分散式事務的解決方案一般有以下幾種:
XA(2PC/3PC):
最具有代表性的是由 Oracle Tuxedo 系統提出的 XA 分散式事務協議。XA 中大致分為兩部分:事務管理器和本地資源管理器。
其中本地資源管理器往往由資料庫實現,比如 Oracle、DB2 這些商業資料庫都實現了 XA 介面,而事務管理器作為全域性的排程者,負責各個本地資源的提交和回滾。
XA 協議通常包含兩階段提交(2PC)和三階段提交(3PC)兩種實現。兩階段提交顧名思義就是要進行兩個階段的提交:第一階段,準備階段(投票階段);第二階段,提交階段(執行階段)。
實現過程如下所示:
二階段提交看似能夠提供原子性的操作,但它存在著一些缺陷,三段提交(3PC)是對兩段提交(2PC)的一種升級最佳化,有興趣的可以深入瞭解一下,這裡不再贅述。
TCC(Try-Confirm-Cancel):
是 Try、Commit、Cancel 三種指令的縮寫,又被稱補償事務。
其邏輯模式類似於 XA 兩階段提交,事務處理流程也很相似,但 2PC 是應用於在 DB 層面,TCC 則可以理解為在應用層面的 2PC,是需要我們編寫業務邏輯來實現。
TCC 它的核心思想是:“針對每個操作都要註冊一個與其對應的確認(Try)和補償(Cancel)”。
訊息事務:
所謂的訊息事務就是基於訊息佇列的兩階段提交,本質上是對訊息佇列的一種特殊利用。
它是將本地事務和發訊息放在了一個分散式事務裡,保證要麼本地操作成功成功並且對外發訊息成功,要麼兩者都失敗。
基於訊息佇列的兩階段提交往往用在高併發場景下,將一個分散式事務拆成一個訊息事務(A 系統的本地操作+發訊息)+B 系統的本地操作。
其中 B 系統的操作由訊息驅動,只要訊息事務成功,那麼 A 操作一定成功,訊息也一定發出來了。
這時候 B 會收到訊息去執行本地操作,如果本地操作失敗,訊息會重投,直到 B 操作成功,這樣就變相地實現了 A 與 B 的分散式事務。
原理如下:
雖然上面的方案能夠完成 A 和 B 的操作,但是 A 和 B 並不是強一致的,而是最終一致(Eventually consistent)的。而這也是滿足 BASE 理論的要求的。
這裡引申一下,BASE 是 Basically Available(基本可用)、Soft state(軟狀態)和 Eventually consistent(最終一致性)三個短語的縮寫。
BASE 理論是對 CAP 中 AP(CAP 已經被證實一個分散式系統最多隻能同時滿足 CAP 三項中的兩項)的一個擴充套件,透過犧牲強一致性來獲得可用性。
當出現故障允許部分不可用但要保證核心功能可用,允許資料在一段時間內是不一致的,但最終達到一致狀態。滿足 BASE 理論的事務,我們稱之為“柔性事務”。
②什麼是 Exactly-once (精確一次)語義?
在分散式系統中,任何節點都有可能出現異常甚至宕機。在訊息佇列中也一樣,當 Producer 在生產訊息時,可能會發生 Broker 宕機不可用,或者網路突然中斷等異常情況。
根據在發生異常時 Producer 處理訊息的方式,系統可以具備以下三種訊息語義。
At-least-once(至少一次)語義:
Producer 透過接收 Broker 的 ACK(訊息確認)通知來確保訊息成功寫入 Topic。
然而,當 Producer 接收 ACK 通知超時,或者收到 Broker 出錯資訊時,會嘗試重新發送訊息。
如果 Broker 正好在成功把訊息寫入到 Topic,但還沒有給 Producer 傳送 ACK 時宕機,Producer 重新發送的訊息會被再次寫入到 Topic,最終導致訊息被重複分發至 Consumer。即:訊息不會丟失,但有可能被重複傳送。
At-most-once(最多一次)語義:
當 Producer 在接收 ACK 超時,或者收到 Broker 出錯資訊時不重發訊息,那就有可能導致這條訊息丟失,沒有寫入到 Topic 中,也不會被 Consumer 消費到。
在某些場景下,為了避免發生重複消費,我們可以容許訊息丟失的發生。即:訊息可能會丟失,但絕不會被重複傳送。
Exactly-once(精確一次)語義:
Exactly-once 語義保證了即使 Producer 多次傳送同一條訊息到服務端,服務端也僅僅會記錄一次。
Exactly-once 語義是最可靠的,同時也是最難理解的。Exactly-once 語義需要訊息佇列服務端,訊息生產端和消費端應用三者的協同才能實現。
比如,當消費端應用成功消費並且 ACK 了一條訊息之後,又把消費位點回滾到之前的一個訊息 ID,那麼從那個訊息 ID 往後的所有訊息都會被消費端應用重新消費到。即:訊息不會丟失,也不會被重複傳送。
RocketMQ、Kafka、Pulsar 事務訊息
①RocketMQ 的事務訊息
RocketMQ 在 4。3。0 版中已經支援分散式事務訊息,這裡 RocketMQ 採用了 2PC 的思想來實現了提交事務訊息,同時增加一個補償邏輯來處理二階段超時或者失敗的訊息。
流程如下圖所示:
其具體工作流程分為正常事務訊息的傳送及提交和不正常情況下事務訊息的補償流程:
在訊息佇列上開啟一個事務主題。
事務中第一個執行的服務傳送一條“半訊息”(半訊息和普通訊息的唯一區別是,在事務提交之前,對於消費者來說,這個訊息是不可見的)給訊息佇列。
半訊息傳送成功後,傳送半訊息的服務就會開始執行本地事務,根據本地事務執行結果來決定事務訊息提交或者回滾。
本地事務成功後會讓這個“半訊息”變成正常訊息,供分散式事務後面的步驟執行自己的本地事務。
這裡的事務訊息,Producer 不會因為 Consumer 消費失敗而做回滾,採用事務訊息的應用,其所追求的是高可用和最終一致性,訊息消費失敗的話,RocketMQ 自己會負責重推訊息,直到消費成功。
補償流程:
RocketMQ 提供事務反查來解決異常情況,如果 RocketMQ 沒有收到提交或者回滾的請求,Broker 會定時到生產者上去反查本地事務的狀態,然後根據生產者本地事務的狀態來處理這個“半訊息”是提交還是回滾。
值得注意的是我們需要根據自己的業務邏輯來實現反查邏輯介面,然後根據返回值 Broker 決定是提交還是回滾。
而且這個反查介面需要是無狀態的,請求到任意一個生產者節點都會返回正確的資料。
其中,補償流程用於解決訊息 Commit 或者 Rollback 發生超時或者失敗的情況。在 RocketMQ 事務訊息的主要流程中,一階段的訊息如何對使用者不可見。
其中,事務訊息相對普通訊息最大的特點就是一階段傳送的訊息對使用者是不可見的。
那麼,如何做到寫入訊息但是對使用者不可見呢?RocketMQ 事務訊息的做法是:如果訊息是“半訊息”,將備份原訊息的主題與訊息消費佇列,然後改變主題為 RMQ_SYS_TRANS_HALF_TOPIC。
由於消費組未訂閱該主題,故消費端無法消費“半訊息”的訊息,然後 RocketMQ 會開啟一個定時任務,從 Topic 為 RMQ_SYS_TRANS_HALF_TOPIC 中拉取訊息進行消費。
根據生產者組獲取一個服務提供者傳送回查事務狀態請求,根據事務狀態來決定是提交或回滾訊息。
講到這裡大家就明白了,這裡說的就是上文提到分散式事務中的訊息事務,目的是在分散式事務中實現系統的最終一致性。
②Kafka 的事務訊息
與 RocketMQ 的事務訊息用途不同,Kafka 的事務基本上是配合其冪等機制來實現 Exactly-once(見上文)語義的。
開發此功能的原因可以總結如下:
流處理的需求:
隨著流處理的興起,對具有更強處理保證的流處理應用的需求也在增長。
例如,在金融行業,金融機構使用流處理引擎為使用者處理借款和信貸。這種型別的用例要求每條訊息都只處理一次,無一例外。
換句話說,如果流處理應用程式消費訊息 A 並將結果作為訊息 B(B = f(A)),那麼恰好一次處理保證意味著當且僅當 B 被成功生產後 A 才能被標記為消費,反之亦然。
事務 API 使流處理應用程式能夠在一個原子操作中使用、處理和生成訊息。這意味著,事務中的一批訊息可以從許多主題分割槽接收、生成和確認。一個事務涉及的所有操作都作為整體成功或失敗。
目前,Kafka 預設提供的交付可靠性保障是 At-least-once。如果訊息成功“提交”,但 Broker 的應答沒有成功傳送回 Producer 端(比如網路出現瞬時抖動),那麼 Producer 就無法確定訊息是否真的提交成功了。
因此,它只能選擇重試,這就是 Kafka 預設提供 At-least-once 保障的原因,不過這會導致訊息重複傳送。
大部分使用者還是希望訊息只會被交付一次,這樣的話,訊息既不會丟失,也不會被重複處理。
或者說,即使 Producer 端重複傳送了相同的訊息,Broker 端也能做到自動去重。
在下游 Consumer 看來,訊息依然只有一條。那麼問題來了,Kafka 是怎麼做到精確一次的呢?
簡單來說,這是透過兩種機制:
冪等性(Idempotence)
事務(Transaction)
冪等性 Producer:“冪等”這個詞原是數學領域中的概念,指的是某些操作或函式能夠被執行多次,但每次得到的結果都是不變的。
冪等性有很多好處,其最大的優勢在於我們可以安全地重試任何冪等性操作,反正它們也不會破壞我們的系統狀態。
如果是非冪等性操作,我們還需要擔心某些操作執行多次對狀態的影響,但對於冪等性操作而言,我們根本無需擔心此事。
在 Kafka 中,Producer 預設不是冪等性的,但我們可以建立冪等性 Producer。它其實是 0。11。0。0 版本引入的新功能。
enable。idempotence 被設定成 true 後,Producer 自動升級成冪等性 Producer,其他所有的程式碼邏輯都不需要改變。
Kafka 自動幫你做訊息的重複去重。Kafka 為了實現冪等性,它在底層設計架構中引入了 ProducerID 和 SequenceNumber。
ProducerID:在每個新的 Producer 初始化時,會被分配一個唯一的 ProducerID,用來標識本次會話。
SequenceNumber:對於每個 ProducerID,Producer 傳送資料的每個 Topic 和 Partition 都對應一個從 0 開始單調遞增的 SequenceNumber 值。
Broker 在記憶體維護(pid,seq)對映,收到訊息後檢查 seq。Producer 在收到明確的的訊息丟失 ack,或者超時後未收到 ack,要進行重試。
new_seq=old_seq+1:正常訊息。
new_seq<=old_seq:重複訊息。
new_seq>old_seq+1:訊息丟失。
另外我們需要了解冪等性 Producer 的作用範圍。首先,它只能保證單分割槽上的冪等性,即一個冪等性 Producer 能夠保證某個主題的一個分割槽上不出現重複訊息,它無法實現多個分割槽的冪等性。
其次,它只能實現單會話上的冪等性,不能實現跨會話的冪等性。這裡的會話,你可以理解為 Producer 程序的一次執行。當你重啟了 Producer 程序之後,這種冪等性保證就喪失了。
如果想實現多分割槽以及多會話上的訊息無重複,應該怎麼做呢?答案就是事務(transaction)或者依賴事務型 Producer。這也是冪等性 Producer 和事務型 Producer 的最大區別。
事務型 Producer:
能夠保證將訊息原子性地寫入到多個分割槽中。這批訊息要麼全部寫入成功,要麼全部失敗。
另外,事務型 Producer 也不受程序的重啟影響。Producer 重啟後,Kafka 依然保證它們傳送訊息的 Exactly-once 處理。
和普通 Producer 程式碼相比,事務型 Producer 的顯著特點是呼叫了一些事務 API。
如 initTransaction、beginTransaction、commitTransaction 和 abortTransaction,它們分別對應事務的初始化、事務開始、事務提交以及事務終止。
Kafka 事務訊息是由 Producer、事務協調器、Broker、組協調器、Consumer 等共同參與實現的。
Producer:
為 Producer 指定固定的 TransactionalId(事務 id),可以穿越 Producer 的多次會(Producer 重啟/斷線重連)中,持續標識 Producer 的身份。
每個生產者增加一個 epoch。用於標識同一個 TransactionalId 在一次事務中的 epoch,每次初始化事務時會遞增,從而讓服務端可以知道生產者請求是否舊的請求。
使用 epoch 標識 Producer 的每一次“重生”,可以防止同一 Producer 存在多個會話。
Producer 遵從冪等訊息的行為,並在傳送的 BatchRecord 中增加事務 id 和 epoch。
事務協調器(Transaction Coordinator):
引入事務協調器,類似於消費組負載均衡的協調者,每一個實現事務的生產端都被分配到一個事務協調者。以兩階段提交的方式,實現訊息的事務提交。
事務協調器使用一個特殊的 Topic:即事務 Topic,事務 Topic 本身也是持久化的,日誌資訊記錄事務狀態資訊,由事務協調者寫入。
事務協調器透過 RPC 呼叫,協調 Broker 和 Consumer 實現事務的兩階段提交。
每一個 Broker 都會啟動一個事務協調器,使用 hash(TransactionalId)確定 Producer 對應的事務協調器,使得整個叢集的負載均衡。
Broker:
引入控制訊息(Control Messages):這些訊息是客戶端產生的並寫入到主題的特殊訊息,但對於使用者來說不可見。它們是用來讓 Broker 告知消費者之前拉取的訊息是否被原子性提交。
Broker 處理事務協調器的 commit/abort 控制訊息,把控制訊息向正常訊息一樣寫入 Topic(圖中標 c 的訊息,和正常訊息交織在一起,用來確認事務提交的日誌偏移),並向前推進訊息提交偏移 hw。
組協調器:
如果在事務過程中,提交了消費偏移,組協調器在 offset log 中寫入事務消費偏移。當事務提交時,在 offset log 中寫入事務 offset 確認訊息。
Consumer:
Consumer 過濾未提交訊息和事務控制訊息,使這些訊息對使用者不可見。
有兩種實現方式:
Consumer 快取方式:
設定 isolation。level=read_uncommitted,此時 topic 的所有訊息對 Consumer 都可見。
Consumer 快取這些訊息,直到收到事務控制訊息。若事務 commit,則對外發布這些訊息;若事務 abort,則丟棄這些訊息。
Broker 過濾方式:
設定 isolation。level=read_committed,此時 topic 中未提交的訊息對 Consumer 不可見,只有在事務結束後,訊息才對 Consumer 可見。
Broker 給 Consumer 的 BatchRecord 訊息中,會包含以列表,指明哪些是“abort”事務,Consumer 丟棄 abort 事務的訊息即可。
因為事務機制會影響消費者所能看到的訊息的範圍,它不只是簡單依賴高水位來判斷。
它依靠一個名為 LSO(Log Stable Offset)的位移值來判斷事務型消費者的可見性。
③Pulsar 的事務訊息
Apache Pulsar 在 2。8。0 正式支援了事務相關的功能,Pulsar 這裡提供的事務區別於 RocketMQ 中 2PC 那種事務的實現方式,沒有本地事務回查的機制,更類似於 Kafka 的事務實現機制。
Apache Pulsar 中的事務主要用來保證類似 Pulsar Functions 這種流計算場景中 Exactly-once 語義的實現。
這也符合 Apache Pulsar 本身 Event Streaming 的定位,即保證端到端(End-to-End)的事務實現的語義。
在 Pulsar 中,對於事務語義是這樣定義的:允許事件流應用將消費、處理、生產訊息整個過程定義為一個原子操作,即生產者或消費者能夠處理跨多個主題和分割槽的訊息,並確保這些訊息作為一個單元被處理。
Pulsar 事務具有以下語義:
事務中的所有操作都作為一個單元提交。要麼提交所有訊息,要麼都不提交。
每條訊息只寫入或處理一次,不會丟失資料或重複(即使發生故障)。
如果事務中止,則此事務中的所有寫入和確認都將回滾。
事務中的批次訊息可以被以多分割槽接收、生產和確認:
消費者只能讀取已提交(確認)的訊息。換句話說,Broker 不傳遞屬於開啟事務的事務訊息或屬於中止事務的訊息。
跨多個分割槽的訊息寫入是原子性的。
跨多個訂閱的訊息確認是原子性的。訂閱下的消費者在確認帶有事務 ID 的訊息時,只會成功確認一次訊息。
Pulsar 事務訊息由以下幾個關鍵點構成:
事務 ID(TxnID):
標識 Pulsar 中的唯一事務。事務 ID 長度是 128-bit。最高 16 位保留給事務協調器的 ID,其餘位用於每個事務協調器中單調遞增的數字。
事務協調器(TC):
是執行在 Pulsar Broker 中的一個模組。它維護事務的整個生命週期,並防止事務進入錯誤狀態;它處理事務超時,並確保事務在事務超時後中止。
事務日誌:
所有事務元資料都儲存在事務日誌中。事務日誌由 Pulsar 主題記錄。如果事務協調器崩潰,它可以從事務日誌恢復事務元資料。
事務日誌儲存事務狀態,而不是事務中的實際訊息(實際訊息儲存在實際的主題分割槽中)。
事務快取:
向事務內的主題分割槽生成的訊息儲存在該主題分割槽的事務緩衝區(TB)中。
在提交事務之前,事務緩衝區中的訊息對消費者不可見。當事務中止時,事務緩衝區中的訊息將被丟棄。
事務緩衝區將所有正在進行和中止的事務儲存在記憶體中。所有訊息都發送到實際的分割槽 Pulsar 主題。
提交事務後,事務緩衝區中的訊息對消費者具體化(可見)。事務中止時,事務緩衝區中的訊息將被丟棄。
待確認狀態:
掛起確認狀態在事務完成之前維護事務中的訊息確認。如果訊息處於掛起確認狀態,則在該訊息從掛起確認狀態中移除之前,其他事務無法確認該訊息。
掛起的確認狀態被保留到掛起的確認日誌中(cursor ledger)。新啟動的 broker 可以從掛起的確認日誌中恢復狀態,以確保狀態確認不會丟失。
處理流程一般分為以下幾個步驟:
開啟事務。
使用事務釋出訊息。
使用事務確認訊息。
結束事務。
Pulsar 的事務處理流程與 Kafka 的事務處理思路大致上保持一致,大家都有一個 TC 以及對應的一個用於持久化 TC 所有操作的 Topic 來記錄所有事務狀態變更的請求。
同樣的在事務開始階段也都有一個專門的 Topic 來去查詢 TC 對應的 Owner Broker 的位置在哪裡。
不同的是:
Kafka 中對於未確認的訊息是維護在 Broker 端的,但是 Pulsar 的是維護在 Client 端的,透過 Transaction Timeout 來決定這個事務是否執行成功,所以有了 Transaction Timeout 的存在之後,就可以確保 Client 和 Broker 側事務處理的一致性。
由於 Kafka 本身沒有單條訊息的 Ack,所以 Kafka 的事務處理只能是順序執行的,當一個事務請求被阻塞之後,會阻塞後續所有的事務請求,但是 Pulsar 是可以對訊息進行單條 Ack 的,所以在這裡每一個事務的 Ack 動作是獨立的,不會出現事務阻塞的情況。
結論
RocketMQ 和 Kafka/Pulsar 的事務訊息實用的場景是不一樣的。
RocketMQ 中的事務,它解決的問題是,確保執行本地事務和發訊息這兩個操作,要麼都成功,要麼都失敗。
並且 RocketMQ 增加了一個事務反查的機制,來儘量提高事務執行的成功率和資料一致性。
Kafka 中的事務,它解決的問題是,確保在一個事務中傳送的多條訊息,要麼都成功,要麼都失敗。
這裡面的多條訊息不一定要在同一個主題和分割槽中,可以是發往多個主題和分割槽的訊息。
當然也可以在 Kafka 事務執行過程中開啟本地事務來實現類似 RocketMQ 事務訊息的效果。
但是 Kafka 是沒有事務訊息反查機制的,它是直接丟擲異常的,使用者可以根據異常來實現自己的重試等方法保證事務正常執行。
它們的共同點就是:都是透過兩階段提交來實現事務的,事務訊息都儲存在單獨的主題上。
不同的地方就是 RocketMQ 是透過“半訊息”來實現的,Kafka 是直接將訊息傳送給對應的 topic,透過客戶端來過濾實現的。
而且它們兩個使用的場景區別是非常之大的,RockteMQ 主要解決的是基於本地事務和訊息的資料一致性,而 Kafka 的事務則是用於實現它的 Exactly-once 機制,應用於實時流計算的場景中。
Pulsar 的事務訊息和 Kafka 應用場景和語義類似,只是由於底層實現機制有差別,在一些細節上有區別。
相信看到這裡就非常清楚了,對於事務訊息如何選型和應用,首先要明白你的業務需求是什麼。
是要實現分散式事務的最終一致性,還是要實現 Exactly-once (精確一次)語義?明白之後需求,選擇什麼元件就十分明確了。
一週年慶 抽獎活動
關注鴻蒙技術社群訂閱號,在此公眾號回覆
“週年慶”
,抽鴻蒙新款
MatePad11
、價值 399、299 元的
鴻蒙盲盒
和 2000ml 健康
隨身杯
!
關注回覆“週年慶”抽獎
作者:劉若愚
簡介:微信支付後臺開發工程師,碩士畢業於北京大學。深度參與騰訊 WXG 境外支付團隊多個重要業務的研發工作,有豐富的後臺開發經驗。騰訊技術分享達人,社會招聘伯樂。
編輯:陶家龍
推薦文章
- 三國殺:打著燈籠都找不到的忠臣有哪些?這些“燈籠忠”死遠點!
光是這個桃子只能自己用的debuff就完全杜絕謀呂蒙當忠的所有可能了,天知道為什麼遊戲裡還有那麼多奇葩忠臣會選這麼個玩意...
- 一首歌曲的故事
當百靈鳥與小白兔分手時,百靈鳥抱歉地向小白兔說它現在沒有禮物可以送給它...
- 巔峰克萊,可能是任何一支爭冠球隊的完美拼圖,但無法跟哈登比較
談歷史地位確實除了團隊合作以外,個人的能力,實力也很重要,像詹姆斯,喬丹,奧尼爾,科比這些球星應該是有實力的,根本就不用懷疑,相信大多數人是服氣的,而且另外的一些就不好說了,有爭議就說明實力不夠,實力差點意思才有人不服吧...