一.概述
分布式系統存在網路,時鐘,以及許多不可預測的故障,分布式事務,一致性與共識問題,迄今為止仍沒有得到很好的解決方案,要想完美地解決分布式系統中的問題不太可能,但是實踐中應對特定問題仍有許多可靠的解決方案,本文不會談及諸如BASE, CAP, ACID 等空泛的理論,只基于實踐中遇到的問題提出可行的解決方案,
二.常見問題
1.讀自己的寫
現象: 用戶在發布頁發布了帖子,然后訪問自己的主頁查看帖子串列,并沒有馬上看到自己剛剛發布的帖子,等待1~2s后才看到
分析:后端db采取主從結構,復制任務在負載較高的情況下會有延遲,用戶讀取帖子串列查詢的是從節點,所以無法及時看到剛剛發布的帖子,一般情況下延遲1~2s是可以接受的,但是為了更好的體驗,可以做一些改進,

解決方案:
- 如果用戶讀取的是自己的主頁,就訪問主節點,如果訪問是他人的主頁,就訪問從節點,只需要在db層路由即可,
- 客戶端還可以記住最近更新時的時間戳,并附帶在讀請求中,據此資訊,系統可以確保對該用戶提供讀服務時都應該至少包含了該時間戳的更新,如果不夠新,要么交由另一個副本來處理,要么等待直到副本接收到了最近的更新
2.單調讀
現象:用戶查看某個帖子下面的評論,一會兒看到5條評論,一會兒看到6條評論,
分析:后端db采取主從結構,復制任務在負載較高的情況下會有延遲,用戶讀取評論串列查詢的是從節點,但是兩次讀的是不同的從節點,當某個從節點具有明顯延遲就會出現資料反復的現象,

解決方案:
- 確保同一個用戶每次都是讀取同一個副本,可以在db層進行路由,這是一種典型的sticky 請求路由,
????replica = hash(user_id) % number_of_replica
3.負載傾斜與熱點問題
現象:某個磁區的資料明顯比其他磁區多,并且訪問頻率高,負載壓力大,
分析:在某些特殊的業務場景下,比如官方或者名人賬號有百萬粉絲,當這些賬號發布訊息事件時,人們會對該訊息進行評論,如果評論資料存盤使用事件id進行hash,就會造成某個磁區的負載產生傾斜,
解決:
- ??在關鍵詞,比如訊息事件id,的開頭或者結尾添加一個亂數,只需一個兩位數的十進制亂數就可以將關鍵字的寫做操作分布到100個不同的關鍵字上,從而分片到不同的磁區上,這些特殊邏輯只應用在一些特殊賬號上,
4.fencing令牌
現象:在采用分布式鎖的情況下,資料庫中的事務重復執行,
分析:在分布式鎖環境中,客戶端A執行事務超時,分布式鎖被釋放,客戶端B執行事務插入資料,客戶端A恢復后繼續執行事務,重復插入資料,

解決方案:
- 這不是分布式事務的范疇,可以采用fencing令牌來解決,我們假設每次鎖服務授予鎖或租約時,同時還會回傳一個fencing令牌,該令牌每授予一次就會遞增,然后,要求客戶端每次向存盤系統發生寫請求時,都必須包含所持有的fencing令牌,當使用zookeeper 作為鎖服務時,可以用事務標識zxid,或節點版本cversion來充當fencing令牌,這兩個都可以滿足單調遞增的要求,

5.Lamport時間戳
現象:客戶端從兩個磁區獲取兩條不同的資料,比如事件a, b;a的序號小于b,但事實上b比a先發生,
分析:常見的有以下幾種非因果序列發生器,產生的序列號與因果關系并不嚴格一致,
- 每個節點單獨產生自己的一組序列號,
- 把墻上時間戳資訊(物理時鐘)附加在每個操作上,
- 預先分配好序列號的區間范圍,比如節點A負責區間1~1000的序列號,節點B負責1001~2000,
解決方案:
- 使用Lamport時間戳,Lamport時間戳是一個kv對(計數器,節點ID),核心流程:每個節點以及每個客戶端都跟蹤迄今為止所見到的最大計數器,并在每個請求中附帶該最大計數器值,當節點收到請求(或者回復)時,如果發現請求內嵌的最大計數器大于節點自身的計數器,則它立即把自己的計數器修改為該最大值,
??
??
6.端到端的重復消除問題
現象:訊息重復是非常普遍的,比如
- 生產者發送訊息到消費者,消費者消費成功后宕機,但是卻沒有更新消費位置,消費者重啟后就會重新消費,
- 常見的rpc呼叫,呼叫方因為網路問題沒有收到被呼叫方的回應,選擇重試,
- 2PC 分布式事務中,因為網路問題,也可能出現重復事務的問題,
- 用戶在頁面重復提交POST請求,
分析:端到端的重復問題是非常普遍的,在TCP 網路中也需要處理重復資料包的問題,有以下兩種解決辦法:
- 最有效的辦法之一是使操作滿足冪等性,即無論執行一次還是多次,確保具有相同的結果,比如以下陳述句無論執行多少次效果都是一致的,
???update table set v = v2 where v = v1
- 可以為操作生成一個唯一的識別符號如(UUID),服務端對此UUID 進行去重校驗,
??
- 在典型的電商下單介面中采用了以上兩種方法的結合:使用唯一識別符號來進行去重,如果寫入例外回傳之前的訂單,
create table order(
# ...
dedup_key varchar(60) not null comment 'key to pretend order duplication',
client_id,
# ...
unique uniq_dedup_key(dedup_key, client_id)
);
@Transactional
Order createOrder(Integer userId, String prodCode, Decimal amount, String dedupKey) {
try {
String orderId = createOrder(userId, prodCode, amount, deupKey); // insert a new order
Order order = getOrderById(orderId); // read order from db
order.setDuplicated(false); // 標記是否有重復下單
return order;
} catch(UniqueKeyViolationException e) {
// if duplicated order has existed, return previous order
Order order = getOrderByDedupKey(dedupKey, clientId);
order.setDuplicated(true);
return order;
} catch (Exception e) {
// hanlde other errors and rollback transaction ...
}
}
7.唯一性約束
現象:在集群高并發的環境下,用戶A創建用戶marquezzzz,用戶B同時創建了用戶marquezzzz,兩者的用戶名相同,這違背了唯一性約束,
分析:創建用戶名的邏輯是,先去db中查詢是否有對應的用戶名(步驟1),如果沒有就創建,如果存在就更新用戶的其他資訊(步驟2),用戶A執行了步驟1, 用戶B執行了步驟1和2,然后用戶A執行了步驟2,這樣生成了兩個同名的用戶,
解決方案:
- 串行化請求,將創建用戶的請求串行化,比如發送到佇列中,這樣可以確保全域唯一性,
- 在db層進行唯一性約束,比如使用唯一索引,考慮到龐大的資料量,性能會下降,如果做了分表,唯一索引的方法也不太可行,
- 使用分布式鎖,比如redis, zookeeper,redis偽代碼如下:
boolean r = redisClient.setnx("userName", currentThread, 10s); // 使用 setnx 原子命令
if (!r) {
return false;
}
// 步驟1 查找db確保沒有重名
// 步驟2 插入用戶
redisClient.delete("userName");
8.時鐘問題
現象:在許多app中,客戶端會上報事件,但是事件的發生時間不準確
分析:app客戶端時鐘可能不準確,或者用戶手動調整過系統時鐘,
解決方案:
為了調整不正確的設備時鐘,一種方法是記錄三個時間戳:
- 根據設備的時鐘,記錄事件發生的時間, device_event_time
- 根據設備的時鐘,記錄將事件發生到服務器的時間, device_send_time
- 根據服務器時鐘,記錄服務器收到事件的時間, server_receive_time
事件真實發生時間 = device_event_time + (server_receive_time - device_send_time)
三.參考
《資料密集型應用系統設計》
https://cloud.tencent.com/developer/article/1121727
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/552394.html
標籤:其他
上一篇:分布式系統常見問題
下一篇:返回列表
