您好,登錄后才能下訂單哦!
這篇“redis原子操作實例分析”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“redis原子操作實例分析”文章吧。
我們在使用 Redis 時,不可避免地會遇到并發訪問的問題,比如說如果多個用戶同時下單,就會對緩存在 Redis 中的商品庫存并發更新。一旦有了并發寫操作,數據就會被修改,如果我們沒有對并發寫請求做好控制,就可能導致數據被改錯,影響到業務的正常使用(例如庫存數據錯誤,導致下單異常)。
為了保證并發訪問的正確性,Redis 提供了兩種方法,分別是加鎖和原子操作。
加鎖是一種常用的方法,在讀取數據前,客戶端需要先獲得鎖,否則就無法進行操作。當一個客戶端獲得鎖后,就會一直持有這把鎖,直到客戶端完成數據更新,才釋放這把鎖。
看上去好像是一種很好的方案,但是,其實這里會有兩個問題:一個是,如果加鎖操作多,會降低系統的并發訪問性能;第二個是,Redis 客戶端要加鎖時,需要用到分布式鎖,而分布式鎖實現復雜,需要用額外的存儲系統來提供加解鎖操作,我會在下節課向你介紹。
原子操作是另一種提供并發訪問控制的方法。原子操作是指執行過程保持原子性的操作,而且原子操作執行時并不需要再加鎖,實現了無鎖操作。這樣一來,既能保證并發控制,還能減少對系統并發性能的影響。
并發訪問中需要對什么進行控制?
我們說的并發訪問控制,是指對多個客戶端訪問操作同一份數據的過程進行控制,以保證任何一個客戶端發送的操作在 Redis 實例上執行時具有互斥性。例如,客戶端 A 的訪問操作在執行時,客戶端 B 的操作不能執行,需要等到 A 的操作結束后,才能執行。
并發訪問控制對應的操作主要是數據修改操作。當客戶端需要修改數據時,基本流程分成兩步:
客戶端先把數據讀取到本地,在本地進行修改;
客戶端修改完數據后,再寫回 Redis。
我們把這個流程叫做“讀取 - 修改 - 寫回”操作(Read-Modify-Write,簡稱為 RMW 操作)。當有多個客戶端對同一份數據執行 RMW 操作的話,我們就需要讓 RMW 操作涉及的代碼以原子性方式執行。訪問同一份數據的 RMW 操作代碼,就叫做臨界區代碼。
不過,當有多個客戶端并發執行臨界區代碼時,就會存在一些潛在問題,接下來,我用一個多客戶端更新商品庫存的例子來解釋一下。
我們先看下臨界區代碼。假設客戶端要對商品庫存執行扣減 1 的操作,偽代碼如下所示:
current = GET(id) current-- SET(id, current)
可以看到,客戶端首先會根據商品 id,從 Redis 中讀取商品當前的庫存值 current(對應 Read),然后,客戶端對庫存值減 1(對應 Modify),再把庫存值寫回 Redis(對應 Write)。當有多個客戶端執行這段代碼時,這就是一份臨界區代碼。
如果我們對臨界區代碼的執行沒有控制機制,就會出現數據更新錯誤。在剛才的例子中,假設現在有兩個客戶端 A 和 B,同時執行剛才的臨界區代碼,就會出現錯誤,你可以看下下面這張圖。
可以看到,客戶端 A 在 t1 時讀取庫存值 10 并扣減 1,在 t2 時,客戶端 A 還沒有把扣減后的庫存值 9 寫回 Redis,而在此時,客戶端 B 讀到庫存值 10,也扣減了 1,B 記錄的庫存值也為 9 了。等到 t3 時,A 往 Redis 寫回了庫存值 9,而到 t4 時,B 也寫回了庫存值 9。
如果按正確的邏輯處理,客戶端 A 和 B 對庫存值各做了一次扣減,庫存值應該為 8。所以,這里的庫存值明顯更新錯了。
出現這個現象的原因是,臨界區代碼中的客戶端讀取數據、更新數據、再寫回數據涉及了三個操作,而這三個操作在執行時并不具有互斥性,多個客戶端基于相同的初始值進行修改,而不是基于前一個客戶端修改后的值再修改。
為了保證數據并發修改的正確性,我們可以用鎖把并行操作變成串行操作,串行操作就具有互斥性。一個客戶端持有鎖后,其他客戶端只能等到鎖釋放,才能拿鎖再進行修改。
下面的偽代碼顯示了使用鎖來控制臨界區代碼的執行情況,你可以看下。
LOCK() current = GET(id) current-- SET(id, current) UNLOCK()
雖然加鎖保證了互斥性,但是加鎖也會導致系統并發性能降低。
如下圖所示,當客戶端 A 加鎖執行操作時,客戶端 B、C 就需要等待。A 釋放鎖后,假設 B 拿到鎖,那么 C 還需要繼續等待,所以,t1 時段內只有 A 能訪問共享數據,t2 時段內只有 B 能訪問共享數據,系統的并發性能當然就下降了。
和加鎖類似,原子操作也能實現并發控制,但是原子操作對系統并發性能的影響較小,接下來,我們就來了解下 Redis 中的原子操作。
Redis 的兩種原子操作方法
為了實現并發控制要求的臨界區代碼互斥執行,Redis 的原子操作采用了兩種方法:
把多個操作在 Redis 中實現成一個操作,也就是單命令操作;
把多個操作寫到一個 Lua 腳本中,以原子性方式執行單個 Lua 腳本。
我們先來看下 Redis 本身的單命令操作。
Redis 是使用單線程來串行處理客戶端的請求操作命令的,所以,當 Redis 執行某個命令操作時,其他命令是無法執行的,這相當于命令操作是互斥執行的。當然,Redis 的快照生成、AOF 重寫這些操作,可以使用后臺線程或者是子進程執行,也就是和主線程的操作并行執行。不過,這些操作只是讀取數據,不會修改數據,所以,我們并不需要對它們做并發控制。
你可能也注意到了,雖然 Redis 的單個命令操作可以原子性地執行,但是在實際應用中,數據修改時可能包含多個操作,至少包括讀數據、數據增減、寫回數據三個操作,這顯然就不是單個命令操作了,那該怎么辦呢?
別擔心,Redis 提供了 INCR/DECR 命令,把這三個操作轉變為一個原子操作了。INCR/DECR 命令可以對數據進行增值 / 減值操作,而且它們本身就是單個命令操作,Redis 在執行它們時,本身就具有互斥性。
比如說,在剛才的庫存扣減例子中,客戶端可以使用下面的代碼,直接完成對商品 id 的庫存值減 1 操作。即使有多個客戶端執行下面的代碼,也不用擔心出現庫存值扣減錯誤的問題。
DECR id
所以,如果我們執行的 RMW 操作是對數據進行增減值的話,Redis 提供的原子操作 INCR 和 DECR 可以直接幫助我們進行并發控制。
但是,如果我們要執行的操作不是簡單地增減數據,而是有更加復雜的判斷邏輯或者是其他操作,那么,Redis 的單命令操作已經無法保證多個操作的互斥執行了。所以,這個時候,我們需要使用第二個方法,也就是 Lua 腳本。
Redis 會把整個 Lua 腳本作為一個整體執行,在執行的過程中不會被其他命令打斷,從而保證了 Lua 腳本中操作的原子性。如果我們有多個操作要執行,但是又無法用 INCR/DECR 這種命令操作來實現,就可以把這些要執行的操作編寫到一個 Lua 腳本中。
然后,我們可以使用 Redis 的 EVAL 命令來執行腳本。這樣一來,這些操作在執行時就具有了互斥性。
再舉個例子,具體解釋下 Lua 的使用。
當一個業務應用的訪問用戶增加時,我們有時需要限制某個客戶端在一定時間范圍內的訪問次數,比如爆款商品的購買限流、社交網絡中的每分鐘點贊次數限制等。
那該怎么限制呢?我們可以把客戶端 IP 作為 key,把客戶端的訪問次數作為 value,保存到 Redis 中。客戶端每訪問一次后,我們就用 INCR 增加訪問次數。
不過,在這種場景下,客戶端限流其實同時包含了對訪問次數和時間范圍的限制,例如每分鐘的訪問次數不能超過 20。所以,我們可以在客戶端第一次訪問時,給對應鍵值對設置過期時間,例如設置為 60s 后過期。同時,在客戶端每次訪問時,我們讀取客戶端當前的訪問次數,如果次數超過閾值,就報錯,限制客戶端再次訪問。你可以看下下面的這段代碼,它實現了對客戶端每分鐘訪問次數不超過 20 次的限制。
//獲取ip對應的訪問次數 current = GET(ip) //如果超過訪問次數超過20次,則報錯 IF current != NULL AND current > 20 THEN ERROR "exceed 20 accesses per second" ELSE //如果訪問次數不足20次,增加一次訪問計數 value = INCR(ip) //如果是第一次訪問,將鍵值對的過期時間設置為60s后 IF value == 1 THEN EXPIRE(ip,60) END //執行其他操作 DO THINGS END
可以看到,在這個例子中,我們已經使用了 INCR 來原子性地增加計數。但是,客戶端限流的邏輯不只有計數,還包括訪問次數判斷和過期時間設置。
對于這些操作,我們同樣需要保證它們的原子性。否則,如果客戶端使用多線程訪問,訪問次數初始值為 0,第一個線程執行了 INCR(ip) 操作后,第二個線程緊接著也執行了 INCR(ip),此時,ip 對應的訪問次數就被增加到了 2,我們就無法再對這個 ip 設置過期時間了。這樣就會導致,這個 ip 對應的客戶端訪問次數達到 20 次之后,就無法再進行訪問了。即使過了 60s,也不能再繼續訪問,顯然不符合業務要求。
所以,這個例子中的操作無法用 Redis 單個命令來實現,此時,我們就可以使用 Lua 腳本來保證并發控制。我們可以把訪問次數加 1、判斷訪問次數是否為 1,以及設置過期時間這三個操作寫入一個 Lua 腳本,如下所示:
local current current = redis.call("incr",KEYS[1]) if tonumber(current) == 1 then redis.call("expire",KEYS[1],60) end
假設我們編寫的腳本名稱為 lua.script,我們接著就可以使用 Redis 客戶端,帶上 eval 選項,來執行該腳本。腳本所需的參數將通過以下命令中的 keys 和 args 進行傳遞。
redis-cli --eval lua.script keys , args
這樣一來,訪問次數加 1、判斷訪問次數是否為 1,以及設置過期時間這三個操作就可以原子性地執行了。即使客戶端有多個線程同時執行這個腳本,Redis 也會依次串行執行腳本代碼,避免了并發操作帶來的數據錯誤。
以上就是關于“redis原子操作實例分析”這篇文章的內容,相信大家都有了一定的了解,希望小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。