最近試驗(yàn)在產(chǎn)品中使用Redis來完成以前MongoDB做的一些工作,發(fā)現(xiàn)在大量消息采集的場(chǎng)景下(咱們這次不談查詢什么的),,redis比mongoDB表現(xiàn)更好──這里主要是指編程更簡(jiǎn)便,、邏輯更清晰。下面我舉一些小例子說說Redis都為我們解決了什么問題,,技術(shù)上下文關(guān)鍵字:高并發(fā),、分布式,。
插入與更新操作的無差別性
Redis的所有SET(包括MSET,HMSET)操作都是:存在則更新,,不存在則插入,即insert if not exists,。所以在編程的時(shí)候開發(fā)人員不需要關(guān)心所做的操作屬于更新還是插入,,減免了判斷,因此也避免了判斷操作可能帶來的鎖定,。
MongoDB也有同樣的操作,,update操作的upsert參數(shù)調(diào)為True即可,,不過經(jīng)過測(cè)試,MongoDB為查詢條件為了索引后使用update with upsert來代替insert操作,效率比光insert要低5倍以上,,而redis的HMSET操作的效率要?jiǎng)俪觥?/p>
GETSET的妙用
上一個(gè)經(jīng)驗(yàn)雖說可以解決這條數(shù)據(jù)該“插入還是更新”的問題,,但需要知道當(dāng)前操作是否針對(duì)某數(shù)據(jù)的首次操作的需求還不少。例如我的程序會(huì)在不同時(shí)間接收到同一條消息的不同分片信息包,,我需要在收到該消息的首個(gè)信息包(發(fā)送是無序的)時(shí)做些特殊處理,。
早些時(shí)候的做法是為消息在MongoDB維護(hù)一個(gè)狀態(tài)對(duì)象,有信息包來的時(shí)候就走“上鎖->檢查消息狀態(tài)->根據(jù)狀態(tài)決定做不做特殊操作->解鎖” 這個(gè)流程,,雖然同事已經(jīng)把鎖的粒度控制得非常細(xì)了,,但有鎖的程序遇上多實(shí)例部署就歇了。
Redis的GETSET是解決這個(gè)問題的終極武器,,只需要把當(dāng)前信息包的唯一標(biāo)識(shí)對(duì)指定的狀態(tài)屬性進(jìn)行一次GETSET操作,,再判斷返回值是否為空則知道是否首次操作。GETSET替我們把兩次讀寫的操作封裝成了原子操作,,V5啊,。
山寨版數(shù)據(jù)過期策略
我曾經(jīng)想過要寫服務(wù)器端的腳本來擴(kuò)展redis,試圖要拿到數(shù)據(jù)過期的事件,,用來做一些回調(diào)來處理過期數(shù)據(jù),,但很快我發(fā)現(xiàn)這個(gè)不現(xiàn)實(shí)。于是我選擇通過使用排序集合(SORTEDSET)來實(shí)現(xiàn)一個(gè)山寨的數(shù)據(jù)過期策略:需要定時(shí)過期的數(shù)據(jù),,統(tǒng)一添加到一個(gè)排序集合:ZADD expiringKey timestamp data,。在這里我使用了時(shí)間值(毫秒為單位的長(zhǎng)整型)作為數(shù)據(jù)的分?jǐn)?shù),那么很自然的,,早期的數(shù)據(jù)總會(huì)排在集合前面,;然后我寫一個(gè)程序會(huì)定時(shí)地過來打理這些過期的數(shù)據(jù)就好了。
存儲(chǔ)結(jié)構(gòu)化數(shù)據(jù)
例如有“通訊錄”這樣的數(shù)據(jù),,包含有”name”,”city”,”gender”等8個(gè)屬性,,使用mongoDB保存就很簡(jiǎn)單,創(chuàng)建一個(gè) Document,,設(shè)置屬性后存儲(chǔ)即可,,而Redis本身并非Document型的DB而是Key Value DB,要存儲(chǔ)這種數(shù)據(jù),,還得在Key上面花一點(diǎn)功夫:使用 contact:id:name,contact:id:city,contact:id:gender之類的Key來存儲(chǔ)其對(duì)應(yīng)的值,。當(dāng)然,這只是使用 redis存儲(chǔ)結(jié)構(gòu)化數(shù)據(jù)最原始的辦法,,更建議的辦法是使用Hash存儲(chǔ),,如 hmset contact:id name jeff contact [email protected] gender male。相對(duì)set操作而言,hmset既節(jié)省了存儲(chǔ)空間又提高了存儲(chǔ)效率,。
使用MongoDB來存儲(chǔ)這些數(shù)據(jù)是小菜一碟,,但鑒于第一點(diǎn)經(jīng)驗(yàn),我還是愿意使用Redis,。
比較可惜的是,,目前Redis的Hash存儲(chǔ)僅支持字符類型的值,不支持其他數(shù)據(jù)結(jié)構(gòu),,我非常期待它日后會(huì)支持其他數(shù)據(jù)結(jié)構(gòu),,甚至支持Hash的嵌套。關(guān)于這點(diǎn),,@wuvist 同學(xué)認(rèn)為十分有可能,。
小結(jié)
上面這些Case都只是Redis牛刀小用,但實(shí)際上給程序帶來的便利是非常明顯的,,最明顯的就是可以把原來的程序上使用的鎖都拋棄掉,,甚至直接支持分布式運(yùn)行和水平擴(kuò)展了。
順便在此小結(jié)一點(diǎn)高并發(fā)分布式應(yīng)用程序編寫的一些推薦的注意事項(xiàng)吧,,當(dāng)然這是我的個(gè)人偏好并結(jié)合了一些特定業(yè)務(wù)領(lǐng)域的性質(zhì):
1. 程序?qū)Y源最好是只讀或只寫,,明確分工。不要在一個(gè)程序里同時(shí)對(duì)資源進(jìn)行讀寫,,除非是原子操作,,如GETSET。
2. 寫操作中,,插入與更新最好是無差別的,,避免程序?qū)Υ诉M(jìn)行行判斷,破壞操作的原子性,。
3. 更新過程中盡最不要對(duì)更新值和原值進(jìn)行比較,,還是關(guān)乎操作的原子性,如果真要進(jìn)行比較,,有兩種方案供參考,。
1). 更新時(shí),為字段追加新數(shù)據(jù),,使用集合(如果是數(shù)值使用排序集合更好)來存儲(chǔ),;比較的邏輯交給讀取的程序處理。
2). 使用CAS,,類似樂觀鎖,,實(shí)現(xiàn)多進(jìn)程數(shù)據(jù)安全控制。如果目標(biāo)資源的服務(wù)器支持最佳,。
4. 還是那一句,,避免在程序里面使用鎖。逼不得已就用分布式鎖吧,。
5. 多線程是萬惡之源,,要慎用,一條線程能把CPU跑滿才是真牛,,多核,、擴(kuò)容時(shí)可考慮多進(jìn)程。