> 原文地址 (https://mp.weixin.qq.com/s/BfuRWaB7RDjpGmQbZdmMZw)
有一類業(yè)務(wù)場景:
(1)超高吞吐量拌禾,每秒要處理海量請求;
(2)寫多讀少展哭,大部分請求是對數(shù)據(jù)進(jìn)行修改湃窍,少部分請求對數(shù)據(jù)進(jìn)行讀取匪傍;
這類業(yè)務(wù)您市,有什么實(shí)現(xiàn)技巧么?
接下來役衡,一起聽我從案例入手茵休,娓娓道來。
快狗打車手蝎,場景舉例:
(1)司機(jī)地理位置信息會隨時(shí)變化榕莺,可能每幾秒鐘地理位置要修改一次;
(2)用戶打車的時(shí)候查看某個(gè)司機(jī)的地理位置柑船,查詢地理位置的頻率相對較低帽撑;
這里要用到兩個(gè)接口:
(1)大量修改司機(jī)信息:
void SetDriverInfo(long driver_id, DriverInfo info);
(2)相對少量查詢司機(jī)信息:
DriverInfo GetDriverInfo(long driver_id);
這一類業(yè)務(wù),一般怎么實(shí)現(xiàn)呢鞍时?
具體到底層的實(shí)現(xiàn)亏拉,往往是一個(gè) Map 內(nèi)存緩存:
(1)查詢 key 定長,例如:司機(jī) ID逆巍;
(2)返回 value 也定長及塘,例如:司機(jī)實(shí)體序列化后的二進(jìn)制串;
即锐极,類似這樣的一個(gè) kv 緩存結(jié)構(gòu):
Map<driver_id, DriverInfo>
這個(gè) kv 內(nèi)存緩存是一個(gè)臨界資源笙僚,對它的并發(fā)訪問,有什么注意事項(xiàng)么灵再?
臨界資源的訪問肋层,需要注意加讀寫鎖亿笤,實(shí)施互斥。
以下栋猖,是加鎖寫入的偽代碼:
void SetDriverInfo(long driver_id, DriverInfo info){
WriteLock (m_lock);
Map<driver_id>= info;
UnWriteLock(m_lock);
}
畫外音:假設(shè) info 已經(jīng)序列化净薛。
以下,是加鎖讀取的偽代碼:
DriverInfo GetDriverInfo(long driver_id){
DriverInfo t;
ReadLock(m_lock);
t= Map<driver_id>;
UnReadLock(m_lock);
return t;
}
當(dāng)吞吐量很高時(shí)蒲拉,上述流程可能存在什么問題肃拜?
假設(shè)快狗打車有 100w 司機(jī)同時(shí)在線,每個(gè)司機(jī)每 5 秒更新一次經(jīng)緯度狀態(tài)雌团,那么每秒就有 20w 次寫并發(fā)操作燃领。
假設(shè)快狗打車日訂單 1000w 個(gè),平均每秒大概也有 300 個(gè)下單锦援,對應(yīng)到查詢并發(fā)量猛蔽,大概每秒 1000 級別的并發(fā)讀操作。
在這樣的吞吐量下(每秒 20w 寫雨涛,1k 讀)枢舶,鎖 m_lock 會成為潛在瓶頸,導(dǎo)致 Map 訪問效率極低替久。
有什么潛在的優(yōu)化方法么?
鎖沖突之所以嚴(yán)重躏尉,是因?yàn)檎麄€(gè) Map 共用一把鎖蚯根,鎖的粒度太粗。
畫外音:可以認(rèn)為是一個(gè)數(shù)據(jù)庫的 “庫級別鎖”胀糜。
是否可能進(jìn)行水平拆分颅拦,來降低鎖沖突呢?
答案是肯定的教藻。
畫外音:類似于數(shù)據(jù)庫里的分庫距帅,把一個(gè)庫鎖變成多個(gè)庫鎖,來提高并發(fā)括堤,降低鎖沖突碌秸。
我們可以把 1 個(gè) Map 水平切分成 N 個(gè) Map:
void SetDriverInfo(long driver_id, DriverInfo info){
i = driver_id % N; // 水平拆分成 N 份,N 個(gè) Map悄窃,N 個(gè)鎖
WriteLock (m_lock[i]); // 鎖第 i 把鎖
Map[i]<driver_id>= info; // 操作第 i 個(gè) Map
UnWriteLock (m_lock[i]); // 解鎖第 i 把鎖
}
如此優(yōu)化讥电,能否提高性能?
(1)一個(gè) Map 變成了 N 個(gè) Map轧抗,每個(gè) Map 的并發(fā)量恩敌,變成了 1/N;
(2)同時(shí)横媚,每個(gè) Map 的數(shù)據(jù)量纠炮,變成了 1/N月趟;
所以理論上,鎖沖突會成平方指數(shù)降低恢口,性能會提升狮斗。
有沒有可能,進(jìn)一步細(xì)化鎖粒度弧蝇,一個(gè)元素一把鎖呢碳褒?
答案也是肯定的。
畫外音:可以認(rèn)為是一個(gè)數(shù)據(jù)庫的 “庫級別鎖”看疗,優(yōu)化為 “行級別鎖”沙峻。
不妨設(shè) driver_id 是遞增生成的,并且假設(shè)內(nèi)存比較大两芳,此時(shí)可以把 Map 優(yōu)化成 Array摔寨,并把鎖的粒度細(xì)化到最細(xì)的,每個(gè)司機(jī)信息一個(gè)鎖:
void SetDriverInfo(long driver_id, DriverInfo info){
index = driver_id;
WriteLock (m_lock[index]); // 超級大內(nèi)存怖辆,一條記錄一個(gè)鎖是复,鎖行鎖
Array[index]= info; //driver_id 就是 Array 下標(biāo)
UnWriteLock (m_lock[index]); // 解鎖行鎖
}
這個(gè)方案使得鎖沖突降到了最低,但鎖資源大增竖螃,在數(shù)據(jù)量非常大的情況下淑廊,內(nèi)存往往是裝不下的。
畫外音:數(shù)據(jù)量比較小的時(shí)候特咆,可以一個(gè)元素一把鎖季惩,典型的是連接池,每個(gè)連接用一把鎖表示連接是否可用腻格。
還沒有方法進(jìn)一步降低鎖沖突画拾,提升并發(fā)量呢?
寫多讀少的業(yè)務(wù)菜职,有一種優(yōu)化方案:無鎖緩存青抛,將鎖沖突降低到。
無鎖緩存酬核,可能存在什么問題蜜另?
如果緩存不加鎖,讀寫吞吐量可以達(dá)到極限愁茁,但是多線程對緩存中同一塊定長數(shù)據(jù)進(jìn)行寫操作時(shí)蚕钦,有可能出現(xiàn)不一致的臟數(shù)據(jù)。
這個(gè)方案為了提高性能鹅很,犧牲了一致性嘶居。
讀取時(shí),獲取到了錯(cuò)誤的數(shù)據(jù),是不能接受的邮屁。
畫外音:作為緩存整袁,允許 cache miss,卻不允許讀臟數(shù)據(jù)佑吝。
臟數(shù)據(jù)是如何產(chǎn)生的坐昙?
不加鎖,在多線程并發(fā)寫時(shí)芋忿,可能出現(xiàn)以下情況:
(1)線程 1 對緩存進(jìn)行操作炸客,對 key 想要寫入 value1;
(2)線程 2 對緩存進(jìn)行操作戈钢,對 key 想要寫入 value2痹仙;
(3)不加鎖,線程 1 和線程 2 對同一個(gè)定長區(qū)域進(jìn)行一個(gè)并發(fā)的寫操作殉了,可能每個(gè)線程寫成功一半开仰,導(dǎo)致出現(xiàn)臟數(shù)據(jù)產(chǎn)生,最終的結(jié)果即不是 value1 也不是 value2薪铜,而是一個(gè)亂七八糟的不符合預(yù)期的值 value-unexpected众弓;
如何解決上述問題呢?
本質(zhì)上隔箍,這是一個(gè)數(shù)據(jù)完整性問題谓娃。
并發(fā)寫入的數(shù)據(jù)分別是 value1 和 value2,讀出的數(shù)據(jù)是 value-unexpected鞍恢,數(shù)據(jù)被篡改傻粘,這本質(zhì)上是一個(gè)數(shù)據(jù)完整性的問題。
通常如何保證數(shù)據(jù)的完整性呢?
例如:運(yùn)維如何保證,從中控機(jī)分發(fā)到上線機(jī)上的二進(jìn)制沒有被篡改恼蓬?
md5阿趁。
又例如:即時(shí)通訊系統(tǒng)中,如何保證接受方收到的消息瀑志,就是發(fā)送方發(fā)送的消息涩搓?
發(fā)送方除了發(fā)送消息本身,還要發(fā)送消息的簽名劈猪,接收方收到消息后要校驗(yàn)簽名昧甘,以確保消息是完整的,未被篡改战得。
“簽名” 是一種常見的保證數(shù)據(jù)完整性的方案充边。
加入 “簽名” 保證數(shù)據(jù)的完整性之后,讀寫流程需要如何升級?
加上簽名之后浇冰,不但緩存要寫入定長 value 本身贬媒,還要寫入定長簽名(例如 16bitCRC 校驗(yàn)):
(1)線程 1 對緩存進(jìn)行操作,對 key 想要寫入 value1肘习,寫入簽名 v1-sign际乘;
(2)線程 2 對緩存進(jìn)行操作,對 key 想要寫入 value2漂佩,寫入簽名 v2-sign脖含;
(3)如果不加鎖,線程 1 和線程 2 對同一個(gè)定長區(qū)域進(jìn)行一個(gè)并發(fā)的寫操作投蝉,可能每個(gè)線程寫成功一半养葵,導(dǎo)致出現(xiàn)臟數(shù)據(jù)產(chǎn)生,最終的結(jié)果即不是 value1 也不是 value2墓拜,而是一個(gè)亂七八糟的不符合預(yù)期的值 value-unexpected港柜,但簽名,一定是 v1-sign 或者 v2-sign 中的任意一個(gè)咳榜;
畫外音:16bit/32bit 的寫可以保證原子性夏醉。
(4)數(shù)據(jù)讀取的時(shí)候,不但要取出 value涌韩,還要像消息接收方收到消息一樣畔柔,校驗(yàn)一下簽名,如果發(fā)現(xiàn)簽名不一致臣樱,緩存則返回 NULL靶擦,即 cache miss;
當(dāng)然雇毫,對應(yīng)到司機(jī)地理位置玄捕,除了內(nèi)存緩存之前,肯定需要 timer 對緩存中的數(shù)據(jù)定期落盤棚放,寫入數(shù)據(jù)庫枚粘,如果 cache miss,可以從數(shù)據(jù)庫中讀取數(shù)據(jù)飘蚯。
巧不巧秒馍迄?
總結(jié)
當(dāng)業(yè)務(wù)滿足:
(1)超高并發(fā);
(2)寫多讀少局骤;
(3)定長 value攀圈;
時(shí),可以用以下方法來提升吞吐量:
(1)水平拆分來降低鎖沖突峦甩;
思路:單庫變多庫赘来。
(2)Map 轉(zhuǎn) Array 的方式來最小化鎖沖突,一條記錄一個(gè)鎖;
思路:庫鎖變行鎖撕捍。
(3)無鎖拿穴,最大化并發(fā);
思路:行鎖變無鎖忧风,完整性與性能的折衷默色。
(4)通過簽名的方式保證數(shù)據(jù)的完整性,實(shí)現(xiàn)無鎖緩存狮腿;
思路:寫時(shí)寫簽名腿宰,讀時(shí)校驗(yàn)簽名。