每個時代,都不會虧待會學習的人。
大家好,我是 yes。
今天我想和大家一起盤一盤分散式事務,會介紹常見的分散式事務實現方案和其優缺點以及適用的場景,並會帶出它們的一些變體實現。
還會捎帶一下分散式資料庫對 2PC 的改進模型,看看分散式資料庫是如何做的。
然後再分析一波分散式事務框架 Seata 的具體實現,看看分散式事務究竟是如何落地的,畢竟協議要落地才是有用的。
首先我們來提一下事務和分散式事務是什麼。
事務
事務的 ACID 想必大家都熟知,這其實是嚴格意義上的定義,指的是事務的實現必須具備原子性、一致性、隔離性和永續性。
不過嚴格意義上的事務很難達到,像我們熟知的資料庫就有各種隔離級別,隔離級別越高效能越低,所以往往我們都會從中找到屬於自己的平衡,不會遵循嚴格意義上的事務。
並且在我們平日的談論中,所謂的事務往往簡單的指代一系列的操作全部執行成功,或者全部失敗,不會出現一些成功一些失敗的情形。
清晰了平日我們對事務的定義之後,再來看看什麼是分散式事務。
分散式事務
由於網際網路的快速發展,以往的單體架構頂不住這麼多的需求,這麼複雜的業務,這麼大的流量。
單體架構的優勢在於前期快速搭建、快速上線,並且方法和模組之間都是內部呼叫,沒有網路的開銷更加的高效。
從某方面來說部署也方便,畢竟就一個包,扔上去。
不過隨著企業的發展,業務的複雜度越來越高,內部耦合極其嚴重,導致牽一髮而動全身,開發不易,測試不易。
並且無法根據熱點服務進行動態的伸縮,比如商品服務訪問量特別大,如果是單體架構的話我們只能把整個應用複製多份叢集部署,浪費資源。
因此拆分勢在必行,微服務架構就這麼來了。
拆分之後服務之間的邊界就清晰了,每個服務都能獨立地執行,獨立地部署,所以能以服務級別彈性伸縮了。
服務之間的本地呼叫變成了遠端呼叫,鏈路更長了,一次呼叫的耗時更長了,但是總體的吞吐量更大了。
不過拆分之後還會引入其他複雜度,比如服務鏈路的監控、整體的監控、容錯措施、彈性伸縮等等運維監控的問題,還有像分散式事務、分散式鎖跟業務息息相關的問題等。
往往解決了一個痛點又會引入別的痛點,所以架構的演進都是權衡的結果,就看你們的系統更能忍受哪種痛點了。
而今天我們談及的就是分散式事務這個痛點。
分散式事務是由多個本地事務組成的,分散式事務跨越了多裝置,之間又經歷的複雜的網路,可想而知想要實現嚴格的事務道路阻且長。
單機版事務都不會嚴格遵守事務的嚴格實現,更別說分散式事務了,所以在現實情況下我們只能實現殘缺版的事務。
在明確了事務和分散式事務之後,我們就先來看看常見的分散式事務方案:2PC、3PC、TCC、本地訊息、事務訊息。
2PC
2PC,Two-phase commit protocol,即兩階段提交協議。
它引入了一個事務協調者角色,來管理各個參與者(就是各資料庫資源)。
整體分為兩個階段,分別是準備階段和提交/回滾階段。
我們先來看看第一個階段,即準備階段。
由事務協調者給每個參與者傳送準備命令,每個參與者收到命令之後會執行相關事務操作,你可以認為除了事務的提交啥都做了。
然後每個參與者會返回響應告知協調者自己是否準備成功。
協調者收到每個參與者的響應之後就進入第二階段,根據收集的響應,如果有一個參與者響應準備失敗那麼就向所有參與者傳送回滾命令,反之傳送提交命令。
這個協議其實很符合正常的思維,就像我們大學上課點名的時候,其實老師就是協調者的角色,我們都是參與者。
老師一個一個的點名,我們一個一個的喊到,最後老師收到所有同學的到之後就開始了今天的講課。
而和點名有所不同的是,老師發現某幾個學生不在還是能繼續上課,而我們的事務可不允許這樣。
事務協調者在第一階段未收到個別參與者的響應,則等待一定時間就會認為事務失敗,會傳送回滾命令,所以在 2PC 中事務協調者有超時機制。
我們再來分析一下 2PC 的優缺點。
2PC 的優點是能利用資料庫自身的功能進行本地事務的提交和回滾,也就是說提交和回滾實際操作不需要我們實現,不侵入業務邏輯由資料庫完成,在之後講解 TCC 之後相信大家對這點會有所體會。
2PC 主要有三大缺點:同步阻塞、單點故障和資料不一致問題。
同步阻塞
可以看到在第一階段執行了準備命令後,我們每個本地資源都處於鎖定狀態,因為除了事務的提交之外啥都做了。
所以這時候如果本地的其他請求要訪問同一個資源,比如要修改商品表 id 等於 100 的那條資料,那麼此時是被阻塞住的,必須等待前面事務的完結,收到提交/回滾命令執行完釋放資源後,這個請求才能得以繼續。
所以假設這個分散式事務涉及到很多參與者,然後有些參與者處理又特別複雜,特別慢,那麼那些處理快的節點也得等著,所以說效率有點低。
單點故障
可以看到這個單點就是協調者,如果協調者掛了整個事務就執行不下去了。
如果協調者在傳送準備命令前掛了還行,畢竟每個資源都還未執行命令,那麼資源是沒被鎖定的。
可怕的是在傳送完準備命令之後掛了,這時候每個本地資源都執行完處於鎖定狀態了,都杵著了,這就很僵硬了,如果是某個熱點資源都阻塞了,這估計就要GG了。
資料不一致問題
因為協調者和參與者之間的交流是經過網路的,而網路有時候就會抽風的或者發生區域性網路異常。
那麼就有可能導致某些參與者無法收到協調者的請求,而某些收到了。比如是提交請求,然後那些收到命令的參與者就提交事務了,此時就產生了資料不一致的問題。
小結一下 2PC
至此我們來先小結一些 2PC ,它是一個同步阻塞的強一致性兩階段提交協議,分別是準備階段和提交/回滾階段。
2PC 的優勢在於對業務沒有侵入,可以利用資料庫自身機制來進行事務的提交和回滾。
它的缺點:是一個同步阻塞協議,會導致高延遲和效能的下降,並且存在協調者單點故障問題,極端情況下會有資料不一致的問題。
當然這只是協議,具體的落地還是可以變通了,比如協調者單點問題,我就搞個主從來實現協調者,對吧。
分散式資料庫的 2PC 改進模型
可能有些人對分散式資料庫不熟悉,沒有關係,我們主要學的是思想,看看人家的思路。
我簡單的講下 Percolator 模型,它是基於分散式儲存系統 BigTable 建立的模型,BigTable 是啥也不清楚的同學沒有關係影響不大。
還是拿轉賬的例子來說,我現在有 200 塊錢,你現在有 100 塊錢,為了突出重點我也不按正常的結構來畫這個表。
然後我要轉 100 塊給你。
此時事務管理器發起了準備請求,然後我賬上的錢就少了,你賬上的錢就多了,而且事務管理器還記錄下這次操作的日誌。
此時的資料還是私有版本,別的事務是讀不到的,簡單的理解 Lock 上有值就還是私有的。
可以看到我的記錄 Lock 標記的是 PK,你的記錄標記的是指向我的記錄指標,這個 PK 是隨機選擇的。
然後事務管理器會向被選擇作為 PK 的那條記錄發起提交指令。
此時就會把我的記錄的鎖給抹去了,這等於我的記錄不再是私有版本了,別的事務就都能訪問了。
那你的記錄上還有鎖啊?不用更新嗎?
嘿嘿不需要及時更新,因為訪問你的這條記錄的時候會去根據指標找我的那個記錄,發現記錄已經提交了所以你的記錄就可以被訪問了。
有人說這效率不就差了,每次都要去找一次,別急。
後臺會有個執行緒來掃描,然後更新把鎖記錄給去了。
這不就穩了嘛。
相比於 2PC 的改進
首先 Percolator 在提交階段不需要和所有的參與者互動,主需要和一個參與者打交道,所以這個提交是原子的!解決了資料不一致問題。
然後事務管理器會記錄操作日誌,這樣當事務管理器掛了之後選舉的新事務管理器就可以通過日誌來得知當前的情況從而繼續工作,解決了單點故障問題。
並且 Percolator 還會有後臺執行緒,會掃描事務狀況,在事務管理器當機之後會回滾各個參與者上的事務。
可以看到相對於 2PC 還是做了很多改進的,也是巧妙的。
其實分散式資料庫還有別的事務模型,不過我也不太熟悉,就不多嗶嗶了,有興趣的同學可以自行了解。
還是挺能拓寬思想的。
XA 規範
讓我們再回來 2PC,既然說到 2PC 了那麼也簡單的提一下 XA 規範,XA 規範是基於兩階段提交的,它實現了兩階段提交協議。
在說 XA 規範之前又得先提一下 DTP 模型,即 Distributed Transaction Processing,這模型規範了分散式事務的模型設計。
而 XA 規範又約束了 DTP 模型中的事務管理器(TM) 和資源管理器(RM)之間的互動,簡單的說就是你們兩之間要按照一定的格式規範來交流!
我們先來看下 XA 約束下的 DTP 模型。
- AP 應用程式,就是我們的應用,事務的發起者。
- RM 資源管理器,簡單的認為就是資料庫,具備事務提交和回滾能力,對應我們上面的 2PC 就是參與者。
- TM 事務管理器,就是協調者了,和每個 RM 通訊。
簡單的說就是 AP 通過 TM 來定義事務操作,TM 和 RM 之間會通過 XA 規範進行通訊,執行兩階段提交,而 AP 的資源是從 RM 拿的。
從模型上看有三個角色,而實際實現可以由一個角色實現兩個功能,比如 AP 來實現 TM 的功能,TM 沒必要抽出來單獨部署。
MySQL XA
知曉了 DTP 之後,我們就來看看 XA 在 MySQL 中是如何操作的,不過只有 InnoDB 支援。
簡單的說就是要先定義一個全域性唯一的 XID,然後告知每個事務分支要進行的操作。
可以看到圖中執行了兩個操作,分別是改名字和插入日誌,等於先註冊下要做的事情,通過 XA START XID 和 XA END XID 來包裹要執行的 SQL。
然後需要傳送準備命令,來執行第一階段,也就是除了事務的提交啥都幹了的階段。
然後根據準備的情況來選擇執行提交事務命令還是回滾事務命令。
基本上就是這麼個流程,不過 MySQL XA 的效能不高這點是需要注意的。
可以看到雖說 2PC 有缺點,但是還是有基於 2PC 的落地實現的,而 3PC 的引出是為了解決 2PC 的一些缺點,但是它整體下來開銷更大,也解決不了網路分割槽的問題,我也沒有找到 3PC 的落地實現。
不過我還是稍微提一下,知曉一下就行,純理論。
3PC
3PC 的引入是為了解決 2PC 同步阻塞和減少資料不一致的情況。
3PC 也就是多了一個階段,一個詢問的階段,分別是準備、預提交和提交這三個階段。
準備階段單純就是協調者去訪問參與者,類似於你還好嗎?能接請求不。
預提交其實就是 2PC 的準備階段,除了事務的提交啥都幹了。
提交階段和 2PC 的提交一致。
3PC 多了一個階段其實就是在執行事務之前來確認參與者是否正常,防止個別參與者不正常的情況下,其他參與者都執行了事務,鎖定資源。
出發點是好的,但是絕大部分情況下肯定是正常的,所以每次都多了一個互動階段就很不划算。
然後 3PC 在參與者處也引入了超時機制,這樣在協調者掛了的情況下,如果已經到了提交階段了,參與者等半天沒收到協調者的情況的話就會自動提交事務。
不過萬一協調者發的是回滾命令呢?你看這就出錯了,資料不一致了。
還有維基百科上說 2PC 參與者準備階段之後,如果協調者掛了,參與者是無法得知整體的情況的,因為大局是協調者掌控的,所以參與者相互之間的狀況它們不清楚。
而 3PC 經過了第一階段的確認,即使協調者掛了參與者也知道自己所處預提交階段是因為已經得到準備階段所有參與者的認可了。
簡單的說就像加了個圍欄,使得各參與者的狀態得以統一。
小結 2PC 和 3PC
從上面已經知曉了 2PC 是一個強一致性的同步阻塞協議,效能已經是比較差的了。
而 3PC 的出發點是為了解決 2PC 的缺點,但是多了一個階段就多了一次通訊的開銷,而且是絕大部分情況下無用的通訊。
雖說引入參與者超時來解決協調者掛了的阻塞問題,但是資料還是會不一致。
可以看到 3PC 的引入並沒什麼實際突破,而且效能更差了,所以實際只有 2PC 的落地實現。
再提一下,2PC 還是 3PC 都是協議,可以認為是一種指導思想,和真正的落地還是有差別的。
TCC
不知道大家注意到沒,不管是 2PC 還是 3PC 都是依賴於資料庫的事務提交和回滾。
而有時候一些業務它不僅僅涉及到資料庫,可能是傳送一條簡訊,也可能是上傳一張圖片。
所以說事務的提交和回滾就得提升到業務層面而不是資料庫層面了,而 TCC 就是一種業務層面或者是應用層的兩階段提交。
TCC 分為指代 Try、Confirm、Cancel ,也就是業務層面需要寫對應的三個方法,主要用於跨資料庫、跨服務的業務操作的資料一致性問題。
TCC 分為兩個階段,第一階段是資源檢查預留階段即 Try,第二階段是提交或回滾,如果是提交的話就是執行真正的業務操作,如果是回滾則是執行預留資源的取消,恢復初始狀態。
比如有一個扣款服務,我需要寫 Try 方法,用來凍結釦款資金,還需要一個 Confirm 方法來執行真正的扣款,最後還需要提供 Cancel 來進行凍結操作的回滾,對應的一個事務的所有服務都需要提供這三個方法。
可以看到本來就一個方法,現在需要膨脹成三個方法,所以說 TCC 對業務有很大的侵入,像如果沒有凍結的那個欄位,還需要改表結構。
我們來看下流程。
雖說對業務有侵入,但是 TCC 沒有資源的阻塞,每一個方法都是直接提交事務的,如果出錯是通過業務層面的 Cancel 來進行補償,所以也稱補償性事務方法。
這裡有人說那要是所有人 Try 都成功了,都執行 Comfirm 了,但是個別 Confirm 失敗了怎麼辦?
這時候只能是不停地重試調失敗了的 Confirm 直到成功為止,如果真的不行只能記錄下來,到時候人工介入了。
TCC 的注意點
這幾個點很關鍵,在實現的時候一定得注意了。
冪等問題,因為網路呼叫無法保證請求一定能到達,所以都會有重調機制,因此對於 Try、Confirm、Cancel 三個方法都需要冪等實現,避免重複執行產生錯誤。
空回滾問題,指的是 Try 方法由於網路問題沒收到超時了,此時事務管理器就會發出 Cancel 命令,那麼需要支援 Cancel 在未執行 Try 的情況下能正常的 Cancel。
懸掛問題,這個問題也是指 Try 方法由於網路阻塞超時觸發了事務管理器發出了 Cancel 命令,但是執行了 Cancel 命令之後 Try 請求到了,你說氣不氣。
這都 Cancel 了你來個 Try,對於事務管理器來說這時候事務已經是結束了的,這凍結操作就被“懸掛”了,所以空回滾之後還得記錄一下,防止 Try 的再呼叫。
TCC 變體
上面我們說的是通用型的 TCC,它需要改造以前的實現,但是有一種情況是無法改造的,就是你呼叫的是別的公司的介面。
沒有 Try 的 TCC
比如坐飛機需要換乘,換乘的又是不同的航空公司,比如從 A 飛到 B,再從 B 飛到 C,只有 A - B 和 B - C 都買到票了才有意義。
這時候的選擇就沒得 Try 了,直接呼叫航空公司的買票操作,當兩個航空公司都買成功了那就直接成功了,如果某個公司買失敗了,那就需要呼叫取消訂票介面。
也就是在第一階段直接就執行完整個業務操作了,所以要重點關注回滾操作,如果回滾失敗得有提醒,要人工介入等。
這其實就是 TCC 的思想。
非同步 TCC
這 TCC 還能非同步?其實也是一種折中,比如某些服務很難改造,並且它又不會影響主業務決策,也就是它不那麼重要,不需要及時的執行。
這時候可以引入可靠訊息服務,通過訊息服務來替代個別服務來進行 Try、Confirm、Cancel 。
Try 的時候只是寫入訊息,訊息還不能被消費,Confirm 就是真正發訊息的操作,Cancel 就是取消訊息的傳送。
這可靠訊息服務其實就類似於等下要提到的事務訊息,這個方案等於糅合了事務訊息和 TCC。
TCC 小結
可以看到 TCC 是通過業務程式碼來實現事務的提交和回滾,對業務的侵入較大,它是業務層面的兩階段提交,。
它的效能比 2PC 要高,因為不會有資源的阻塞,並且適用範圍也大於 2PC,在實現上要注意上面提到的幾個注意點。
它是業界比較常用的分散式事務實現方式,而且從變體也可以得知,還是得看業務變通的,不是說你要用 TCC 一定就得死板的讓所有的服務都改造成那三個方法。
本地訊息表
本地訊息就是利用了本地事務,會在資料庫中存放一直本地事務訊息表,在進行本地事務操作中加入了本地訊息的插入,即將業務的執行和將訊息放入訊息表中的操作放在同一個事務中提交
這樣本地事務執行成功的話,訊息肯定也插入成功,然後再呼叫其他服務,如果呼叫成功就修改這條本地訊息的狀態。
如果失敗也不要緊,會有一個後臺執行緒掃描,發現這些狀態的訊息,會一直呼叫相應的服務,一般會設定重試的次數,如果一直不行則特殊記錄,待人工介入處理。
可以看到還是很簡單的,也是一種最大努力通知思想。
事務訊息
這個其實我寫過一篇文章,專門講事務訊息,從原始碼層面剖析了 RocketMQ 、Kafka 的事務訊息實現,以及兩者之間的區別。
在這裡我不再詳細闡述,因為之前的文章寫的很詳細了,大概四五千字吧。我就附上鍊接了:事務訊息
Seata 的實現
首先什麼是 Seata ,摘抄官網的一段話。
Seata 是一款開源的分散式事務解決方案,致力於提供高效能和簡單易用的分散式事務服務。Seata 將為使用者提供了 AT、TCC、SAGA 和 XA 事務模式,為使用者打造一站式的分散式解決方案。
可以看到提供了很多模式,我們先來看看 AT 模式。
AT模式
AT 模式就是兩階段提交,前面我們提到了兩階段提交有同步阻塞的問題,效率太低了,那 Seata 是怎麼解決的呢?
AT 的一階段直接就把事務提交了,直接釋放了本地鎖,這麼草率直接提交的嘛?當然不是,這裡和本地訊息表有點類似,就是利用本地事務,執行真正的事務操作中還會插入回滾日誌,然後在一個事務中提交。
這回滾日誌怎麼來的?
通過框架代理 JDBC 的一些類,在執行 SQL 的時候解析 SQL 得到執行前的資料映象,然後執行 SQL ,再得到執行後的資料映象,然後把這些資料組裝成回滾日誌。
再伴隨的這個本地事務的提交把回滾日誌也插入到資料庫的 UNDO_LOG 表中(所以資料庫需要有一張UNDO_LOG 表)。
這波操作下來在一階段就可以沒有後顧之憂的提交事務了。
然後一階段如果成功,那麼二階段可以非同步的刪除那些回滾日誌,如果一階段失敗那麼可以通過回滾日誌來反向補償恢復。
這時候有細心的同學想到了,萬一中間有人改了這條資料怎麼辦?你這映象就不對了啊?
所以說還有個全域性鎖的概念,在事務提交前需要拿到全域性鎖(可以理解為對這條資料的鎖),然後才能順利提交本地事務。
如果一直拿不到那就需要回滾本地事務了。
官網的示例很好,我就不自己編了,以下部分內容摘抄自 Seata 官網的示例:
此時有兩個事務,分別是 tx1、和 tx2,分別對 a 表的 m 欄位進行更新操作,m 的初始值 1000。
tx1 先開始,開啟本地事務,拿到本地鎖,更新操作 m = 1000 - 100 = 900。本地事務提交前,先拿到該記錄的 全域性鎖 ,本地提交釋放本地鎖。
tx2 後開始,開啟本地事務,拿到本地鎖,更新操作 m = 900 - 100 = 800。本地事務提交前,嘗試拿該記錄的 全域性鎖 ,tx1 全域性提交前,該記錄的全域性鎖被 tx1 持有,tx2 需要重試等待全域性鎖 。
可以看到 tx2 的修改被阻塞了,之後重試拿到全域性鎖之後就能提交然後釋放本地鎖。
如果 tx1 的二階段全域性回滾,則 tx1 需要重新獲取該資料的本地鎖,進行反向補償的更新操作,實現分支的回滾。
此時,如果 tx2 仍在等待該資料的全域性鎖,同時持有本地鎖,則 tx1 的分支回滾會失敗。分支的回滾會一直重試,直到 tx2 的全域性鎖等鎖超時,放棄全域性鎖並回滾本地事務釋放本地鎖,tx1 的分支回滾最終成功。
因為整個過程全域性鎖在 tx1 結束前一直是被 tx1 持有的,所以不會發生髒寫的問題。
然後 AT 模式預設全域性是讀未提交的隔離級別,如果應用在特定場景下,必需要求全域性的讀已提交 ,可以通過 SELECT FOR UPDATE 語句的代理。
當然前提是你本地事務隔離級別是讀已提交及以上。
AT 模式小結
可以看到通過代理來無侵入的得到資料的前後映象,組裝成回滾日誌伴隨本地事務一起提交,解決了兩階段的同步阻塞問題。
並且利用全域性鎖來實現寫隔離。
為了總體效能的考慮,預設是讀未提交隔離級別,只代理了 SELECT FOR UPDATE 來進行讀已提交的隔離。
這其實就是兩階段提交的變體實現。
TCC 模式
沒什麼花頭,就是我們們上面分析的需要搞三個方法, 然後把自定義的分支事務納入到全域性事務的管理中
我貼一張官網的圖應該挺清晰了。
Saga 模式
這個 Saga 是 Seata 提供的長事務解決方案,適用於業務流程多且長的情況下,這種情況如果要實現一般的 TCC 啥的可能得巢狀多個事務了。
並且有些系統無法提供 TCC 這三種介面,比如老專案或者別人公司的,所以就搞了個 Saga 模式,這個 Saga 是在 1987 年 Hector & Kenneth 發表的論⽂中提出的。
那 Saga 如何做呢?來看下這個圖。
假設有 N 個操作,直接從 T1 開始就是直接執行提交事務,然後再執行 T2,可以看到就是無鎖的直接提交,到 T3 發現執行失敗了,然後就進入 Compenstaing 階段,開始一個一個倒回補償了。
思想就是一開始蒙著頭幹,別慫,出了問題我們們再一個一個改回去唄。
可以看到這種情況是不保證事務的隔離性的,並且 Saga 也有 TCC 的一樣的注意點,需要空補償,防懸掛和冪等。
而且極端情況下會因為資料被改變了導致無法回滾的情況。比如第一步給我打了 2 萬塊錢,我給取出來花了,這時候你回滾,我賬上餘額已經 0 了,你說怎麼辦嘛?難道給我還搞負的不成?
這種情況只能在業務流程上入手,我寫程式碼其實一直是這樣寫的,就拿買皮膚的場景來說,我都是先扣錢再給皮膚。
假設先給皮膚扣錢失敗了不就白給了嘛?這錢你來補啊?你覺得使用者會來反饋說皮膚給了錢沒扣嘛?
可能有小機靈鬼說我到時候把皮膚給改回去,嘿嘿這種事情確實發生過,嘖嘖,被罵的真慘。
所以正確的流程應該是先扣錢再給皮膚,錢到自己袋裡先,皮膚沒給成功使用者自然而然會找過來,這時候再給他唄,雖說可能你寫出了個 BUG ,但是還好不是個白給的 BUG。
所以說這點在編碼的時候還是得注意下的。
最後
可以看到分散式事務還是會有各種問題,一般分散式事務的實現還是隻能達到最終一致性。
極端情況下還是得人工介入,所以做好日誌記錄很關鍵。
還有編碼的業務流程,要往利於公司的方向寫,就例如先拿到使用者的錢,再給使用者東西這個方向,切記。
在上分散式事務之前想想,有沒有必要,能不能改造一下避免分散式事務?
再極端一點,你的業務有沒有必要上事務?
最後個人能力有限,如有紕漏請趕緊聯絡鞭撻我,如果覺得文章不錯還望點個在看支援一下喲。
巨人的肩膀
分散式協議與演算法實戰,韓健
分散式資料庫30講,王磊
seata.io
我是 yes,從一點點到億點點,我們下篇見。