首頁 > 軟體

Redis並行存取問題詳細講解

2022-12-03 14:01:39

前言

我們在使用Redis的過程中,難免會遇到並行存取及資料更新的問題。但很多場景對資料的並行修改是很敏感的,比如庫存資料如果沒有做好並行讀取和更新的版本控制,就會導致嚴重的業務問題。今天就來說說應該如何做好並行存取及資料更新問題。

什麼場景需要控制並行存取

需要控制並行存取,說明這些並行的存取可能會對其他的存取造成影響。比如上面提到的庫存問題,若同一時期有多個使用者端存取商品A的庫存資料,並且可能要更更新庫存資料,這時候就需要對並行存取進行控制了。

說到底,並行存取需要控制的就是對資料的更新動作。 一般來說,使用者端要進行資料更新時可分為2個步驟:

  • 使用者端讀取Redis資料到本地。
  • 確認資料後,修改Redis的資料。

單個存取來看,這個過程並沒什麼問題。但是並行多了,一分為二的過程就會造成資料錯誤的問題。這裡還是用庫存的例子來說:

  • 時間a::使用者端1讀取到庫存=10,我們需要對庫存+1=11的操作。
  • 時間b:使用者端2讀取到的庫存也是10,這次要對庫存-1=9的操作。
  • 時間c:使用者端1將+1後的值11寫回到Redis中。
  • 時間d:使用者端2將-1後的值9寫回到Redis中。

這樣下來,很明顯能發現庫存資料錯了。10+1-1 = 10,正確庫存是10,而上述場景最後為9。

由此可見,這個一分為二的操作不具有原子性,從而產生了錯誤的結果。型別這種場景很多,因此我們需要對這些並行存取的場景加以控制。

並行存取的控制方法

Redis並行存取的控制,總的來說有2種方式。分別是加入鎖機制和讓一系列操作原子化。

1、加入鎖機制

首先第一點,加入鎖機制是很常見的解決方案。簡單來說就是一個使用者端存取資料之前,先要獲取鎖,等資料操作完之後再解鎖。而在這個使用者端擁有鎖的過程中,其他使用者端如果也想存取修改該資料,必須得等鎖釋放了之後,獲取到了鎖才行。

加鎖這個方案是可以解決並行存取的資料準確問題,但放在redis這個場景中並不是很好。首先,Redis作為快取本身並行存取就很多,頻繁的加鎖解鎖,會大大降低redis的存取效能;然後,Redis的使用者端在要加鎖時,需要用到分散式鎖。我們又得用額外的精力去維護這個分散式鎖。

2、操作原子化

操作原子化,也就是讓要執行的一系列動作都保持原子性操作。它的優點就是不需要加入額外的鎖機制。並行的資料準確性達到了,對Redis的效能也不會有太大的影響。

Redis要實現原子操作,總結有2種方式:

  • 單命令操作:也就是Redis中的INCR、HINCRBY等命令,直接將簡單的加減操作合成一個命令執行;
  • Lua指令碼:藉助Lua指令碼,讓多個操作在Lua指令碼上實現原子性操作。

1.單命令操作

首先,單命令操作,將數值的加減直接用Redis命令來執行。像string的加減可用INCR、DECR操作,hash列表field的加減可用HINCRBY操作。

比如下面截圖,兩個使用者端在不同時刻讀取的linux_pids a值為4,各自+1、-1後a值為4。結果是正確的。

由此可見,用Redis的INCR、DECR等命令可以解決數值簡單增減的並行場景。但如果我們對資料的更新不僅僅是簡單的加減操作時,Redis的這些命令就無能為力了。此時我們可以考慮另一種方案:Lua指令碼。

2.Lua指令碼

Lua語言是由C寫的,因此支援多平臺和系統。從Redis2.6開始,Redis就內建了Lua直譯器,我們能直接用Redis使用者端來執行lua指令碼。

我們可以將需要執行的一系列操作用Lua指令碼寫好,然後用Redis執行它。Lua指令碼的方法能保證原子性操作的原因是:Redis會將Lua指令碼一次性執行,也就是說執行Lua指令碼是0-1的操作,要麼成功,要麼失敗。可以理解成MySQL的事務特性。

Redis使用lua指令碼有2種方式:

  • 使用者端中使用:用到script load指令碼內容、evalsha等命令
  • 執行直接執行lua指令碼

我們一般用第二種方式來執行。

使用者端使用方法:

先用script load載入指令碼命令,再用evalsha執行載入得到的sha1值。

127.0.0.1:6379> script load "return 'hello'"
"1b936e3fe509bcbc9cd0664897bbe8fd0cac101b"
127.0.0.1:6379> evalsha "1b936e3fe509bcbc9cd0664897bbe8fd0cac101b" 0
"hello"

再來看看Redis使用Lua指令碼的語法:

redis-cli --eval {lua_path} KEYS[1] KEYS[2]... , ARGV[1] ARGV[2]...
 
--eval:          執行lua指令碼的命令
{lua_path}:         lua指令碼的路徑
KEYS[1] KEYS[2]:       lua指令碼中要操作的redis鍵,我們可以在lua指令碼中用KEYS[1],KEYS[2],KEYS[3]指定多個
ARGV[1] ARGV[2]:    傳入到lua指令碼的引數,在指令碼中用ARGV[1],ARGV[2]...來獲取。

Redis使用lua指令碼的場景很多,最經典的案例當屬利用lua來控制某個IP的存取頻率了。比如說需要防止惡意存取網站的行為,我們規定1分鐘記憶體取次數不能超過30次,實現的方法有很多,比如說漏桶方案、令牌桶方案,但使用最多的還是Redis+lua的分散式限流方案。

我們用lua指令碼(test_lua.script)來簡單實現一下上述功能,就是1分鐘內若存取次數超過30,直接攔截,否則存取次數+1:

-- 限流的key
local limit_key = KEYS[1]
-- 限流次數
local limit_nums = 30
-- 當前存取次數
local current_num = tonumber(redis.call('get', limit_key) or 0)
-- 超出限流次數
if current_num + 1 > limit_num
    then
    return '超出存取次數'
-- 沒有超出限流數,存取次數+1
else
  redis.call("INCRBY", limit_key, "1")
  -- 第一次存取,設定過期時間
  if current_num == 0 then
    redis.call("expire", limit_key, "60")
  return current + 1
end

用Redis執行,命令如下:

redis-cli --eval test_lua.script limit_key

小結

本文介紹了Redis並行存取的控制問題,以及如何保證並行操作的原子化。原子化操作可通過單命令操作和Lua指令碼的方式實現。我們在應對相關問題時,可根據需要選擇對應方案解決之。

到此這篇關於Redis並行存取問題詳細講解的文章就介紹到這了,更多相關Redis並行存取內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com