探索Redis設計與實現14:Redis事務淺析與ACID特性介紹

本文轉自互聯網

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裏查看

https://github.com/h2pl/Java-Tutorial

喜歡的話麻煩點下Star哈

文章首發於我的個人博客:

www.how2playlife.com

本文是微信公衆號【Java技術江湖】的《探索Redis設計與實現》其中一篇,本文部分內容來源於網絡,爲了把本文主題講得清晰透徹,也整合了很多我認爲不錯的技術博客內容,引用其中了一些比較好的博客文章,如有侵權,請聯繫作者。

該系列博文會告訴你如何從入門到進階,Redis基本的使用方法,Redis的基本數據結構,以及一些進階的使用方法,同時也需要進一步瞭解Redis的底層數據結構,再接着,還會帶來Redis主從複製、集羣、分佈式鎖等方面的相關內容,以及作爲緩存的一些使用方法和注意事項,以便讓你更完整地瞭解整個Redis相關的技術體系,形成自己的知識框架。

如果對本系列文章有什麼建議,或者是有什麼疑問的話,也可以關注公衆號【Java技術江湖】聯繫作者,歡迎你參與本系列博文的創作和修訂。

事務

MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事務相關的命令。事務可以一次執行多個命令, 並且帶有以下兩個重要的保證:

  • 事務是一個單獨的隔離操作:事務中的所有命令都會序列化、按順序地執行。事務在執行的過程中,不會被其他客戶端發送來的命令請求所打斷。

  • 事務是一個原子操作:事務中的命令要麼全部被執行,要麼全部都不執行。

EXEC 命令負責觸發並執行事務中的所有命令:

  • 如果客戶端在使用 MULTI 開啓了一個事務之後,卻因爲斷線而沒有成功執行 EXEC ,那麼事務中的所有命令都不會被執行。
  • 另一方面,如果客戶端成功在開啓事務之後執行 EXEC ,那麼事務中的所有命令都會被執行。

當使用 AOF 方式做持久化的時候, Redis 會使用單個 write(2) 命令將事務寫入到磁盤中。

然而,如果 Redis 服務器因爲某些原因被管理員殺死,或者遇上某種硬件故障,那麼可能只有部分事務命令會被成功寫入到磁盤中。

如果 Redis 在重新啓動時發現 AOF 文件出了這樣的問題,那麼它會退出,並彙報一個錯誤。

使用redis-check-aof程序可以修復這一問題:它會移除 AOF 文件中不完整事務的信息,確保服務器可以順利啓動。

從 2.2 版本開始,Redis 還可以通過樂觀鎖(optimistic lock)實現 CAS (check-and-set)操作,具體信息請參考文檔的後半部分。

用法

MULTI 命令用於開啓一個事務,它總是返回 OK 。 MULTI 執行之後, 客戶端可以繼續向服務器發送任意多條命令, 這些命令不會立即被執行, 而是被放到一個隊列中, 當 EXEC命令被調用時, 所有隊列中的命令纔會被執行。

另一方面, 通過調用 DISCARD , 客戶端可以清空事務隊列, 並放棄執行事務。

以下是一個事務例子, 它原子地增加了 foo 和 bar 兩個鍵的值:

> MULTIOK> INCR fooQUEUED> INCR barQUEUED> EXEC1) (integer) 12) (integer) 1

EXEC 命令的回覆是一個數組, 數組中的每個元素都是執行事務中的命令所產生的回覆。 其中, 回覆元素的先後順序和命令發送的先後順序一致。

當客戶端處於事務狀態時, 所有傳入的命令都會返回一個內容爲 QUEUED 的狀態回覆(status reply), 這些被入隊的命令將在 EXEC 命令被調用時執行。

事務中的錯誤

使用事務時可能會遇上以下兩種錯誤:

  • 事務在執行 EXEC 之前,入隊的命令可能會出錯。比如說,命令可能會產生語法錯誤(參數數量錯誤,參數名錯誤,等等),或者其他更嚴重的錯誤,比如內存不足(如果服務器使用 maxmemory 設置了最大內存限制的話)。
  • 命令可能在 EXEC 調用之後失敗。舉個例子,事務中的命令可能處理了錯誤類型的鍵,比如將列表命令用在了字符串鍵上面,諸如此類。

對於發生在 EXEC 執行之前的錯誤,客戶端以前的做法是檢查命令入隊所得的返回值:如果命令入隊時返回 QUEUED ,那麼入隊成功;否則,就是入隊失敗。如果有命令在入隊時失敗,那麼大部分客戶端都會停止並取消這個事務。

不過,從 Redis 2.6.5 開始,服務器會對命令入隊失敗的情況進行記錄,並在客戶端調用 EXEC 命令時,拒絕執行並自動放棄這個事務。

在 Redis 2.6.5 以前, Redis 只執行事務中那些入隊成功的命令,而忽略那些入隊失敗的命令。 而新的處理方式則使得在流水線(pipeline)中包含事務變得簡單,因爲發送事務和讀取事務的回覆都只需要和服務器進行一次通訊。

至於那些在 EXEC 命令執行之後所產生的錯誤, 並沒有對它們進行特別處理: 即使事務中有某個/某些命令在執行時產生了錯誤, 事務中的其他命令仍然會繼續執行。

從協議的角度來看這個問題,會更容易理解一些。 以下例子中, LPOP 命令的執行將出錯, 儘管調用它的語法是正確的:

Trying 127.0.0.1...Connected to localhost.Escape character is '^]'.MULTI+OKSET a 3abc+QUEUEDLPOP a+QUEUEDEXEC*2+OK-ERR Operation against a key holding the wrong kind of value

EXEC 返回兩條bulk-string-reply: 第一條是 OK ,而第二條是 -ERR 。 至於怎樣用合適的方法來表示事務中的錯誤, 則是由客戶端自己決定的。

最重要的是記住這樣一條, 即使事務中有某條/某些命令執行失敗了, 事務隊列中的其他命令仍然會繼續執行 —— Redis 不會停止執行事務中的命令。

以下例子展示的是另一種情況, 當命令在入隊時產生錯誤, 錯誤會立即被返回給客戶端:

MULTI+OKINCR a b c-ERR wrong number of arguments for 'incr' command

因爲調用 INCR 命令的參數格式不正確, 所以這個 INCR 命令入隊失敗。

爲什麼 Redis 不支持回滾(roll back)

如果你有使用關係式數據庫的經驗, 那麼 “Redis 在事務失敗時不進行回滾,而是繼續執行餘下的命令”這種做法可能會讓你覺得有點奇怪。

以下是這種做法的優點:

  • Redis 命令只會因爲錯誤的語法而失敗(並且這些問題不能在入隊時發現),或是命令用在了錯誤類型的鍵上面:這也就是說,從實用性的角度來說,失敗的命令是由編程錯誤造成的,而這些錯誤應該在開發的過程中被發現,而不應該出現在生產環境中。
  • 因爲不需要對回滾進行支持,所以 Redis 的內部可以保持簡單且快速。

有種觀點認爲 Redis 處理事務的做法會產生 bug , 然而需要注意的是, 在通常情況下, 回滾並不能解決編程錯誤帶來的問題。 舉個例子, 如果你本來想通過 INCR 命令將鍵的值加上 1 , 卻不小心加上了 2 , 又或者對錯誤類型的鍵執行了 INCR, 回滾是沒有辦法處理這些情況的。

放棄事務

當執行 DISCARD 命令時, 事務會被放棄, 事務隊列會被清空, 並且客戶端會從事務狀態中退出:

> SET foo 1OK> MULTIOK> INCR fooQUEUED> DISCARDOK> GET foo"1"

使用 check-and-set 操作實現樂觀鎖

WATCH 命令可以爲 Redis 事務提供 check-and-set (CAS)行爲。

被 WATCH 的鍵會被監視,並會發覺這些鍵是否被改動過了。 如果有至少一個被監視的鍵在 EXEC 執行之前被修改了, 那麼整個事務都會被取消, EXEC 返回nil-reply來表示事務已經失敗。

舉個例子, 假設我們需要原子性地爲某個值進行增 1 操作(假設 INCR 不存在)。

首先我們可能會這樣做:

val = GET mykeyval = val + 1SET mykey $val

上面的這個實現在只有一個客戶端的時候可以執行得很好。 但是, 當多個客戶端同時對同一個鍵進行這樣的操作時, 就會產生競爭條件。舉個例子, 如果客戶端 A 和 B 都讀取了鍵原來的值, 比如 10 , 那麼兩個客戶端都會將鍵的值設爲 11 , 但正確的結果應該是 12 纔對。

有了 WATCH , 我們就可以輕鬆地解決這類問題了:

WATCH mykeyval = GET mykeyval = val + 1MULTISET mykey $valEXEC

使用上面的代碼, 如果在 WATCH 執行之後, EXEC 執行之前, 有其他客戶端修改了 mykey 的值, 那麼當前客戶端的事務就會失敗。 程序需要做的, 就是不斷重試這個操作, 直到沒有發生碰撞爲止。

這種形式的鎖被稱作樂觀鎖, 它是一種非常強大的鎖機制。 並且因爲大多數情況下, 不同的客戶端會訪問不同的鍵, 碰撞的情況一般都很少, 所以通常並不需要進行重試。

瞭解 WATCH

WATCH 使得 EXEC 命令需要有條件地執行: 事務只能在所有被監視鍵都沒有被修改的前提下執行, 如果這個前提不能滿足的話,事務就不會被執行。 瞭解更多->

WATCH 命令可以被調用多次。 對鍵的監視從 WATCH 執行之後開始生效, 直到調用 EXEC 爲止。

用戶還可以在單個 WATCH 命令中監視任意多個鍵, 就像這樣:

redis> WATCH key1 key2 key3OK

當 EXEC 被調用時, 不管事務是否成功執行, 對所有鍵的監視都會被取消。

另外, 當客戶端斷開連接時, 該客戶端對鍵的監視也會被取消。

使用無參數的 UNWATCH 命令可以手動取消對所有鍵的監視。 對於一些需要改動多個鍵的事務, 有時候程序需要同時對多個鍵進行加鎖, 然後檢查這些鍵的當前值是否符合程序的要求。 當值達不到要求時, 就可以使用 UNWATCH 命令來取消目前對鍵的監視, 中途放棄這個事務, 並等待事務的下次嘗試。

使用 WATCH 實現 ZPOP

WATCH 可以用於創建 Redis 沒有內置的原子操作。舉個例子, 以下代碼實現了原創的 ZPOP 命令, 它可以原子地彈出有序集合中分值(score)最小的元素:

WATCH zsetelement = ZRANGE zset 0 0MULTIZREM zset elementEXEC

程序只要重複執行這段代碼, 直到 EXEC 的返回值不是nil-reply回覆即可。

Redis 腳本和事務

從定義上來說, Redis 中的腳本本身就是一種事務, 所以任何在事務裏可以完成的事, 在腳本里面也能完成。 並且一般來說, 使用腳本要來得更簡單,並且速度更快。

因爲腳本功能是 Redis 2.6 才引入的, 而事務功能則更早之前就存在了, 所以 Redis 纔會同時存在兩種處理事務的方法。

不過我們並不打算在短時間內就移除事務功能, 因爲事務提供了一種即使不使用腳本, 也可以避免競爭條件的方法, 而且事務本身的實現並不複雜。

不過在不遠的將來, 可能所有用戶都會只使用腳本來實現事務也說不定。 如果真的發生這種情況的話, 那麼我們將廢棄並最終移除事務功能。

redis事務的ACID特性

在傳統的關係型數據庫中,嚐嚐用ACID特質來檢測事務功能的可靠性和安全性。

在redis中事務總是具有原子性(Atomicity),一致性(Consistency)和隔離性(Isolation),並且當redis運行在某種特定的持久化
模式下,事務也具有耐久性(Durability).

①原子性

事務具有原子性指的是,數據庫將事務中的多個操作當作一個整體來執行,服務器要麼就執行事務中的所有操作,要麼就一個操作也不執行。

但是對於redis的事務功能來說,事務隊列中的命令要麼就全部執行,要麼就一個都不執行,因此redis的事務是具有原子性的。我們通常會知道

兩種關於redis事務原子性的說法,一種是要麼事務都執行,要麼都不執行。另外一種說法是redis事務當事務中的命令執行失敗後面的命令還
會執行,錯誤之前的命令不會回滾。其實這個兩個說法都是正確的。但是缺一不可。我們接下來具體分析下

 我們先看一個可以正確執行的事務例子
redis > MULTIOK redis > SET username "bugall"QUEUED redis > EXEC1) OK2) "bugall"
與之相反,我們再來看一個事務執行失敗的例子。這個事務因爲命令在放入事務隊列的時候被服務器拒絕,所以事務中的所有命令都不會執行,因爲前面我們有介紹到,redis的事務命令是統一先放到事務隊列裏,在用戶輸入EXEC命令的時候再統一執行。但是我們錯誤的使用"GET"命令,在命令放入事務隊列的時候被檢測到事務,這時候還沒有接收到EXEC命令,所以這個時候不牽扯到回滾的問題,在EXEC的時候發現事務隊列裏有命令存在錯誤,所以事務裏的命令就全都不執行,這樣就達到裏事務的原子性,我們看下例子。
redis > MULTIOK redis > GET(error) ERR wrong number of arguments for 'get' command redis > GET usernameQUEUED redis > EXEC(error) EXECABORT Transaction discarded because of previous errors
redis的事務和傳統的關係型數據庫事務的最大區別在於,redis不支持事務的回滾機制,即使事務隊列中的某個命令在執行期間出現錯誤,整個事務也會繼續執行下去,直到將事務隊列中的所有命令都執行完畢爲止,我們看下面的例子
redis > SET username "bugall"OK redis > MULTIOK redis > SADD member "bugall" "litengfe" "yangyifang"QUEUED redis > RPUSH username "b" "l" "y" //錯誤對鍵username使用列表鍵命令QUEUED redis > SADD password "123456" "123456" "123456"QUEUED redis > EXEC1) (integer) 32) (error) WRONGTYPE Operation against a key holding the wrong kind of value3) (integer) 3
redis的作者在十五功能的文檔中解釋說,不支持事務回滾是因爲這種複雜的功能和redis追求的簡單高效的設計主旨不符合,並且他認爲,redis事務的執行時錯誤通常都是編程錯誤造成的,這種錯誤通常只會出現在開發環境中,而很少會在實際的生產環境中出現,所以他認爲沒有必要爲redis開發事務回滾功能。所以我們在討論redis事務回滾的時候,一定要區分命令發生錯誤的時候。

②一致性

    事務具有一致性指的是,如果數據庫在執行事務之前是一致的,那麼在事務執行之後,無論事務是否執行成功,數據庫也應該仍然一致的。    ”一致“指的是數據符合數據庫本身的定義和要求,沒有包含非法或者無效的錯誤數據。redis通過謹慎的錯誤檢測和簡單的設計來保證事務一致性。

③隔離性

    事務的隔離性指的是,即使數據庫中有多個事務併發在執行,各個事務之間也不會互相影響,並且在併發狀態下執行的事務和串行執行的事務產生的結果完全    相同。    因爲redis使用單線程的方式來執行事務(以及事務隊列中的命令),並且服務器保證,在執行事務期間不會對事物進行中斷,因此,redis的事務總是以串行    的方式運行的,並且事務也總是具有隔離性的

④持久性

    事務的耐久性指的是,當一個事務執行完畢時,執行這個事務所得的結果已經被保持到永久存儲介質裏面。    因爲redis事務不過是簡單的用隊列包裹起來一組redis命令,redis並沒有爲事務提供任何額外的持久化功能,所以redis事務的耐久性由redis使用的模式    決定    - 當服務器在無持久化的內存模式下運行時,事務不具有耐久性,一旦服務器停機,包括事務數據在內的所有服務器數據都將丟失    - 當服務器在RDB持久化模式下運作的時候,服務器只會在特定的保存條件滿足的時候纔會執行BGSAVE命令,對數據庫進行保存操作,並且異步執行的BGSAVE不    能保證事務數據被第一時間保存到硬盤裏面,因此RDB持久化模式下的事務也不具有耐久性    - 當服務器運行在AOF持久化模式下,並且appedfsync的選項的值爲always時,程序總會在執行命令之後調用同步函數,將命令數據真正的保存到硬盤裏面,因此    這種配置下的事務是具有耐久性的。    - 當服務器運行在AOF持久化模式下,並且appedfsync的選項的值爲everysec時,程序會每秒同步一次
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章