久久精品人人爽,华人av在线,亚洲性视频网站,欧美专区一二三

redis原子操作實(shí)例分析

共計(jì) 4226 個(gè)字符,預(yù)計(jì)需要花費(fèi) 11 分鐘才能閱讀完成。

這篇“redis 原子操作實(shí)例分析”文章的知識(shí)點(diǎn)大部分人都不太理解,所以丸趣 TV 小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細(xì),步驟清晰,具有一定的借鑒價(jià)值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來(lái)看看這篇“redis 原子操作實(shí)例分析”文章吧。

redis 原子操作

我們?cè)谑褂?Redis 時(shí),不可避免地會(huì)遇到并發(fā)訪問(wèn)的問(wèn)題,比如說(shuō)如果多個(gè)用戶(hù)同時(shí)下單,就會(huì)對(duì)緩存在 Redis 中的商品庫(kù)存并發(fā)更新。一旦有了并發(fā)寫(xiě)操作,數(shù)據(jù)就會(huì)被修改,如果我們沒(méi)有對(duì)并發(fā)寫(xiě)請(qǐng)求做好控制,就可能導(dǎo)致數(shù)據(jù)被改錯(cuò),影響到業(yè)務(wù)的正常使用(例如庫(kù)存數(shù)據(jù)錯(cuò)誤,導(dǎo)致下單異常)。

為了保證并發(fā)訪問(wèn)的正確性,Redis 提供了兩種方法,分別是加鎖和原子操作。

加鎖是一種常用的方法,在讀取數(shù)據(jù)前,客戶(hù)端需要先獲得鎖,否則就無(wú)法進(jìn)行操作。當(dāng)一個(gè)客戶(hù)端獲得鎖后,就會(huì)一直持有這把鎖,直到客戶(hù)端完成數(shù)據(jù)更新,才釋放這把鎖。
看上去好像是一種很好的方案,但是,其實(shí)這里會(huì)有兩個(gè)問(wèn)題:一個(gè)是,如果加鎖操作多,會(huì)降低系統(tǒng)的并發(fā)訪問(wèn)性能;第二個(gè)是,Redis 客戶(hù)端要加鎖時(shí),需要用到分布式鎖,而分布式鎖實(shí)現(xiàn)復(fù)雜,需要用額外的存儲(chǔ)系統(tǒng)來(lái)提供加解鎖操作,我會(huì)在下節(jié)課向你介紹。

原子操作是另一種提供并發(fā)訪問(wèn)控制的方法。原子操作是指執(zhí)行過(guò)程保持原子性的操作,而且原子操作執(zhí)行時(shí)并不需要再加鎖,實(shí)現(xiàn)了無(wú)鎖操作。這樣一來(lái),既能保證并發(fā)控制,還能減少對(duì)系統(tǒng)并發(fā)性能的影響。

并發(fā)訪問(wèn)中需要對(duì)什么進(jìn)行控制?
我們說(shuō)的并發(fā)訪問(wèn)控制,是指對(duì)多個(gè)客戶(hù)端訪問(wèn)操作同一份數(shù)據(jù)的過(guò)程進(jìn)行控制,以保證任何一個(gè)客戶(hù)端發(fā)送的操作在 Redis 實(shí)例上執(zhí)行時(shí)具有互斥性。例如,客戶(hù)端 A 的訪問(wèn)操作在執(zhí)行時(shí),客戶(hù)端 B 的操作不能執(zhí)行,需要等到 A 的操作結(jié)束后,才能執(zhí)行。

并發(fā)訪問(wèn)控制對(duì)應(yīng)的操作主要是數(shù)據(jù)修改操作。當(dāng)客戶(hù)端需要修改數(shù)據(jù)時(shí),基本流程分成兩步:

客戶(hù)端先把數(shù)據(jù)讀取到本地,在本地進(jìn)行修改;

客戶(hù)端修改完數(shù)據(jù)后,再寫(xiě)回 Redis。

我們把這個(gè)流程叫做“讀取 – 修改 – 寫(xiě)回”操作(Read-Modify-Write,簡(jiǎn)稱(chēng)為 RMW 操作)。當(dāng)有多個(gè)客戶(hù)端對(duì)同一份數(shù)據(jù)執(zhí)行 RMW 操作的話(huà),我們就需要讓 RMW 操作涉及的代碼以原子性方式執(zhí)行。訪問(wèn)同一份數(shù)據(jù)的 RMW 操作代碼,就叫做臨界區(qū)代碼。

不過(guò),當(dāng)有多個(gè)客戶(hù)端并發(fā)執(zhí)行臨界區(qū)代碼時(shí),就會(huì)存在一些潛在問(wèn)題,接下來(lái),我用一個(gè)多客戶(hù)端更新商品庫(kù)存的例子來(lái)解釋一下。

我們先看下臨界區(qū)代碼。假設(shè)客戶(hù)端要對(duì)商品庫(kù)存執(zhí)行扣減 1 的操作,偽代碼如下所示:

current = GET(id)
current--
SET(id, current)

可以看到,客戶(hù)端首先會(huì)根據(jù)商品 id,從 Redis 中讀取商品當(dāng)前的庫(kù)存值 current(對(duì)應(yīng) Read),然后,客戶(hù)端對(duì)庫(kù)存值減 1(對(duì)應(yīng) Modify),再把庫(kù)存值寫(xiě)回 Redis(對(duì)應(yīng) Write)。當(dāng)有多個(gè)客戶(hù)端執(zhí)行這段代碼時(shí),這就是一份臨界區(qū)代碼。

如果我們對(duì)臨界區(qū)代碼的執(zhí)行沒(méi)有控制機(jī)制,就會(huì)出現(xiàn)數(shù)據(jù)更新錯(cuò)誤。在剛才的例子中,假設(shè)現(xiàn)在有兩個(gè)客戶(hù)端 A 和 B,同時(shí)執(zhí)行剛才的臨界區(qū)代碼,就會(huì)出現(xiàn)錯(cuò)誤,你可以看下下面這張圖。

可以看到,客戶(hù)端 A 在 t1 時(shí)讀取庫(kù)存值 10 并扣減 1,在 t2 時(shí),客戶(hù)端 A 還沒(méi)有把扣減后的庫(kù)存值 9 寫(xiě)回 Redis,而在此時(shí),客戶(hù)端 B 讀到庫(kù)存值 10,也扣減了 1,B 記錄的庫(kù)存值也為 9 了。等到 t3 時(shí),A 往 Redis 寫(xiě)回了庫(kù)存值 9,而到 t4 時(shí),B 也寫(xiě)回了庫(kù)存值 9。

如果按正確的邏輯處理,客戶(hù)端 A 和 B 對(duì)庫(kù)存值各做了一次扣減,庫(kù)存值應(yīng)該為 8。所以,這里的庫(kù)存值明顯更新錯(cuò)了。

出現(xiàn)這個(gè)現(xiàn)象的原因是,臨界區(qū)代碼中的客戶(hù)端讀取數(shù)據(jù)、更新數(shù)據(jù)、再寫(xiě)回?cái)?shù)據(jù)涉及了三個(gè)操作,而這三個(gè)操作在執(zhí)行時(shí)并不具有互斥性,多個(gè)客戶(hù)端基于相同的初始值進(jìn)行修改,而不是基于前一個(gè)客戶(hù)端修改后的值再修改。

為了保證數(shù)據(jù)并發(fā)修改的正確性,我們可以用鎖把并行操作變成串行操作,串行操作就具有互斥性。一個(gè)客戶(hù)端持有鎖后,其他客戶(hù)端只能等到鎖釋放,才能拿鎖再進(jìn)行修改。

下面的偽代碼顯示了使用鎖來(lái)控制臨界區(qū)代碼的執(zhí)行情況,你可以看下。

LOCK()
current = GET(id)
current--
SET(id, current)
UNLOCK()

雖然加鎖保證了互斥性,但是加鎖也會(huì)導(dǎo)致系統(tǒng)并發(fā)性能降低。

如下圖所示,當(dāng)客戶(hù)端 A 加鎖執(zhí)行操作時(shí),客戶(hù)端 B、C 就需要等待。A 釋放鎖后,假設(shè) B 拿到鎖,那么 C 還需要繼續(xù)等待,所以,t1 時(shí)段內(nèi)只有 A 能訪問(wèn)共享數(shù)據(jù),t2 時(shí)段內(nèi)只有 B 能訪問(wèn)共享數(shù)據(jù),系統(tǒng)的并發(fā)性能當(dāng)然就下降了。

和加鎖類(lèi)似,原子操作也能實(shí)現(xiàn)并發(fā)控制,但是原子操作對(duì)系統(tǒng)并發(fā)性能的影響較小,接下來(lái),我們就來(lái)了解下 Redis 中的原子操作。

Redis 的兩種原子操作方法

為了實(shí)現(xiàn)并發(fā)控制要求的臨界區(qū)代碼互斥執(zhí)行,Redis 的原子操作采用了兩種方法:

把多個(gè)操作在 Redis 中實(shí)現(xiàn)成一個(gè)操作,也就是單命令操作;

把多個(gè)操作寫(xiě)到一個(gè) Lua 腳本中,以原子性方式執(zhí)行單個(gè) Lua 腳本。

我們先來(lái)看下 Redis 本身的單命令操作。

Redis 是使用單線(xiàn)程來(lái)串行處理客戶(hù)端的請(qǐng)求操作命令的,所以,當(dāng) Redis 執(zhí)行某個(gè)命令操作時(shí),其他命令是無(wú)法執(zhí)行的,這相當(dāng)于命令操作是互斥執(zhí)行的。當(dāng)然,Redis 的快照生成、AOF 重寫(xiě)這些操作,可以使用后臺(tái)線(xiàn)程或者是子進(jìn)程執(zhí)行,也就是和主線(xiàn)程的操作并行執(zhí)行。不過(guò),這些操作只是讀取數(shù)據(jù),不會(huì)修改數(shù)據(jù),所以,我們并不需要對(duì)它們做并發(fā)控制。

你可能也注意到了,雖然 Redis 的單個(gè)命令操作可以原子性地執(zhí)行,但是在實(shí)際應(yīng)用中,數(shù)據(jù)修改時(shí)可能包含多個(gè)操作,至少包括讀數(shù)據(jù)、數(shù)據(jù)增減、寫(xiě)回?cái)?shù)據(jù)三個(gè)操作,這顯然就不是單個(gè)命令操作了,那該怎么辦呢?

別擔(dān)心,Redis 提供了 INCR/DECR 命令,把這三個(gè)操作轉(zhuǎn)變?yōu)橐粋€(gè)原子操作了。INCR/DECR 命令可以對(duì)數(shù)據(jù)進(jìn)行增值 / 減值操作,而且它們本身就是單個(gè)命令操作,Redis 在執(zhí)行它們時(shí),本身就具有互斥性。

比如說(shuō),在剛才的庫(kù)存扣減例子中,客戶(hù)端可以使用下面的代碼,直接完成對(duì)商品 id 的庫(kù)存值減 1 操作。即使有多個(gè)客戶(hù)端執(zhí)行下面的代碼,也不用擔(dān)心出現(xiàn)庫(kù)存值扣減錯(cuò)誤的問(wèn)題。

DECR id

所以,如果我們執(zhí)行的 RMW 操作是對(duì)數(shù)據(jù)進(jìn)行增減值的話(huà),Redis 提供的原子操作 INCR 和 DECR 可以直接幫助我們進(jìn)行并發(fā)控制。

但是,如果我們要執(zhí)行的操作不是簡(jiǎn)單地增減數(shù)據(jù),而是有更加復(fù)雜的判斷邏輯或者是其他操作,那么,Redis 的單命令操作已經(jīng)無(wú)法保證多個(gè)操作的互斥執(zhí)行了。所以,這個(gè)時(shí)候,我們需要使用第二個(gè)方法,也就是 Lua 腳本。

Redis 會(huì)把整個(gè) Lua 腳本作為一個(gè)整體執(zhí)行,在執(zhí)行的過(guò)程中不會(huì)被其他命令打斷,從而保證了 Lua 腳本中操作的原子性。如果我們有多個(gè)操作要執(zhí)行,但是又無(wú)法用 INCR/DECR 這種命令操作來(lái)實(shí)現(xiàn),就可以把這些要執(zhí)行的操作編寫(xiě)到一個(gè) Lua 腳本中。
然后,我們可以使用 Redis 的 EVAL 命令來(lái)執(zhí)行腳本。這樣一來(lái),這些操作在執(zhí)行時(shí)就具有了互斥性。

再舉個(gè)例子,具體解釋下 Lua 的使用。
當(dāng)一個(gè)業(yè)務(wù)應(yīng)用的訪問(wèn)用戶(hù)增加時(shí),我們有時(shí)需要限制某個(gè)客戶(hù)端在一定時(shí)間范圍內(nèi)的訪問(wèn)次數(shù),比如爆款商品的購(gòu)買(mǎi)限流、社交網(wǎng)絡(luò)中的每分鐘點(diǎn)贊次數(shù)限制等。

那該怎么限制呢?我們可以把客戶(hù)端 IP 作為 key,把客戶(hù)端的訪問(wèn)次數(shù)作為 value,保存到 Redis 中。客戶(hù)端每訪問(wèn)一次后,我們就用 INCR 增加訪問(wèn)次數(shù)。

不過(guò),在這種場(chǎng)景下,客戶(hù)端限流其實(shí)同時(shí)包含了對(duì)訪問(wèn)次數(shù)和時(shí)間范圍的限制,例如每分鐘的訪問(wèn)次數(shù)不能超過(guò) 20。所以,我們可以在客戶(hù)端第一次訪問(wèn)時(shí),給對(duì)應(yīng)鍵值對(duì)設(shè)置過(guò)期時(shí)間,例如設(shè)置為 60s 后過(guò)期。同時(shí),在客戶(hù)端每次訪問(wèn)時(shí),我們讀取客戶(hù)端當(dāng)前的訪問(wèn)次數(shù),如果次數(shù)超過(guò)閾值,就報(bào)錯(cuò),限制客戶(hù)端再次訪問(wèn)。你可以看下下面的這段代碼,它實(shí)現(xiàn)了對(duì)客戶(hù)端每分鐘訪問(wèn)次數(shù)不超過(guò) 20 次的限制。

// 獲取 ip 對(duì)應(yīng)的訪問(wèn)次數(shù)
current = GET(ip)
// 如果超過(guò)訪問(wèn)次數(shù)超過(guò) 20 次,則報(bào)錯(cuò)
IF current != NULL AND current   20 THEN
 ERROR  exceed 20 accesses per second 
 // 如果訪問(wèn)次數(shù)不足 20 次,增加一次訪問(wèn)計(jì)數(shù)
 value = INCR(ip)
 // 如果是第一次訪問(wèn),將鍵值對(duì)的過(guò)期時(shí)間設(shè)置為 60s 后
 IF value == 1 THEN
 EXPIRE(ip,60)
 END
 // 執(zhí)行其他操作
 DO THINGS
END

可以看到,在這個(gè)例子中,我們已經(jīng)使用了 INCR 來(lái)原子性地增加計(jì)數(shù)。但是,客戶(hù)端限流的邏輯不只有計(jì)數(shù),還包括訪問(wèn)次數(shù)判斷和過(guò)期時(shí)間設(shè)置。

對(duì)于這些操作,我們同樣需要保證它們的原子性。否則,如果客戶(hù)端使用多線(xiàn)程訪問(wèn),訪問(wèn)次數(shù)初始值為 0,第一個(gè)線(xiàn)程執(zhí)行了 INCR(ip) 操作后,第二個(gè)線(xiàn)程緊接著也執(zhí)行了 INCR(ip),此時(shí),ip 對(duì)應(yīng)的訪問(wèn)次數(shù)就被增加到了 2,我們就無(wú)法再對(duì)這個(gè) ip 設(shè)置過(guò)期時(shí)間了。這樣就會(huì)導(dǎo)致,這個(gè) ip 對(duì)應(yīng)的客戶(hù)端訪問(wèn)次數(shù)達(dá)到 20 次之后,就無(wú)法再進(jìn)行訪問(wèn)了。即使過(guò)了 60s,也不能再繼續(xù)訪問(wèn),顯然不符合業(yè)務(wù)要求。

所以,這個(gè)例子中的操作無(wú)法用 Redis 單個(gè)命令來(lái)實(shí)現(xiàn),此時(shí),我們就可以使用 Lua 腳本來(lái)保證并發(fā)控制。我們可以把訪問(wèn)次數(shù)加 1、判斷訪問(wèn)次數(shù)是否為 1,以及設(shè)置過(guò)期時(shí)間這三個(gè)操作寫(xiě)入一個(gè) Lua 腳本,如下所示:

local current
current = redis.call(incr ,KEYS[1])
if tonumber(current) == 1 then
 redis.call(expire ,KEYS[1],60)
end

假設(shè)我們編寫(xiě)的腳本名稱(chēng)為 lua.script,我們接著就可以使用 Redis 客戶(hù)端,帶上 eval 選項(xiàng),來(lái)執(zhí)行該腳本。腳本所需的參數(shù)將通過(guò)以下命令中的 keys 和 args 進(jìn)行傳遞。

redis-cli --eval lua.script keys , args

這樣一來(lái),訪問(wèn)次數(shù)加 1、判斷訪問(wèn)次數(shù)是否為 1,以及設(shè)置過(guò)期時(shí)間這三個(gè)操作就可以原子性地執(zhí)行了。即使客戶(hù)端有多個(gè)線(xiàn)程同時(shí)執(zhí)行這個(gè)腳本,Redis 也會(huì)依次串行執(zhí)行腳本代碼,避免了并發(fā)操作帶來(lái)的數(shù)據(jù)錯(cuò)誤。

以上就是關(guān)于“redis 原子操作實(shí)例分析”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望丸趣 TV 小編分享的內(nèi)容對(duì)大家有幫助,若想了解更多相關(guān)的知識(shí)內(nèi)容,請(qǐng)關(guān)注丸趣 TV 行業(yè)資訊頻道。

正文完
 
丸趣
版權(quán)聲明:本站原創(chuàng)文章,由 丸趣 2023-07-15發(fā)表,共計(jì)4226字。
轉(zhuǎn)載說(shuō)明:除特殊說(shuō)明外本站除技術(shù)相關(guān)以外文章皆由網(wǎng)絡(luò)搜集發(fā)布,轉(zhuǎn)載請(qǐng)注明出處。
評(píng)論(沒(méi)有評(píng)論)
主站蜘蛛池模板: 行唐县| 西盟| 元江| 汶上县| 宝清县| 类乌齐县| 纳雍县| 韶山市| 雅安市| 中卫市| 普宁市| 汝州市| 内丘县| 渝中区| 大悟县| 平谷区| 鄂温| 梁河县| 铜梁县| 南京市| 潢川县| 惠水县| 台湾省| 阆中市| 马边| 社旗县| 金阳县| 恩平市| 汉源县| 阳东县| 佛山市| 岳阳县| 瓮安县| 冷水江市| 万安县| 香港| 兴安盟| 晋州市| 洛隆县| 库伦旗| 子长县|