在處理實時數據的過程中需要緩存的參與,由於在更新實時數據時併發處理的特點,因此在更新實時數據時經常產生新老數據相互覆蓋的情況,針對這個情況調查了Redis事務和Lua腳本後,發現Redis事務並不能很好的滿足該場景的業務需要,必須藉助Lua腳本執行原子化的操作才能在理論上解決數據更新的準確性問題。 ...
摘要
在處理實時數據的過程中需要緩存的參與,由於在更新實時數據時併發處理的特點,因此在更新實時數據時經常產生新老數據相互覆蓋的情況,針對這個情況調查了Redis事務和Lua腳本後,發現Redis事務並不能很好的滿足該場景的業務需要,必須藉助Lua腳本執行原子化的操作才能在理論上解決數據更新的準確性問題。
實時數據處理過程中遇到的問題
在處理實時數據的過程中,經常使用Redis存取數據執行CAS(check and set)操作。一般做法是先從Redis中獲取到目標數據,然後根據數據的特征指標判斷是應該更新還是放棄更新。在實時數據流量較小時這個辦法簡單粗暴的解決了數據更新的邏輯問題,但是面對上傳頻率較高的場景或者在更新實時數據時同步更新相關數據的彙總值時就會經常面臨更新時新數據被老數據覆蓋的問題,而且問題的出現具有隨機性,無法有效解決數據的緩存準確性的要求。
此過程中的調用示意圖:
發送命令請求,獲取時間戳
Caller ----------------------------------------> Redis
發送時間戳給客戶端
Caller <---------------------------------------- Redis
發送更新指令
Caller ----------------------------------------> Redis
返回執行結果
Caller <---------------------------------------- Redis
由上圖可見在獲取時間戳到發送更新指令之前由於不是原子操作,因此存在數據被更新的可能。在解決這個問題的時候不禁會想:“如果這個場景發生在資料庫中會怎樣?”
如果在資料庫中,可以使用語句中的where條件來限制SQL語句的執行,做到按條件執行的目的。
上述思想可以用偽代碼表達為:
UPDATE 終端狀態 set status='XXXXX' where code='XXXX' and 時間戳>timestamp
這是個典型的先讀後寫的操作,該語句在資料庫中以鎖的方式保證了處理串列化和操作的原子性。
那麼,問題是:Redis事務中能不能做到?
Redis事務
Redis的事務由四個關鍵命令構成:MULTI、EXEC、DISCARD和WATCH構成。
名稱 | 作用 |
---|---|
MULTI | 聲明開啟事務通道 |
EXEC | 開始批量執行 |
DISCARD | 放棄執行之前發生的命令 |
WATCH | 監聽某個KEY的變化 |
Redis事務的基本執行方式是:
- 使用
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+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腳本中不要編寫太複雜的操作,應該以儘量簡單的邏輯完成整個操作過程,避免因為腳本的執行產生阻塞效應。