面試竟被問到Redis事務(wù),觸及知識(shí)盲區(qū),臉都綠了
前言
前幾天有讀者說自己面試被問到Redis的事務(wù),雖然不常用,但是面試竟然被問到,平時(shí)自己沒有注意Redis的事務(wù)這一塊,面試的時(shí)候被問到非常不好受。
雖然,這位讀者面試最后算是過了,但是薪資方面沒有拿到自己理想的薪資。
其實(shí)這個(gè)也是正常的,一般面試被問到爛大街的,誰還問你啊,專門挑一些不常見的來問你,就是為了壓你的薪資。
所以在這里寫一篇文章對Redis的事務(wù)進(jìn)行詳細(xì)的講解,估計(jì)對Redis事務(wù)從理解到原理深入這一篇就夠了。
以后面試都不用擔(dān)心了再被問道Redis的事務(wù)了,這一篇主要講解Redis事務(wù)原理和實(shí)操的演練,理解理論的同時(shí)也通過實(shí)操來證實(shí)理論。
事務(wù)介紹
Redis事務(wù)是一組命令的集合,將多個(gè)命令進(jìn)行打包,然后這些命令會(huì)被順序的添加到隊(duì)列中,并且按順序的執(zhí)行這些命令。
「Redis事務(wù)中沒有像Mysql關(guān)系型數(shù)據(jù)庫事務(wù)隔離級(jí)別的概念,不能保證原子性操作,也沒有像Mysql那樣執(zhí)行事務(wù)失敗會(huì)進(jìn)行回滾操作」。
這個(gè)與Redis的特點(diǎn):「快速、高效」有著密切的關(guān)聯(lián),「因?yàn)橐恍┝谢貪L操作、像事務(wù)隔離級(jí)別那這樣加鎖、解鎖,是非常消耗性能的」。所以,Redis中執(zhí)行事務(wù)的流程只需要簡單的下面三個(gè)步驟:
- 開始事務(wù)(MULTI)
- 命令入隊(duì)
- 執(zhí)行事務(wù)(EXEC)、撤銷事務(wù)(DISCARD )
在Redis中事務(wù)的實(shí)現(xiàn)主要是通過如下的命令實(shí)現(xiàn)的:
命令 | 功能描述 |
---|---|
MULTI | 「事務(wù)開始的命令」,執(zhí)行該命令后,后面執(zhí)行的對Redis數(shù)據(jù)類型的「操作命令都會(huì)順序的放進(jìn)隊(duì)列中」,等待執(zhí)行EXEC命令后隊(duì)列中的命令才會(huì)被執(zhí)行 |
DISCARD | 「放棄執(zhí)行隊(duì)列中的命令」,你可以理解為Mysql的回滾操作,「并且將當(dāng)前的狀態(tài)從事務(wù)狀態(tài)改為非事務(wù)狀態(tài)」。 |
EXEC | 執(zhí)行該命令后「表示順序執(zhí)行隊(duì)列中的命令」,執(zhí)行完后并將結(jié)果顯示在客戶端,「將當(dāng)前狀態(tài)從事務(wù)狀態(tài)改為非事務(wù)狀態(tài)」。若是執(zhí)行該命令之前有key被執(zhí)行WATCH命令并且又被其它客戶端修改,那么就會(huì)放棄執(zhí)行隊(duì)列中的所有命令,在客戶端顯示報(bào)錯(cuò)信息,若是沒有修改就會(huì)執(zhí)行隊(duì)列中的所有命令。 |
WATCH key | 表示指定監(jiān)視某個(gè)key,「該命令只能在MULTI命令之前執(zhí)行」,如果監(jiān)視的key被其他客戶端修改,「EXEC將會(huì)放棄執(zhí)行隊(duì)列中的所有命令」 |
UNWATCH | 「取消監(jiān)視之前通過WATCH 命令監(jiān)視的key」,通過執(zhí)行EXEC 、DISCARD 兩個(gè)命令之前監(jiān)視的key也會(huì)被取消監(jiān)視 |
以上就是一個(gè)Redis事務(wù)的執(zhí)行過程包含的命令,下面就來詳細(xì)的圍繞著這幾個(gè)命令進(jìn)行講解。
開始事務(wù)
MULTI 命令表示事務(wù)的開始,當(dāng)看到OK表示已經(jīng)進(jìn)入事務(wù)的狀態(tài):
該命令執(zhí)行后客戶端會(huì)將「當(dāng)前的狀態(tài)從非事務(wù)狀態(tài)修改為事務(wù)狀態(tài)」,這一狀態(tài)的切換是將客戶端的flags屬性中打開REDIS_MULTI來完成的,該命令可以理解關(guān)系型數(shù)據(jù)庫Mysql的BEGIN TRANCATION語句:
命令入隊(duì)
執(zhí)行完MULTI命令后,后面執(zhí)行的操作Redis五種類型的命令都會(huì)按順序的進(jìn)入命令隊(duì)列中,該部分也是真正的業(yè)務(wù)邏輯的部分。
Redis客戶端的命令執(zhí)行后若是當(dāng)前狀態(tài)處于事務(wù)狀態(tài)命令就會(huì)進(jìn)入隊(duì)列中,并且返回QUEUED字符串,表示該命令已經(jīng)進(jìn)入了命令隊(duì)列中,并且「事務(wù)隊(duì)列是以先進(jìn)先出(FIFO)的方式保存入隊(duì)的命令」的。
若是當(dāng)前狀態(tài)是非事務(wù)狀態(tài)就會(huì)立即執(zhí)行命令,并將結(jié)果返回客戶端。在事務(wù)狀態(tài)「執(zhí)行操作事務(wù)的命令就會(huì)被立即執(zhí)行」,如EXEC、DISCARD、UNWATCH。
結(jié)合上面的分析,Redis執(zhí)行命令的流程如下圖所示:
事務(wù)的命令隊(duì)列中有三個(gè)參數(shù)分別是:「要執(zhí)行的命令」、「命令的參數(shù)」、「參數(shù)的個(gè)數(shù)」。例如:通過執(zhí)行如下的命令:
- redis> MULTI
- OK
- redis> SET name "黎杜"
- QUEUED
- redis> GET name
- QUEUED
那么對應(yīng)上面的隊(duì)列中三個(gè)參數(shù)如下表格所示:
執(zhí)行的命令 | 命令的參數(shù) | 參數(shù)的個(gè)數(shù) |
---|---|---|
SET | ["name", "黎杜"] | 2 |
GET | ["name"] | 1 |
執(zhí)行事務(wù)
當(dāng)客戶端執(zhí)行EXEC命令的時(shí)候,上面的命令隊(duì)列就會(huì)被按照先進(jìn)先出的順序被執(zhí)行,當(dāng)然執(zhí)行的結(jié)果有成功有失敗,這個(gè)后面分析。
上面說到當(dāng)客戶端處于非事務(wù)的狀態(tài)命令發(fā)送到服務(wù)端會(huì)被立即執(zhí)行,若是客戶端處于事務(wù)狀態(tài)命令就會(huì)被放進(jìn)命令隊(duì)列。
命令入隊(duì)的時(shí)候,會(huì)按照順序進(jìn)入隊(duì)列,隊(duì)列以先進(jìn)先出的特點(diǎn)來執(zhí)行隊(duì)列中的命令。
若是客戶端處于事務(wù)狀態(tài),執(zhí)行的是EXEC、DISCARD、UNWATCH這些操作事務(wù)的命令,也會(huì)被立即執(zhí)行。
正常執(zhí)行
還是上面的例子,執(zhí)行如下的代碼:
- redis> MULTI
- OK
- redis> SET name "黎杜"
- QUEUED
- redis> GET name
- QUEUED
所有的命令進(jìn)入了隊(duì)列,當(dāng)最后執(zhí)行EXEC,首先會(huì)執(zhí)行SET命令,然后執(zhí)行GET命令,并且執(zhí)行后的結(jié)果也會(huì)進(jìn)入一個(gè)隊(duì)列中保存,最后返回給客戶端:
回復(fù)的類型 | 回復(fù)的內(nèi)容 |
---|---|
status code reply | OK |
bulk reply | "黎杜" |
所以最后你會(huì)在客戶端看到「OK、黎杜」,這樣的結(jié)果顯示,這個(gè)也就是一個(gè)事務(wù)成功執(zhí)行的過程。
至此一個(gè)事務(wù)就完整的執(zhí)行完成,并且此時(shí)客戶端也從事務(wù)狀態(tài)更改為非事務(wù)狀態(tài)。
放棄事務(wù)
當(dāng)然你也可以放棄執(zhí)行該事務(wù),只要你再次執(zhí)行DISCARD操作就會(huì)放棄執(zhí)行此次的事務(wù)。具體代碼如下所示:
- redis> MULTI
- OK
- redis> SET name "黎杜"
- QUEUED
- redis> GET name
- QUEUED
- redis> DISCARD // 放棄執(zhí)行事務(wù)
- OK
DISCARD命令取消一個(gè)事務(wù)的時(shí)候,就會(huì)將命令隊(duì)列清空,并且將客戶端的狀態(tài)從事務(wù)狀態(tài)修改為非事務(wù)的狀態(tài)。
「Redis的事務(wù)是不可重復(fù)的」,當(dāng)客戶端處于事務(wù)狀態(tài)的時(shí)候,再次向服務(wù)端發(fā)送MULTI命令時(shí),直接就會(huì)向客戶端返回錯(cuò)誤。
WATCH 命令
WATCH命令是在MULTI命令之前執(zhí)行的,表示監(jiān)視任意數(shù)量的key,與它對應(yīng)的命令就是UNWATCH命令,取消監(jiān)視的key。
WATCH命令有點(diǎn)「類似于樂觀鎖機(jī)制」,在事務(wù)執(zhí)行的時(shí)候,若是被監(jiān)視的任意一個(gè)key被更改,則隊(duì)列中的命令不會(huì)被執(zhí)行,直接向客戶端返回(nil)表示事務(wù)執(zhí)行失敗。
下面我們來演示一下WATCH命令的操作流程,具體實(shí)現(xiàn)代碼如下:
- redis> WATCH num
- OK
- redis> MULTI
- OK
- redis> incrby num 10
- QUEUED
- redis> decrby num 1
- QUEUED
- redis> EXEC // 執(zhí)行成功
這個(gè)是WATCH命令的正常的操作流程,若是在其它的客戶端,修改了被監(jiān)視的任意key,就會(huì)放棄執(zhí)行該事務(wù),如下圖所示:
客戶端一 | 客戶端二 |
---|---|
WATCH num | |
MULTI | |
incrby num 10 | get num |
decrby num 1 | |
EXEC | |
執(zhí)行失敗,返回(nil) |
WATCH命令的底層實(shí)現(xiàn)中保存了watched_keys 字典,「字典的鍵保存的是監(jiān)視的key,值是一個(gè)鏈表,鏈表中的每個(gè)節(jié)點(diǎn)值保存的是監(jiān)視該key的客戶端」。
若是某個(gè)客戶端不再監(jiān)視某個(gè)key,該客戶端就會(huì)從鏈表中脫離。如client3,通過執(zhí)行UNWATCH命令,不再監(jiān)視key1:
錯(cuò)誤處理
上面說到Redis是沒有回滾機(jī)制的,那么執(zhí)行的過程,若是不小心敲錯(cuò)命令,Redis的命令發(fā)送到服務(wù)端沒有被立即執(zhí)行,所以是暫時(shí)發(fā)現(xiàn)不到該錯(cuò)誤。
那么在Redis中的錯(cuò)誤處理主要分為兩類:「語法錯(cuò)誤」、「運(yùn)行錯(cuò)誤」。下面主要來講解一下這兩類錯(cuò)誤的區(qū)別。
語法錯(cuò)誤
比如執(zhí)行命令的時(shí)候,命令的不存在或者錯(cuò)誤的敲錯(cuò)命令、參數(shù)的個(gè)數(shù)不對等都會(huì)導(dǎo)致語法錯(cuò)誤。
下面來演示一下,執(zhí)行下面的四個(gè)命令,前后的兩個(gè)命令是正確的,中間的兩個(gè)命令是錯(cuò)誤的,如下所示:
- 127.0.0.1:6379> multi
- OK
- 127.0.0.1:6379> set num 1
- QUEUED
- 127.0.0.1:6379> set num
- (error) ERR wrong number of arguments for 'set' command
- 127.0.0.1:6379> ssset num 3
- (error) ERR unknown command 'ssset'
- 127.0.0.1:6379> set num 2
- QUEUED
- 127.0.0.1:6379> exec
- (error) EXECABORT Transaction discarded because of previous errors.
語法錯(cuò)誤是在Redis語法檢測的時(shí)候就能發(fā)現(xiàn)的,所以當(dāng)你執(zhí)行錯(cuò)誤命令的時(shí)候,也會(huì)即使的返回錯(cuò)誤的提示。
最后,即使命令進(jìn)入隊(duì)列,只要存在語法錯(cuò)誤,該隊(duì)列中的命令都不會(huì)被執(zhí)行,會(huì)直接向客戶端返回事務(wù)執(zhí)行失敗的提示。
運(yùn)行錯(cuò)誤
執(zhí)行時(shí)使用不同類型的操作命令操作不同數(shù)據(jù)類型就會(huì)出現(xiàn)運(yùn)行時(shí)錯(cuò)誤,這種錯(cuò)誤時(shí)Redis在不執(zhí)行命令的情況下,是無法發(fā)現(xiàn)的。
- 127.0.0.1:6379> multi
- OK
- 127.0.0.1:6379> set num 3
- QUEUED
- 127.0.0.1:6379> sadd num 4
- QUEUED
- 127.0.0.1:6379> set num 6
- QUEUED
- 127.0.0.1:6379> exec
- 1) OK
- 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
- 3) OK
- 127.0.0.1:6379> get num
- "6"
這樣就會(huì)導(dǎo)致,正確的命令被執(zhí)行,而錯(cuò)誤的命令不會(huì)不執(zhí)行,這也顯示出Redis的事務(wù)并不能保證數(shù)據(jù)的一致性,因?yàn)橹虚g出現(xiàn)了錯(cuò)誤,有些語句還是被執(zhí)行了。
這樣的結(jié)果只能程序員自己根據(jù)之前執(zhí)行的命令,自己一步一步正確的回退,所謂自己的爛攤子,自己收拾。
Redis事務(wù)與Mysql事務(wù)
我們知道關(guān)系性數(shù)據(jù)庫Mysql中具有事務(wù)的四大特性:「原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持久性(Durability)」。
但是Redis的事務(wù)為了保證Redis除了客戶端的請求高效,去除了傳統(tǒng)關(guān)系型數(shù)據(jù)庫的「事務(wù)回滾、加鎖、解鎖」這些消耗性能的操作,Redis的事務(wù)實(shí)現(xiàn)簡單。
原子性中Redis的事務(wù)只能保證單個(gè)命令的原子性,多個(gè)命令就無法保證,如上面索道的運(yùn)行時(shí)錯(cuò)誤,即使中間有運(yùn)行時(shí)錯(cuò)誤出現(xiàn)也會(huì)正確的執(zhí)行后面正確的命令,不具有回滾操作。
既然沒有了原子性,數(shù)據(jù)的一致性也就無法保證,這些都需要程序員自己手動(dòng)去實(shí)現(xiàn)。
Reids在進(jìn)行事務(wù)的時(shí)候,不會(huì)被中斷知道事務(wù)的運(yùn)行結(jié)束,也具有一定的隔離性,并且Redis也能持久化數(shù)據(jù)。