文章目錄
- 什么是事務
- 原子性
- 一致性
- 持久性
- 隔離性
- 優勢
- 與 MySQL 對比
- 用處
- 事務相關命令
- 開啟事務——MULTI
- 執行事務——EXEC
- 放棄當前事務——DISCARD
- 監控某個 key——WATCH
- 作用場景
- 使用方法
- 實現原理
- 事務總結
什么是事務
MySQL
事務:
- 原子性:把多個操作,打包成一個整體
- 一致性:事務執行之前和之后,數據都不能離譜(變化前后能對上號)
- 持久性:事務中做出的修改都會存硬盤
- 隔離性:事務并發執行,涉及到的一些問題
原子性
相比于 MySQL
來說,Redis
的事務就是個弟弟:
- 原子性:
Redis
的事務,到底有沒有原子性?存在爭議- 最原本的含義是把多個操作打包到一起,要么都執行,要么都不執行
Redis
做到了上述的含義,但是MySQL
這里的原子性走的更遠MySQL
也是把多個操作打包到一起,要么全都執行成功,要么都不執行。如果事務中有操作執行失敗的,就要進行回滾,把中間已經執行的操作,全部回退Redis
事務中的如干操作,要是有失敗的,無所謂。
Redis
把多個操作打包到一起執行,已經可以稱為是原子性了,只是 MySQL
標桿,提高了“原子性”門檻,這就使人們談到原子性的時候,更多的想到的是 MySQL
這樣帶回滾的原子性
所以,一般說到 Redis
的事務有沒有原子性,更多的傾向于沒有(或者弱化的原子性)
一致性
Redis
沒有約束,也沒有回滾機制,事務執行過程中如果某個修改操作出現失敗,就可能引起不一致的情況
- 所以
Redis
事務不具備一致性
持久性
reids
本身就是內存數據庫,數據是存儲在內存中的,雖然 Redis
也有持久化機制(AOF
),但這里的持久化機制和事務沒有什么關系
- 像
MySQL
那邊,事務百分百有持久性,Redis
這邊把持久化機制關了。這是不一樣的 - 所以
Redis
事務不具備持久性
隔離性
Redis
是一個單線程模型的服務器程序,所有的請求/事務都是“串行”執行的。而談到隔離性,都是并發執行才會涉及到的
- 所以
Redis
事務不涉及隔離性
優勢
Redis
事務,主要的意義就是為了“打包”,避免其他客戶端的命令,插隊插到中間
Redis
中實現事務,是引入了一個隊列(每個客戶端都有一個)- 開啟事務的時候,此時客戶端輸入的命令,就會發給服務器,并且進入這個隊列中(而不是立即執行)。當遇到了“執行事務”命令的時候,此時就會把隊列中的這些任務都按順序依次執行
- “按順序依次執行”是在
Redis
主線程中完成的,主線程會把事務中的操作都執行完,再處理別的客戶端
- “按順序依次執行”是在
與 MySQL 對比
Redis
的事務為什么就設計的這么簡單,而不設計成和 MySQL
一樣強大呢?
MySQL
的事務,在背后付出了很大的代價- 空間上,要花費更多的空間來存儲更多的數據(實現回滾,就要額外開辟空間去存儲必要的日志…)
- 時間上,也要有更大的執行開銷 (需要做更多額外的動作)
正是因為 MySQL
上述的問題,才有 Redis
上場的機會(簡單高效的優勢)
用處
什么時候需要用到 Redis
事務呢?
- 如果我們需要把多個操作打包進行操作,使用事務是比較合適的
比如一個商品秒殺出售場景:
一個貨品 A,進行秒殺出售,市場火爆。此時最重要的就是不能出現“超賣”的情況(超賣:放貨 5000 臺,賣出了 5001 臺)
一個典型的程序寫法:
獲取倉庫中剩余的商品個數
if(個數 > 0) {下單成功;個數--;
}
- 如果不加上任何限制,就可能會存在"線程安全"問題
- 所以我們得讓
下單成功
和個數--
這兩個操作是原子的- 以前在多線程中,是通過加鎖的方式,來避免插隊
- 在
Redis
中,就直接使用事務即可
使用事務之后的寫法:
開啟事務
get count
if count > 0decr count
執行事務
- 在
redis
接收到命令的時候,不會立即執行,只會將其按順序放在隊列中。當收到“執行事務”操作的時候,才會開始按順序執行命令 - 第二個客戶端的“執行事務”命令發過來之后,服務器才真正執行第二個事務中的內容。此時第一個事務執行命令已經運行過了,此時第二個事務
get
到的count
就已經是第一個事務自減之后的結果了
這個場景中,沒加鎖,也能解決上述“超賣”問題
redis
命令里能進行條件判定嗎?
redis
原生命令中確實沒有這種條件判斷定,但是redis
支持lua
腳本
lua
是另外一種編程語言,特點是小巧,很多程序都可以內嵌lua
語言,從而去執行其他的語言- 通過
lua
腳本,就能實現上述的“條件判定”,并且也和事務一樣是打包批量執行的lua
腳本的實現方式,是redis
事務的進階版本
確實,redis
的事務的應用場景,沒有 MySQL
的事務那么多(有點雞肋的感覺)。redis
如果是按照集群模式部署,就不支持事務
事務相關命令
開啟事務——MULTI
(貓體,不是馬體)
開啟一個事務,執行成功返回 OK
- 此時只是在服務器的事務隊列中,保存了上述請求,并沒有真正執行命令
- 此時如果另外開一個客戶端,嘗試查詢這幾個
key
,是查詢不到的
執行事務——EXEC
真正執行事務
- 此時客戶端才會真正把上述操作發給客戶端,此時就可以獲取到
key
的值了
放棄當前事務——DISCARD
放棄當前事務,此時直接清空事務隊列,之前的操作都不會真正執行到
- 前面輸入的命令,都被丟棄了
當我們開啟事務,并且給服務器發送若干命令之后,此時重啟服務器,會怎么樣?
- 此時的效果就等同于
discard
- 事務隊列終歸是內存中的結構,重啟之后,自然是沒有了
監控某個 key——WATCH
在執行事務的時候,如果某個事務中修改的值,被別的客戶端修改了,此時就容易出現數據不一致的問題
作用場景
從時間上來看,客戶端 1 是先發送了 set key 222
,客戶端 2 是后發送了 set key 333
- 由于客戶端 1 中,得是
exec
執行了,才會真正執行set key 222
。這個操作變成了實際上更晚執行的操作,最終key
的值就是222
使用方法
在剛才的場景中,就可以使用 watch
命令來監控這個 key
,看看這個 key
在事務的 multi
和 exec
之間,set key
之后,是否在外部被其他客戶端修改了
- 被監控的
key
被修改之后,exec
之后返回值為nil
實現原理
watch
的實現,類似與一個“樂觀鎖”
- 樂觀鎖/悲觀鎖不是指某個具體的鎖,而是指的是某一類鎖的特征
- 樂觀鎖:加鎖之前,就有一個心理預期,接下來鎖的沖突概率較低
- 悲觀鎖:加鎖之前,也有一個心理預期,接下來鎖的沖突概率較高
- 沖突:兩個線程針對同一個鎖加鎖,一個能加鎖成功,另一個就得阻塞等待
- 鎖沖突概率高低,接下來要做的工作是不一樣的
樂觀鎖在 https://yeeear.blog.csdn.net/article/details/141102212 這篇文章中有詳細解釋
redis
的 watch
就相當于是基于版本號這樣的機制,來實現了“樂觀鎖”(就是 CAS
中的 ABA
問題的解決方法)
watch
必須搭配事務使用,并且必須在multi
之前使用- 如果
watch
的版本號和exec
的版本號- 一致:說明當前
key
在事務開啟到最終執行這個過程中,沒有被別的客戶端修改,于是才能真正進行設置 - 不一致:說明
key
在其他客戶端中改過了,因此此處就直接丟棄事務中的操作,exec
返回nil
- 一致:說明當前
所以,watch
本質上就是給 exec
加了一個判定條件
事務總結
Redis
的事務,要比 MySQL
的事務,簡單很多
- 原子性:
Redis
的事務,并不支持回滾 - 一致性:
Redis
并不會保證事務執行前后的內容統一 - 持久性:
Redis
主要通過內存來存儲數據 - 隔離性:
Redis
自身作為一個單線程的服務器模型,上面處理的請求本質上都是串行執行的
四個關于事務的命令:
- 開啟事務——
nulti
- 執行事務——
exec
- 放棄當前事務——
discard
- 監控某個
key
是否被修改——watch