有一類業務場景:
(1)超高吞吐量,每秒要處理海量請求;
(2)寫多讀少,大部分請求是對數據進行修改,少部分請求對數據進行讀取;
這類業務,有什麼實現技巧麼?
接下來,一塊兒聽我從案例入手,娓娓道來。數據庫
(1)司機地理位置信息會隨時變化,可能
每幾秒鐘地理位置要修改一次
;
(2)用戶打車的時候查看某個司機的地理位置,
查詢地理位置的頻率相對較低
;
void SetDriverInfo(long driver_id, DriverInfo info);
DriverInfo GetDriverInfo(long driver_id);
(2)返回value也定長,例如:司機實體序列化後的二進制串;
Map<driver_id, DriverInfo>
這個kv內存緩存是一個臨界資源,對它的併發訪問,有什麼注意事項麼?
void SetDriverInfo(long driver_id, DriverInfo info){緩存
WriteLock (m_lock);微信
Map<driver_id>= info;多線程
UnWriteLock(m_lock);架構
}併發
DriverInfo GetDriverInfo(long driver_id){app
DriverInfo t;運維
ReadLock(m_lock);高併發
t= Map<driver_id>;性能
UnReadLock(m_lock);
return t;
}
假設快狗打車有100w司機同時在線,每一個司機每5秒更新一次經緯度狀態,那麼
每秒就有20w次寫併發操做
。
假設快狗打車日訂單1000w個,平均每秒大概也有300個下單,對應到查詢併發量,大概
每秒1000級別的併發讀操做
。
在這樣的吞吐量下(每秒20w寫,1k讀),
鎖m_lock會成爲潛在瓶頸
,致使Map訪問效率極低。
有什麼潛在的優化方法麼?
鎖衝突之因此嚴重,是由於整個Map共用一把鎖,鎖的粒度太粗。
畫外音:能夠認爲是一個數據庫的「庫級別鎖」。
是否可能進行水平拆分,來下降鎖衝突呢?
答案是確定的。
畫外音:相似於數據庫裏的分庫,把一個庫鎖變成多個庫鎖,來提升併發,下降鎖衝突。
咱們能夠把1個Map水平切分紅N個Map:
void SetDriverInfo(long driver_id, DriverInfo info){
i = driver_id % N; // 水平拆分紅N份,N個Map,N個鎖
WriteLock (m_lock[i]); //鎖第i把鎖
Map[i]<driver_id>= info; // 操做第i個Map
UnWriteLock (m_lock[i]); // 解鎖第i把鎖
}
(1)一個Map變成了N個Map,
每一個Map的併發量,變成了1/N
;
(2)同時,
每一個Map的數據量,變成了1/N
;
有沒有可能,進一步細化鎖粒度,一個元素一把鎖呢?
答案也是確定的。
畫外音:能夠認爲是一個數據庫的「庫級別鎖」,優化爲「行級別鎖」。
不妨設driver_id是遞增生成的,而且假設內存比較大,此時能夠把Map優化成Array,並把鎖的粒度細化到最細的,每一個司機信息一個鎖:
void SetDriverInfo(long driver_id, DriverInfo info){
index = driver_id;
WriteLock (m_lock[index]); //超級大內存,一條記錄一個鎖,鎖行鎖
Array[index]= info; //driver_id就是Array下標
UnWriteLock (m_lock[index]); // 解鎖行鎖
}
這個方案使得鎖衝突降到了最低,但鎖資源大增,在數據量很是大的狀況下,內存每每是裝不下的。
畫外音:數據量比較小的時候,能夠一個元素一把鎖,典型的是鏈接池,每一個鏈接用一把鎖表示鏈接是否可用。
寫多讀少的業務,有一種優化方案:無鎖緩存,將鎖衝突下降到。
若是緩存不加鎖,讀寫吞吐量能夠達到極限,可是多線程對緩存中同一塊定長數據進行寫操做時,
有可能出現不一致的髒數據
。
畫外音:做爲緩存,容許
cache miss
,卻不容許讀髒數據。
(1)線程1對緩存進行操做,對
key
想要寫入
value1
;
(2)線程2對緩存進行操做,對
key
想要寫入
value2
;
(3)不加鎖,線程1和線程2對同一個定長區域進行一個併發的寫操做,
可能每一個線程寫成功一半,致使出現髒數據產生
,最終的結果即不是
value1
也不是
value2
,而是一個亂七八糟的不符合預期的值
value-unexpected
;
併發寫入的數據分別是
value1
和
value2
,讀出的數據是
value-unexpected
,數據被篡改,這本質上是一個數據完整性的問題。
例如:運維如何保證,從中控機分發到上線機上的二進制沒有被篡改?
又例如:即時通信系統中,如何保證接受方收到的消息,就是發送方發送的消息?
發送方除了發送消息自己,還要發送消息的簽名
,接收方收到消息後要校驗簽名,以確保消息是完整的,未被篡改。
加入「簽名」保證數據的完整性以後,讀寫流程須要如何升級?
加上簽名以後,
不但緩存要寫入定長value自己,還要寫入定長簽名
(例如
16bitCRC
校驗):
(1)線程1對緩存進行操做,對
key
想要寫入
value1
,寫入簽名
v1-sign
;
(2)線程2對緩存進行操做,對
key
想要寫入
value2
,寫入簽名
v2-sign
;
(3)若是不加鎖,線程1和線程2對同一個定長區域進行一個併發的寫操做,可能每一個線程寫成功一半,致使出現髒數據產生,最終的結果即不是
value1
也不是
value2
,而是一個亂七八糟的不符合預期的值
value-unexpected
,
但簽名,必定是v1-sign或者v2-sign中的任意一個
;
畫外音:16bit/32bit的寫能夠保證原子性。
(4)數據讀取的時候,不但要取出
value
,還要像消息接收方收到消息同樣,校驗一下簽名,若是發現簽名不一致,緩存則返回
NULL
,即
cache miss
;
固然,對應到司機地理位置,除了內存緩存以前,確定須要timer對緩存中的數據按期落盤,寫入數據庫,若是cache miss,能夠從數據庫中讀取數據。
總結
(2)Map轉Array的方式來最小化鎖衝突,一條記錄一個鎖;
(4)經過簽名的方式保證數據的完整性,實現無鎖緩存;
若是你喜歡本文,大機率會喜歡這個架構訓練營,歡迎一塊兒來玩。