這篇文章為大家帶來了關於redis原子操作的相關知識,為了確保並發訪問的正確性,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视频教程》、《2022最新redis面试题大全及答案》
以上是十分鐘搞懂redis原子操作的詳細內容。更多資訊請關注PHP中文網其他相關文章!

Redis的數據模型和結構包括五種主要類型:1.字符串(String):用於存儲文本或二進制數據,支持原子操作。 2.列表(List):有序元素集合,適合隊列和堆棧。 3.集合(Set):無序唯一元素集合,支持集合運算。 4.有序集合(SortedSet):帶分數的唯一元素集合,適用於排行榜。 5.哈希表(Hash):鍵值對集合,適合存儲對象。

Redis的數據庫方法包括內存數據庫和鍵值存儲。 1)Redis將數據存儲在內存中,讀寫速度快。 2)它使用鍵值對存儲數據,支持複雜數據結構,如列表、集合、哈希表和有序集合,適用於緩存和NoSQL數據庫。

Redis是一個強大的數據庫解決方案,因為它提供了極速性能、豐富的數據結構、高可用性和擴展性、持久化能力以及廣泛的生態系統支持。 1)極速性能:Redis的數據存儲在內存中,讀寫速度極快,適合高並發和低延遲應用。 2)豐富的數據結構:支持多種數據類型,如列表、集合等,適用於多種場景。 3)高可用性和擴展性:支持主從復制和集群模式,實現高可用性和水平擴展。 4)持久化和數據安全:通過RDB和AOF兩種方式實現數據持久化,確保數據的完整性和可靠性。 5)廣泛的生態系統和社區支持:擁有龐大的生態系統和活躍社區,

Redis的關鍵特性包括速度、靈活性和豐富的數據結構支持。 1)速度:Redis作為內存數據庫,讀寫操作幾乎瞬時,適用於緩存和會話管理。 2)靈活性:支持多種數據結構,如字符串、列表、集合等,適用於復雜數據處理。 3)數據結構支持:提供字符串、列表、集合、哈希表等,適合不同業務需求。

Redis的核心功能是高性能的內存數據存儲和處理系統。 1)高速數據訪問:Redis將數據存儲在內存中,提供微秒級別的讀寫速度。 2)豐富的數據結構:支持字符串、列表、集合等,適應多種應用場景。 3)持久化:通過RDB和AOF方式將數據持久化到磁盤。 4)發布訂閱:可用於消息隊列或實時通信系統。

Redis支持多種數據結構,具體包括:1.字符串(String),適合存儲單一值數據;2.列表(List),適用於隊列和棧;3.集合(Set),用於存儲不重複數據;4.有序集合(SortedSet),適用於排行榜和優先級隊列;5.哈希表(Hash),適合存儲對像或結構化數據。

Redis計數器是一種使用Redis鍵值對存儲來實現計數操作的機制,包含以下步驟:創建計數器鍵、增加計數、減少計數、重置計數和獲取計數。 Redis計數器的優勢包括速度快、高並發、持久性和簡單易用。它可用於用戶訪問計數、實時指標跟踪、遊戲分數和排名以及訂單處理計數等場景。

使用 Redis 命令行工具 (redis-cli) 可通過以下步驟管理和操作 Redis:連接到服務器,指定地址和端口。使用命令名稱和參數向服務器發送命令。使用 HELP 命令查看特定命令的幫助信息。使用 QUIT 命令退出命令行工具。


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

WebStorm Mac版
好用的JavaScript開發工具

EditPlus 中文破解版
體積小,語法高亮,不支援程式碼提示功能

Dreamweaver Mac版
視覺化網頁開發工具

禪工作室 13.0.1
強大的PHP整合開發環境

SAP NetWeaver Server Adapter for Eclipse
將Eclipse與SAP NetWeaver應用伺服器整合。