在處理實時數據的過程當中須要緩存的參與,因爲在更新實時數據時併發處理的特色,所以在更新實時數據時常常產生新老數據相互覆蓋的狀況,針對這個狀況調查了Redis事務和Lua腳本後,發現Redis事務並不能很好的知足該場景的業務須要,必須藉助Lua腳本執行原子化的操做才能在理論上解決數據更新的準確性問題。html
在處理實時數據的過程當中,常用Redis存取數據執行CAS(check and set)操做。通常作法是先從Redis中獲取到目標數據,而後根據數據的特徵指標判斷是應該更新仍是放棄更新。在實時數據流量較小時這個辦法簡單粗暴的解決了數據更新的邏輯問題,可是面對上傳頻率較高的場景或者在更新實時數據時同步更新相關數據的彙總值時就會常常面臨更新時新數據被老數據覆蓋的問題,並且問題的出現具備隨機性,沒法有效解決數據的緩存準確性的要求。redis
此過程當中的調用示意圖:數據庫
發送命令請求,獲取時間戳 Caller ----------------------------------------> Redis 發送時間戳給客戶端 Caller <---------------------------------------- Redis 發送更新指令 Caller ----------------------------------------> Redis 返回執行結果 Caller <---------------------------------------- Redis
由上圖可見在獲取時間戳到發送更新指令以前因爲不是原子操做,所以存在數據被更新的可能。在解決這個問題的時候不由會想:「若是這個場景發生在數據庫中會怎樣?」緩存
若是在數據庫中,可使用語句中的where條件來限制SQL語句的執行,作到按條件執行的目的。服務器
上述思想能夠用僞代碼表達爲:併發
UPDATE 終端狀態 set status='XXXXX' where code='XXXX' and 時間戳>timestamp
這是個典型的先讀後寫的操做,該語句在數據庫中以鎖的方式保證了處理串行化和操做的原子性。函數
那麼,問題是:Redis事務中能不能作到?atom
Redis的事務由四個關鍵命令構成:MULTI、EXEC、DISCARD和WATCH構成。lua
名稱 | 做用 |
---|---|
MULTI | 聲明開啓事務通道 |
EXEC | 開始批量執行 |
DISCARD | 放棄執行以前發生的命令 |
WATCH | 監聽某個KEY的變化 |
Redis事務的基本執行方式是:code
WATCH
聲明監聽某個KEY值的變化MULTI
命令EXEC指令
,若是監聽到數據在watch後發生變化則放棄提交Redis事務的執行示意圖:
發送命令請求,獲取時間戳 Caller ----------------------------------------> Redis 發送時間戳給客戶端 Caller <---------------------------------------- Redis 發送WATCH指令 Caller ----------------------------------------> Redis 發送MULTI指令 Caller ----------------------------------------> Redis 發送更新指令 Caller ----------------------------------------> Redis 發送EXEC指令 Caller ----------------------------------------> Redis 返回執行結果 Caller <---------------------------------------- Redis
Redis的事務跟數據庫的事務有極大不一樣,其事務實際是由WATCH監聽KEY值的變化加批量執行來完成的,並且事務執行過程當中沒法與客戶端進行交互的。這個事務的實現方式就限制了其所能知足的業務場景,好比本文中遇到的時間戳+實時數據的更新場景中,要求時時刻刻都將最新的數據更新到Redis中,而不是在更新時發現有其餘client搶先更新了目標KEY以後就放棄當前比較新的時間戳的更新權。
那麼,如何才能知足將最新的數據更新到Redis中這個業務需求呢?方法也是有的,那就是使用Lua腳本
Redis 2.6+都集成了Lua腳本。經過內嵌對於Lua的支持,Redis解決了長久以來不能高效處理CAS的缺點。在Redis中執行Lua腳本主要涉及到兩個關鍵命令:EVAL
和EVALSHA
,另外還有個輔助的命令SCRIPT EXISTS sha1 sha2 ... shaN
能夠用於查詢腳本是否已經緩存。
名稱 | 做用 |
---|---|
EVAL | 執行某個客戶端傳入的腳本 |
EVALSHA | 執行某個已經在Redis Server中緩存的腳本 |
使用Lua腳本執行CAS操做的基本步驟:
EVAL 腳本 參數...
命令其中第2步是徹底在服務器端執行,根據Redis官網的描述:
Redis uses the same Lua interpreter to run all the commands. Also Redis guarantees that a script is executed in an atomic way: no other script or Redis command will be executed while a script is being executed.
在Redis Server中執行Lua腳本是一個原子性的操做,時間戳較舊的數據會自動放棄更新緩存數據,所以就能夠保證存入緩存中的數據永遠是最新的,所以也就解決了數據併發更新時老數據被新數據覆蓋的問題。
Lua腳本內部邏輯能夠用僞代碼描述爲:
timestamp=Redis.call('獲取時間戳的指令') if (timestamp==nil) then Redis.call('執行更新') elseif (時間戳>timestamp) then Redis.call('執行更新') end
發送腳本 Caller ----------------------------------------> Redis 爲腳本建立 Lua 函數 Redis ----------------------------------------> Lua 綁定超時處理鉤子 Redis ----------------------------------------> Lua 執行腳本函數 Redis ----------------------------------------> Lua 返回函數執行結果(一個 Lua 值) Redis <---------------------------------------- Lua 將 Lua 值轉換爲 Redis 回覆 並將結果返回給客戶端 Caller <---------------------------------------- Redis
Redis中進行原子化操做有兩個方法:Redis事務或Lua腳本。使用Redis事務只能知足數據在未發生變化進行更新而發生變化就放棄更新的場景。對於實時數據的處理場景來講,Redis的事務沒法知足根據時間戳進行業務處理的須要。因爲Redis執行Lua腳本時是原子化的而且腳本內部能夠編寫讀寫判斷邏輯,所以能夠藉助Lua腳本完成實時數據更新的業務須要。
雖然使用Lua腳本能夠較好的知足業務須要,可是在使用Redis腳本時也有必定的注意事項,Lua腳本中不要編寫太複雜的操做,應該以儘可能簡單的邏輯完成整個操做過程,避免由於腳本的執行產生阻塞效應。
本文連接:http://www.cnblogs.com/zhu-wj/p/7777762.html