1. 程式人生 > >Redis快取技術學習系列之事務處理

Redis快取技術學習系列之事務處理

​在本系列的第一篇文章中,我們主要針對Redis中的“鍵”和“值”進行了學習。我們可以注意到,Redis是一個C/S架構的資料庫,在我們目前的認知中,它是通過終端中的一條條命令來儲存和讀取的,即它是一個非常典型的“請求-響應”模型。可是我們知道在實際的應用中,我們要面對的或許是更為複雜的業務邏輯,因為Redis中不存在傳統關係型資料庫中表的概念,因此在使用Redis的過程中,我們要面對兩個實際的問題,即如何更好的維護資料庫中的”鍵“、如何在高效執行命令的同時保證命令執行成功。對於前者,我認為這是一個設計上的問題,而對於後者,我認為這是一個技術上的問題。所以,這篇文章的核心內容就是找到這兩個問題的答案。帶著這樣的問題出發,我們就可以正式進入這篇文章的主題:Redis中的事務處理。

從資料庫事務說起

​通常我們提及資料庫都不可避免的要提到事務,那麼什麼是事務呢?事務是指作為單個邏輯工作單元執行的一系列操作。所以,首先事務是一系列操作,這一系列操作具有二態性,即完全地執行或者完全地不執行。因此事務處理可以確保除非事務單元內的所有操作的成功完成,否則不會永久更新面向資料的資源。我們這裡舉一個例子,資料庫中除查詢操作以外,插入(Insert)、刪除(Delete)和更新(Update)這三種操作都會對資料造成影響,因為事務處理能夠保證一系列操作可以完全地執行或者完全不執行,因此在一個事務被提交以後,該事務中的任何一條SQL語句在被執行的時候,都會生成一條撤銷日誌(Undo Log),而撤銷日誌中記錄的是和當前擦作完全相反的操作,比如刪除的相反操作是插入,插入的相反操作是刪除等。我們通常所說的事務回滾其實就是去執行這些插銷日誌裡的相反操作,這同樣告訴我們一個道理,只有事務中的一系列操作完全執行的情況下可以回滾,如果是在意外情況下導致事務中的一系列操作沒有完全執行,這個時候我們是不能保證資料一定可以回滾的。

​在資料庫相關理論中,一個邏輯工作單元想要成為事務,就必須滿足ACID,即原子性、一致性、隔離性和永續性。(1):原子性這個概念其實就是指,一個事務內的所有SQL操作都是一個整體,因此只有所有的SQL操作都完全執行成功,該事務方可以認為提交成功。如果在提交事務過程中某一條SQL語句執行失敗,則整個事務必須回滾到事務提交前的狀態。(2):而一致性這個概念則是指,事務在完成的時候,必須要保證所有的資料都保持一致的狀態,而落實到資料庫的各個組成部分上,則要求開發人員能夠保證資料、索引、約束、日誌等在事務前後具備一致性。(3):隔離性這個概念主要針對併發,其核心思想就是不同的併發事務對資料產生的修改必須是相互隔離的,假設有兩個不同的事務A和B併發執行,那麼對A來講,它在執行前的狀態只有兩種,即B執行前和B執行後。同理,對B來講同樣是如此,這樣的特性我們就稱為隔離性。(4):永續性相對簡單,是指事務完成以後它對資料的影響是永久性的。

Redis中的事務處理

​好了,截止到目前為止,我們對資料庫中事務處理的相關理論有了一個基本的認識,或許這個世界上的資料庫系統千差萬別,但我相信在事務處理這個問題上它們最終會殊途同歸,就像我們解決併發過程中的衝突問題,常規的做法依然是加鎖一樣,這是我之所以要花費精力去理解和解釋這些理論知識的原因,技術可謂是日新月異,如果我們總是一味地為新技術而疲於奔命,那麼或許我們會漸漸地失去對這個行業的熱愛,我相信原理永遠比框架更為重要,沒有系統學習過計算機專業的課程,這件事情讓我至今都頗為遺憾。Redis中的事務是可以視為一個佇列,即我們可以通過MULTI開始一個事務,這相當於我們聲明瞭一個命令佇列。接下來,我們向Redis中提交的每條命令,都會被排入這個命令佇列。當我們輸入EXEC命令時,將觸發當前事務,這相當於我們從命令佇列中取出命令並執行,所以Redis中一個事務從開始到執行會經歷開始事務命令入隊執行事務三個階段。下面是一個在Redis中使用事務的簡單示例:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET Book_Name "GIt Pro"
QUEUED
127.0.0.1:6379> SADD Program_Language "C++" "C#" "Jave" "Python" 
QUEUED
127.0.0.1:6379> GET Book_Name
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (integer) 4
3) "GIt Pro"

我們可以注意到Redis中的事務和通常意義上的事務基本上是一致的,即

  • 事務是由一系列操作組成的單個邏輯工作執行單元。特別地,因為在Redis中命令是儲存在一個佇列中,所以,事務中的所有命令都會按順序執行,並且在執行事務的過程中不會被客戶端傳送的其它命令中斷。
  • 事務是一個原子操作,事物中的命令只有兩種執行結果,即全部執行或者全部不執行。如果客戶端在使用MULTI命令開啟事務後因為意外而沒有執行EXEC命令,則事務中的所有命令都不會執行。同理,如果客戶端在使用MULTI命令開啟事務後執行EXEC命令,則事務中的所有命令都會執行。
  • Redis中的事務可以使用DISCARD命令來清空一個命令佇列,並放棄對事務的執行。如果命令在入隊時發生錯誤,Redis將在客戶端呼叫EXEC命令時拒絕執行並取消事務,但是在EXEC命令執行後發生的錯誤,Redis將選擇自動忽略。

我們知道,常見的併發控制方案主要有悲觀鎖和樂觀鎖兩種方案,這裡首先來解釋下這兩種概念。所謂悲觀鎖,顧名思義是一種悲觀的策略,悲觀鎖認為:在對任何記錄做修改前都應該加鎖,如果加鎖失敗則表明該機錄正在被修改,此時應該丟擲異常;如果加鎖成功則修改記錄並在事務完成後解鎖;如果有其它人修改則應該等待當前修改解鎖或者是丟擲異常。而所謂樂觀鎖,顧名思義是一種樂觀的策略,樂觀鎖認為:每次從記錄中查詢資料別人都不會修改,因此這個過程中不需要加鎖,但是在更新記錄的時候,會通過版本號來判斷別人是否修改過當前記錄。

通常來講,樂觀鎖適合在寫衝突相對較少的場合下,悲觀鎖適合在寫衝突相對較多的場合下。Redis中提供了一種稱為check-and-set的機制,該機制主要通過WATCH命令來實現,其原理正是基於樂觀鎖的策略,Redis會在執行EXEC命令前檢查被監視的鍵對應的值是否發生變化,如果該值發生變化表明有人修改過這個鍵中儲存的值,此時Redis將會自動取消當前事務。我們來看這個簡單的例子:

WATCH Record_Count
val = GET Record_Count
val = val + 1
MULTI
SET Record_Count $val
EXEC

在這個例子中,我們嘗試在事務中對Record_Count做一個自增操作,這段程式碼在非併發的情況下是沒有問題的,可是在併發的情況下,如果在執行EXEC命令前有一個使用者修改了Record_Count的值,那麼我們此時的結果就會比期望的結果小1,現在我們有了WATCH,Redis就會對Record_Count進行監聽,當Redis監聽到該值發生變化的時候,這個事務就會被自動取消,進而避免造成衝突。

談談Redis中鍵的管理問題

​其實從切題的角度來講,這篇部落格基本上說清楚了事務處理問題,因此這篇部落格雖然沒有給大家帶來多少驚喜,卻依然可以非常恰到好處的結題,可是因為之前有朋友在部落格中留言並問到Redis的鍵管理的問題,所以博主決定在這裡簡單的討論下這個問題,鑑於博主和大家一樣都是感剛接觸Redis,所以下面的觀點僅僅是一家之言,如果有疑問可以在部落格中留言,歡迎大家批評指正。我認為Redis中的鍵的管理,基本上有兩種策略,即惰性刪除和定期刪除,而實際上這正是Redis預設的鍵刪除策略:

redis使用惰性刪除定期刪除兩種策略來刪除過期的鍵:惰性刪除策略在碰到過期鍵時方進行刪除操作,定期刪除策略則每隔一段時間主動查詢並刪除過期鍵。

所以,基於這兩種鍵刪除策略,我們可以想到的做法有:

  • 對於臨時變數可以採用臨時鍵來儲存,在資料庫全域性設定一個過期時間,由Redis在鍵過期後自動刪除。
  • 對於持久化資料可以採用普通鍵來儲存,通過伺服器和客戶端間定義協議來由客戶端主動刪除鍵。
  • 對於不同模組中的鍵採取統一規範的命名規則來命名鍵,從而解決Redis中鍵管理混亂的問題。
  • 設計合理的鍵回收機制,避免Redis使用超過95%以上的記憶體,或者通過設定Redis中的最大記憶體容量及其記憶體策略來主動觸發Redis對鍵的淘汰。具體可以參考:Sunnyxd - Redis學習筆記-事務、鍵空間的維護與效能

好了,這篇文章就是這樣了,希望大家喜歡,下篇見!