消息順序性,究竟為什么這么難?
?很多業(yè)務都需要考慮消息投遞的順序性:
- 單聊消息投遞,保證發(fā)送方發(fā)送順序與接收方展現(xiàn)順序一致;
- 群聊消息投遞,保證所有接收方展現(xiàn)順序一致;
- 充值支付消息,保證同一個用戶發(fā)起的請求在服務端執(zhí)行序列一致;
消息順序性是分布式系統(tǒng)架構(gòu)設計中非常難的問題,有什么常見優(yōu)化實踐呢?
折衷一:以客戶端或者服務端的時序為準
不管什么情況,都需要一個標尺來衡量時序的先后順序,可以根據(jù)業(yè)務場景,以客戶端或者服務端的時間為準,例如:
(1) 郵件展示順序,其實是以客戶端發(fā)送時間為準的;
畫外音:發(fā)送方只要將郵件協(xié)議里的時間調(diào)整為1970年或者2970年,就可以在接收方收到郵件后一直“置頂”或者“置底”。
(2) 秒殺活動時間判斷,肯定得以服務器的時間為準,不可能讓客戶端修改本地時間,就能夠提前秒殺;
折衷二:服務端生成單調(diào)遞增id作為時序依據(jù)
對于嚴格時序的業(yè)務場景,可以利用單點寫db的seq/auto_inc_id生成單調(diào)遞增的id,來保證順序性。
畫外音:這個生成id的單點容易成為瓶頸。
折衷三:假如業(yè)務能接受誤差不大的趨勢遞增id
消息發(fā)送、帖子發(fā)布時間、甚至秒殺時間都沒有這么精準時序的要求:
- 同1s內(nèi)發(fā)布的聊天消息時序亂了,沒事;
- 同1s內(nèi)發(fā)布的帖子排序不對,沒事;
- 用1s內(nèi)發(fā)起的秒殺,由于服務器多臺之間時間有誤差,落到A服務器的秒殺;成功了,落到B服務器的秒殺還沒開始,業(yè)務上也是可以接受的(用戶感知不到)
所以,大部分業(yè)務,長時間趨勢遞增的時序就能夠滿足業(yè)務需求,非常短時間的時序誤差一定程度上能夠接受。
于是,可以使用分布式id生成算法來生成id,作為時序依據(jù)。
折衷四:利用單點序列化,可以保證多機相同時序
數(shù)據(jù)為了保證高可用,需要做到進行數(shù)據(jù)冗余,同一份數(shù)據(jù)存儲在多個地方,怎么保證這些數(shù)據(jù)的修改消息是一致的呢?
“單點序列化”是可行的:
- 先在一臺機器上序列化操作;
- 再將操作序列分發(fā)到所有的機器,以保證多機的操作序列是一致的,最終數(shù)據(jù)是一致的;
典型場景一:數(shù)據(jù)庫主從同步
數(shù)據(jù)庫的主從架構(gòu),上游分別發(fā)起了op1,op2,op3三個操作,主庫master來序列化所有的SQL寫操作op3,op1,op2,然后把相同的序列發(fā)送給從庫slave執(zhí)行,以保證所有數(shù)據(jù)庫數(shù)據(jù)的一致性,就是利用“單點序列化”這個思路。
典型場景二:GFS中文件的一致性
GFS(Google File System)為了保證文件的可用性,一份文件要存儲多份,在多個上游對同一個文件進行寫操作時,也是由一個主chunk-server先序列化寫操作,再將序列化后的操作發(fā)送給其他chunk-server,來保證冗余文件的數(shù)據(jù)一致性的。
單對單聊天,怎么保證發(fā)送順序與接收順序一致呢?
單人聊天的需求,發(fā)送方A依次發(fā)出了msg1,msg2,msg3三個消息給接收方B,這三條消息能否保證顯示時序的一致性(發(fā)送與顯示的順序一致)?
方案設計思路如下:
- 如果利用服務器單點序列化時序,可能出現(xiàn)服務端收到消息的時序為msg3,msg1,msg2,就會與發(fā)出序列不一致。
- 業(yè)務上不需要全局消息一致,只需要對于同一個發(fā)送方A,ta發(fā)給B的消息時序一致,常見優(yōu)化方案,在A往B發(fā)出的消息中,加上發(fā)送方A本地的一個絕對時序,來表示接收方B的展現(xiàn)時序。
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會先展現(xiàn),后收到msg1和msg2后,會展現(xiàn)在msg3的前面。
群聊消息,怎么保證各接收方收到順序一致?
群聊消息的需求,N個群友在一個群里聊,怎么保證所有群友收到的消息顯示時序一致?
方案設計思路如下:
- 假設和單聊消息一樣,利用發(fā)送方的seq來保證時序,因為發(fā)送方不單點,seq無法統(tǒng)一生成,可能存在不一致。
- 于是,可以利用服務器的單點做序列化。
如上圖,此時群聊的發(fā)送流程為:
- sender1發(fā)出msg1,sender2發(fā)出msg2;
- msg1和msg2經(jīng)過接入集群,服務集群;
- service層到底層拿一個唯一seq,來確定接收方展示時序;
- service拿到msg2的seq是20,msg1的seq是30;
- 通過投遞服務將消息給多個群友,群友即使接收到msg1和msg2的時間不同,但可以統(tǒng)一按照seq來展現(xiàn);
這個方法能實現(xiàn),所有群友的消息展示時序相同。
缺點是,生成全局遞增序列號的服務很容易成為系統(tǒng)瓶頸。
還有沒有進一步的優(yōu)化方法呢?
群消息其實也不用保證全局消息序列有序,而只要保證一個群內(nèi)的消息有序即可,這樣的話,“id串行化”就成了一個很好的思路。
這個方案中,service層不再需要去一個統(tǒng)一的后端拿全局seq,而是在service連接池層面做細小的改造,保證一個群的消息落在同一個service上,這個service就可以用本地seq來序列化同一個群的所有消息,保證所有群友看到消息的時序是相同的。
此時利用本地時鐘來生成seq就湊效了,是不是很巧妙?
總結(jié)
- 要“有序”,先得有衡量“有序”的標尺,可以是客戶端標尺,可以是服務端標尺;
- 大部分業(yè)務能夠接受大范圍趨勢有序,小范圍誤差;絕對有序的業(yè)務,可以借助服務器絕對時序的能力;
- 單點序列化,是一種常見的保證多機時序統(tǒng)一的方法,典型場景有db主從一致,gfs多文件一致;
- 單對單聊天,只需保證發(fā)出的時序與接收的時序一致,可以利用客戶端seq;
- 群聊,只需保證所有接收方消息時序一致,需要利用服務端seq,方法有兩種,一種單點絕對時序,另一種id串行化;
思路比結(jié)論更重要,希望大家有收獲。