天天看點

消息順序性為何這麼難?

很多業務都需要考慮消息投遞的順序性:

  • 單聊消息投遞,保證發送方發送順序與接收方展現順序一緻
  • 群聊消息投遞,保證所有接收方展現順序一緻
  • 充值支付消息,保證同一個使用者發起的請求在服務端執行序列一緻

消息順序性是分布式系統架構設計中非常難的問題,有什麼常見優化實踐呢?

折衷一:以用戶端或者服務端的時序為準

不管什麼情況,都需要一個标尺來衡量時序的先後順序,可以根據業務場景,以用戶端或者服務端的時間為準,例如:

  • 郵件展示順序,其實是以用戶端發送時間為準的

畫外音:發送方隻要将郵件協定裡的時間調整為1970年或者2970年,就可以在接收方收到郵件後一直“置頂”或者“置底”。

  • 秒殺活動時間判斷,肯定得以伺服器的時間為準,不可能讓用戶端修改本地時間,就能夠提前秒殺

折衷二:服務端生成單調遞增id作為時序依據

對于嚴格時序的業務場景,可以利用單點寫db的seq/auto_inc_id生成單調遞增的id,來保證順序性。

畫外音:這個生成id的單點容易成為瓶頸。

折衷三:假如業務能接受誤差不大的趨勢遞增id

消息發送、文章釋出時間、甚至秒殺時間都沒有這麼精準時序的要求:

  • 同1s内釋出的聊天消息時序亂了,沒事
  • 同1s内釋出的文章排序不對,沒事
  • 用1s内發起的秒殺,由于伺服器多台之間時間有誤差,落到A伺服器的秒殺成功了,落到B伺服器的秒殺還沒開始,業務上也是可以接受的(使用者感覺不到)

是以,大部分業務,長時間趨勢遞增的時序就能夠滿足業務需求,非常短時間的時序誤差一定程度上能夠接受。

于是,可以始終分布式id生成算法來生成id,作為時序依據。

折衷四:利用單點序列化,可以保證多機相同時序

資料為了保證高可用,需要做到進行資料備援,同一份資料存儲在多個地方,怎麼保證這些資料的修改消息是一緻的呢?

“單點序列化”是可行的:

  • 先在一台機器上序列化操作
  • 再将操作序列分發到所有的機器,以保證多機的操作序列是一緻的,最終資料是一緻的

典型場景一:資料庫主從同步

消息順序性為何這麼難?

 資料庫的主從架構,上遊分别發起了op1,op2,op3三個操作,主庫master來序列化所有的SQL寫操作op3,op1,op2,然後把相同的序列發送給從庫slave執行,以保證所有資料庫資料的一緻性,就是利用“單點序列化”這個思路。

 典型場景二:GFS中檔案的一緻性

消息順序性為何這麼難?

GFS(Google File System)為了保證檔案的可用性,一份檔案要存儲多份,在多個上遊對同一個檔案進行寫操作時,也是由一個主chunk-server先序列化寫操作,再将序列化後的操作發送給其他chunk-server,來保證備援檔案的資料一緻性的。

單對單聊天,怎麼保證發送順序與接收順序一緻呢?

單人聊天的需求,發送方A依次發出了msg1,msg2,msg3三個消息給接收方B,這三條消息能否保證顯示時序的一緻性(發送與顯示的順序一緻)?

方案設計思路如下:

(1)如果利用伺服器單點序列化時序,可能出現服務端收到消息的時序為msg3,msg1,msg2,就會與發出序列不一緻。

(2)業務上不需要全局消息一緻,隻需要對于同一個發送方A,ta發給B的消息時序一緻,常見優化方案,在A往B發出的消息中,加上發送方A本地的一個絕對時序,來表示接收方B的展現時序。

msg1{sender:A, seq:10, receiver:B, msg:content1}

msg2{sender:A, seq:20, receiver:B, msg:content2}

msg3{sender:A, seq:30, receiver:B, msg:content3}

消息順序性為何這麼難?

可能存在問題是:如果接收方B先收到msg3,msg3會先展現,後收到msg1和msg2後,會展現在msg3的前面。

 群聊消息,怎麼保證各接收方收到順序一緻?

群聊消息的需求,N個群友在一個群裡聊,怎麼保證所有群友收到的消息顯示時序一緻?

(1)假設和單聊消息一樣,利用發送方的seq來保證時序,因為發送方不單點,seq無法統一生成,可能存在不一緻。

(2)于是,可以利用伺服器的單點做序列化。

消息順序性為何這麼難?

如上圖,此時群聊的發送流程為:

(1)sender1發出msg1,sender2發出msg2;

(2)msg1和msg2經過接入叢集,服務叢集;

(3)service層到底層拿一個唯一seq,來确定接收方展示時序;

(4)service拿到msg2的seq是20,msg1的seq是30;

(5)通過投遞服務講消息給多個群友,群友即使接收到msg1和msg2的時間不同,但可以統一按照seq來展現;

這個方法能實作,所有群友的消息展示時序相同。

缺點是,生成全局遞增序列号的服務很容易成為系統瓶頸。

還有沒有進一步的優化方法呢? 

群消息其實也不用保證全局消息序列有序,而隻要保證一個群内的消息有序即可,這樣的話,“id串行化”就成了一個很好的思路。

消息順序性為何這麼難?

這個方案中,service層不再需要去一個統一的後端拿全局seq,而是在service連接配接池層面做細小的改造,保證一個群的消息落在同一個service上,這個service就可以用本地seq來序列化同一個群的所有消息,保證所有群友看到消息的時序是相同的。

此時利用本地時鐘來生成seq就湊效了,是不是很巧妙?

總結

(1)要“有序”,先得有衡量“有序”的标尺,可以是用戶端标尺,可以是服務端标尺;

(2)大部分業務能夠接受大範圍趨勢有序,小範圍誤差;絕對有序的業務,可以借助伺服器絕對時序的能力;

(3)單點序列化,是一種常見的保證多機時序統一的方法,典型場景有db主從一緻,gfs多檔案一緻;

(4)單對單聊天,隻需保證發出的時序與接收的時序一緻,可以利用用戶端seq;

(5)群聊,隻需保證所有接收方消息時序一緻,需要利用服務端seq,方法有兩種,一種單點絕對時序,另一種id串行化;

思路比結論更重要,希望大家有收獲。

本文轉自“架構師之路”公衆号,58沈劍提供。