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