基於Lua腳本解決實時數據處理流程中的關鍵問題

摘要

在處理實時數據的過程當中須要緩存的參與,因爲在更新實時數據時併發處理的特色,所以在更新實時數據時常常產生新老數據相互覆蓋的狀況,針對這個狀況調查了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事務

Redis的事務由四個關鍵命令構成:MULTI、EXEC、DISCARD和WATCH構成。lua

名稱 做用
MULTI 聲明開啓事務通道
EXEC 開始批量執行
DISCARD 放棄執行以前發生的命令
WATCH 監聽某個KEY的變化

Redis事務的基本執行方式是:code

  1. 使用WATCH聲明監聽某個KEY值的變化
  2. 發送MULTI命令
  3. 發送事務中須要執行的指令
  4. 發送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+Lua腳本

Redis 2.6+都集成了Lua腳本。經過內嵌對於Lua的支持,Redis解決了長久以來不能高效處理CAS的缺點。在Redis中執行Lua腳本主要涉及到兩個關鍵命令:EVALEVALSHA,另外還有個輔助的命令SCRIPT EXISTS sha1 sha2 ... shaN能夠用於查詢腳本是否已經緩存。

名稱 做用
EVAL 執行某個客戶端傳入的腳本
EVALSHA 執行某個已經在Redis Server中緩存的腳本

使用Lua腳本執行CAS操做的基本步驟:

  1. 客戶端發送EVAL 腳本 參數...命令
  2. 服務端執行腳本
    • 獲取用於進行判斷的鍵值
    • 判斷是否應該更新
    • 執行/放棄更新
  3. 返回腳本執行結果

其中第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

使用Lua腳本的調用示意圖

發送腳本
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

參考資料

  1. Lua 腳本
  2. EVAL script numkeys key [key ...] arg [arg ...]
  3. Redis Transactions
相關文章
相關標籤/搜索