使用Redis SortedSet實現增量更新


導讀:前段時間有個需求是提供一個接口供客戶端增量更新數據,當有數據被刪除了以后客戶端也需要感知到,並且要支持一定並發;

關鍵詞:高並發,增量更新

前言

何謂增量更新,顧名思義就是只更新變化的部分,這樣即經濟(尤其對流量敏感型用戶)又高效,比如微信朋友圈,微博的消息,頭條推薦等等。要實現增量更新,首先要解決三個問題,1.如何識別數據的變化,2.如何識別增量更新的起始位置,3.如何感知數據被刪除。

初步分析

首先說說如何識別數據的變化,簡單來說就是每條記錄都需要有一個版本信息,可能是時間戳或者是一個自增的數字,為了簡答起見我選用的是時間戳,但是時間戳在並發極其高的時候可能會重復,從而導致增量更新永遠結束不了,這個需要特別注意,總體來說,整個數據集需要有一個全局遞增的版本號,有了這個前提才能滿足第二點“識別增量更新的起始位置”,這樣很容易就能想到只要每次查詢的時候讓前端把最后一條記錄的版本信息帶上來作為查詢條件就可以滿足需求了,至於第三點,其實就是對數據做邏輯刪除。畫個簡答的時序圖加深理解。

 

 

 直抒胸臆

我分析完之后首先想到的就是使用redis的SortedSet來實現,用member存儲uid,用score來存儲uid的最后更新時間,借助ZRANGEBYSCORE實現增量更新。當用戶信息添加或者修改的時候使用ZADD來修改uid的最后更新時間。刪除的時候就稍微麻煩一些,不僅要修改最后更新時間,還需要將刪除的uid保存起來,使用一個SortedSet實現不了,需要將刪除的uid暫存到另一個SortedSet(set更簡單,為什么不用?)中,這樣在查詢數據的時候判斷下uid是否已被刪除然后打標即可讓前端感知到數據被刪除。

剝繭抽絲

前面說過我會使用ZADD來修改uid對應的score,也就是最后更新時間,那這個score由誰產生呢?第一版使用的是服務器的當前時間(System.currentTimeMillis),似乎一切都很完美,但是跑了幾天以后客戶端反饋說有個用戶的信息已經刪除了,但是客戶端卻沒有感知到,查了服務端的操作日志並沒有發現什么問題,uid的score是最新的,那為什么增量接口沒有感知到呢?后來在分析客戶端請求日志的時候發現了一個細節,客戶端攜帶的version(本質就是個時間戳)居然比有問題uid的score要大,那就好解釋為什么查詢不到了,我是將version作為ZRANGEBYSCORE的min參數來實現增量更新的,如果version已經是最新的了,就不會返回數據,那version怎么就超前了呢?前面說過我是使用服務器的當前時間作為score,有沒有可能程序里取到的時間忽早忽晚呢,說到這兒可能有人不信,但這就是事實,在集群環境下時鍾不是強同步的,通過一個表格來還原現場。

 

原因找到了,怎么解決呢?有人會說,搞單台服務器,對不起,我們畢竟是一個追求高可用的系統。用消息隊列,單線程消費呢?這個倒是可行,但說實話復雜了,能不能讓redis內部消化呢,我們都知道,redis執行指令內部也是排隊的,如果這個score讓redis生成就可以解決問題,帶着問題我查看了redis的api,發現TIME命令可以滿足需求,TIME命令返回兩個字符串,第一個代表當前的秒,第二個代表當前這一秒已經過去的微秒,做一個簡單的計算就就可以得到當前時間對應的微秒,最終的jua腳本如下:

local times=redis.call('TIME') ; 
local score = times[1]*1000000+times[2] //通過計算得到當前時間的微秒
redis.call('zadd',KEYS[2],score,ARGV[1])  

接着再來聊下刪除操作的細節,刪除時有兩步操作要完成,為了確保這兩步操作的原子性,還是要借助lua來實現,最終lua腳本如下:
local times=redis.call('TIME') ;
local score = times[1]*1000000+times[2] ;
redis.call('zadd',KEYS[1],score,ARGV[1]) ;//修改uid最后更新時間
redis.call('zadd',KEYS[2],score,ARGV[1]) ;//插入刪除set

 

查詢時如何給用戶打標,首先通過ZRANGEBYSCORE查詢出一批uid,然后遍歷uid判斷是否已刪除,如果已刪除給uid打標,考慮到性能,這里還是使用lua來實現,最終lua腳本如下:

local signlist = redis.call('zrangebyscore',KEYS[1],ARGV[1],ARGV[2],'WITHSCORES','LIMIT',ARGV[3],ARGV[4]) 
local signTable = {} 
//使用lua腳本判斷uid是否刪除可以避免多次網絡請求(加入將signList返回給服務端,服務端再判斷) for i = 1, #signlist, 2 do local h = {} h['uid'] = signlist[i] h['t']= signlist[i + 1] h['status']=1 if(redis.call('ZRANK',KEYS[2],signlist[i])) then h['status'] =0 //使用ZRANK判斷uid是否在刪除列表中,如果已刪除標志位刪除狀態 end table.insert(signTable,h)
end local result = #signTable>0 and cjson.encode(signTable) or '[]' return result

  

最后的一點優化

截止到這,整個方案可以說介紹完了,不知道細心的你有沒有發現一個問題,那些邏輯刪除的數據終將會成為一顆炸彈,想象一下redis被撐爆的那天,送給自己兩句詩,“待到山花爛漫時,她在叢中笑”。為了避免悲劇,還是早日將優化提到日程上來吧(不知道從哪聽到的一句話,程序員都抱有僥幸心理),怎么優化呢?歸根節點就是要將這部分數據給清除了,但是物理刪除客戶端就感知不到了,似乎陷入了矛與盾的世界中。搞什么增量更新,真是麻煩啊,要不每次拉全量。每次全量絕對不行,倒是可以偶爾拉一次全量,這個偶爾最終被賦予的含義是“如果客戶端兩天沒在線就拉全量數據,否則拉增量數據”,鑒於此客戶端還需要一個最后請求時間(有人會問為什么不用最后一條記錄的更新時間呢?畢竟不是一直有數據被更新啊),這樣服務端就可以將兩天之前邏輯刪除的數據做物理刪除了,前面有個疑問是“存儲刪除用戶的uid用set不是更簡單嗎,為什么用SortedSet”,正好在這解答下,因為我可以很方便的找出兩天之前被刪除的那些uid。

總結

看似一個簡單的需求,當你用心去完成的時候一定會帶給你意想不到的收獲,如果覺得不錯,請點個推薦。

 
        

來我的公眾號與我交流

 




 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM