? 騰訊 面試官:「數(shù)據(jù)庫(kù)事務(wù)機(jī)制了解么?」「內(nèi)心獨(dú)白:小意思,不就
AC ID 嘛,轉(zhuǎn)眼一想,我面試的可是技術(shù)專家,不會(huì)這么簡(jiǎn)單的問(wèn)題吧」程許遠(yuǎn):「balabala…… 極其自信且從容淡定的說(shuō)了一通?!?/p>
? 騰訊面試官:「Redis 的事務(wù)了解么?它的事務(wù)機(jī)制能實(shí)現(xiàn) ACID 屬性么?」程許遠(yuǎn):「撓頭,這個(gè)……我知道 lua 腳本能實(shí)現(xiàn)事務(wù)…」
騰訊 面試官:「好的,回去等通知吧?!?/p>
? 碼哥,我跟著你學(xué)習(xí)了 《Redis 系列》斬獲了很多 offer,沒(méi)想到最后敗在了 「Redis 如何實(shí)現(xiàn)事務(wù)?」這個(gè)問(wèn)題上。我們來(lái)一步步分析:
什么是事務(wù) ACID? Redis 如何實(shí)現(xiàn)事務(wù)? Redis 的事務(wù)能實(shí)現(xiàn)哪些屬性? Lua 腳本實(shí)現(xiàn)。 什么是事務(wù)的 ACID 鬼吹燈之《云南蟲(chóng)谷》中的摸金校尉有句話叫「合則生,分則死」,為了尋找雮塵珠他們?nèi)朔止っ鞔_、齊心協(xié)力共進(jìn)退方可成功。事務(wù)(Transaction)是并發(fā)控制單位,一個(gè)操作序列組合而成,這些操作要么都執(zhí)行,要么都不執(zhí)行。「是一個(gè)不可分割的工作單位」。事務(wù)在執(zhí)行時(shí),會(huì)提供專門(mén)的屬性保證:
原子性(Atomicity):一個(gè)事務(wù)的多個(gè)操作必須完成,或者都不完成(ps:MySQL 的原子性靠什么實(shí)現(xiàn)呢?歡迎留言區(qū)評(píng)論); 一致性(Consistency):事務(wù)執(zhí)行結(jié)束后,數(shù)據(jù)庫(kù)的完整性約束沒(méi)有被破壞,事務(wù)執(zhí)行的前后順序都是合法數(shù)據(jù)狀態(tài)。數(shù)據(jù)庫(kù)的完整性約束包括但不限于: 實(shí)體完整性(如行的主鍵存在且唯一); 列完整性(如字段的類型、大小、長(zhǎng)度要符合要求) 外鍵約束; 用戶自定義完整性(如轉(zhuǎn)賬前后,兩個(gè)賬戶余額的和應(yīng)該不變)。 隔離性(Isolation):事務(wù)內(nèi)部的操作與其他事務(wù)是隔離的,并發(fā)執(zhí)行的各個(gè)事務(wù)之間不能互相干擾。講究的是不同事務(wù)之間的相互影響,嚴(yán)格的隔離性對(duì)應(yīng)隔離級(jí)別中的可串行化(Serializable)。 持久性(Durability):事務(wù)一旦提交,所有的修改將永久的保存到數(shù)據(jù)庫(kù)中,即使系統(tǒng)崩潰重啟后數(shù)據(jù)也不會(huì)丟失。 ? 碼哥,了解了 ACID 的具體要求后,Redis 是如何實(shí)現(xiàn)事務(wù)機(jī)制呢?Redis 如何實(shí)現(xiàn)事務(wù) MULTI、EXEC、DISCARD 和 WATCH 命令是 Redis 實(shí)現(xiàn)事務(wù)的的基礎(chǔ)。Redis 事務(wù)的執(zhí)行過(guò)程包含三個(gè)步驟:
開(kāi)啟事務(wù); 命令入隊(duì); 執(zhí)行事務(wù)或丟棄; 顯式開(kāi)啟一個(gè)事務(wù) 客戶端通過(guò)
MULTI
命令顯式地表示開(kāi)啟一個(gè)事務(wù),隨后的命令將排隊(duì)緩存,并不會(huì)實(shí)際執(zhí)行。
命令入隊(duì) 客戶端把事務(wù)中的要執(zhí)行的一系列指令發(fā)送到服務(wù)端。需要注意的是,雖然指令發(fā)送到服務(wù)端,但是 Redis 實(shí)例只是把這一系列指令暫存在一個(gè)命令隊(duì)列中,并不會(huì)立刻執(zhí)行。
執(zhí)行事務(wù)或丟棄 客戶端向服務(wù)端發(fā)送提交或者丟棄事務(wù)的命令,讓 Redis 執(zhí)行第二步中發(fā)送的具體指令或者清空隊(duì)列命令,放棄執(zhí)行。Redis 只需在調(diào)用 EXEC 時(shí),即可安排隊(duì)列命令執(zhí)行。也可通過(guò) DISCARD 丟棄第二步中保存在隊(duì)列中的命令。
Redis 事務(wù)案例 通過(guò)在線調(diào)試網(wǎng)站執(zhí)行我們的樣例代碼:https://try.redis.io
正常執(zhí)行 通過(guò)
MULTI
和
EXEC
執(zhí)行一個(gè)事務(wù)過(guò)程:
#?開(kāi)啟事務(wù) >?MULTI OK#?開(kāi)始定義一些列指令 >?SET?“公眾號(hào):碼哥字節(jié)”?"粉絲?100?萬(wàn)" QUEUED >?SET?"order" ?"30" QUEUED >?SET?"文章數(shù)" ?666 QUEUED >?GET?"文章數(shù)" QUEUED#?實(shí)際執(zhí)行事務(wù) >?EXEC 1)?OK 2)?OK 3)?OK 4)?"666"
我們看到每個(gè)讀寫(xiě)指令執(zhí)行后的返回結(jié)果都是
QUEUED
,表示謝謝操作都被暫存到了命令隊(duì)列,還沒(méi)有實(shí)際執(zhí)行。當(dāng)執(zhí)行了
EXEC
命令,就可以看到具體每個(gè)指令的響應(yīng)數(shù)據(jù)。
放棄事務(wù) 通過(guò)
MULTI
和
DISCARD
丟棄隊(duì)列命令:
#?初始化訂單數(shù) >?SET?"order:mobile" ?100 OK#?開(kāi)啟事務(wù) >?MULTI OK#?訂單?-?1 >?DECR?"order:mobile" QUEUED#?丟棄丟列命令 >?DISCARD OK#?數(shù)據(jù)沒(méi)有被修改 >?GET?"order:mobile" "100"
? 碼哥,Redis 的事務(wù)能保證 ACID 特性么?這個(gè)問(wèn)題問(wèn)得好,我們一起來(lái)分析下。
Redis 事務(wù)滿足 ACID? Redis 事務(wù)可以一次執(zhí)行多個(gè)命令, 并且?guī)в幸韵氯齻€(gè)重要的保證:
批量指令在執(zhí)行 EXEC 命令之前會(huì)放入隊(duì)列暫存; 收到 EXEC 命令后進(jìn)入事務(wù)執(zhí)行,事務(wù)中任意命令執(zhí)行失敗,其余的命令依然被執(zhí)行; 事務(wù)執(zhí)行過(guò)程中,其他客戶端提交的命令不會(huì)插入到當(dāng)前命令執(zhí)行的序列中。 原子性 ? 碼哥,如果事務(wù)執(zhí)行過(guò)程中發(fā)生錯(cuò)誤了,原子性能保證么?在事務(wù)期間,可能遇到兩種命令錯(cuò)誤:
在執(zhí)行 EXEC
命令前,發(fā)送的指令本身就錯(cuò)誤。如下: 參數(shù)數(shù)量錯(cuò)誤; 命令名稱錯(cuò)誤,使用了不存在的命令; 內(nèi)存不足(Redis 實(shí)例使用 maxmemory
指令配置內(nèi)存限制)。 在執(zhí)行 EXEC
命令后,命令可能會(huì)失敗。例如,命令和操作的數(shù)據(jù)類型不匹配(對(duì) String 類型 的 value 執(zhí)行了 List 列表操作); 在執(zhí)行事務(wù)的 EXEC
命令時(shí)。Redis 實(shí)例發(fā)生了故障導(dǎo)致事務(wù)執(zhí)行失敗。 EXEC 執(zhí)行前報(bào)錯(cuò) 在命令入隊(duì)時(shí),Redis 就會(huì)報(bào)錯(cuò)并且記錄下這個(gè)錯(cuò)誤。此時(shí),我們還能繼續(xù)提交命令操作。等到執(zhí)行了
EXEC
命令之后,Redis 就會(huì)拒絕執(zhí)行所有提交的命令操作,返回事務(wù)失敗的結(jié)果。這樣一來(lái),事務(wù)中的所有命令都不會(huì)再被執(zhí)行了,保證了原子性。如下是指令入隊(duì)發(fā)生錯(cuò)誤,導(dǎo)致事務(wù)失敗的例子:
#開(kāi)啟事務(wù) >?MULTI OK#發(fā)送事務(wù)中的第一個(gè)操作,但是Redis不支持該命令,返回報(bào)錯(cuò)信息 127.0.0.1:6379>?PUT?order?6 (error)?ERR?unknown?command ?`PUT`,?with?args?beginning?with:?`order`,?`6`,#發(fā)送事務(wù)中的第二個(gè)操作,這個(gè)操作是正確的命令,Redis把該命令入隊(duì) >?DECR?b:stock QUEUED#實(shí)際執(zhí)行事務(wù),但是之前命令有錯(cuò)誤,所以Redis拒絕執(zhí)行 >?EXEC (error)?EXECABORT?Transaction?discarded?because?of?previous?errors.
EXEC 執(zhí)行后報(bào)錯(cuò) 事務(wù)操作入隊(duì)時(shí),命令和操作的數(shù)據(jù)類型不匹配,但 Redis 實(shí)例沒(méi)有檢查出錯(cuò)誤。但是,在執(zhí)行完 EXEC 命令以后,Redis 實(shí)際執(zhí)行這些指令,就會(huì)報(bào)錯(cuò)。敲黑板了:Redis 雖然會(huì)對(duì)錯(cuò)誤指令報(bào)錯(cuò),但是事務(wù)依然會(huì)把正確的命令執(zhí)行完,這時(shí)候事務(wù)的原子性就無(wú)法保證了!
? 碼哥,為什么 Redis 不支持回滾?其實(shí),Redis 中并沒(méi)有提供回滾機(jī)制。雖然 Redis 提供了 DISCARD 命令。但是,這個(gè)命令只能用來(lái)主動(dòng)放棄事務(wù)執(zhí)行,把暫存的命令隊(duì)列清空,起不到回滾的效果。
EXEC 執(zhí)行時(shí),發(fā)生故障 如果 Redis 開(kāi)啟了 AOF 日志,那么,只會(huì)有部分的事務(wù)操作被記錄到 AOF 日志中。我們需要使用 redis-check-aof 工具檢查 AOF 日志文件,這個(gè)工具可以把未完成的事務(wù)操作從 AOF 文件中去除。這樣一來(lái),我們使用 AOF 恢復(fù)實(shí)例后,事務(wù)操作不會(huì)再被執(zhí)行,從而保證了原子性。簡(jiǎn)單總結(jié):
命令入隊(duì)時(shí)就報(bào)錯(cuò),會(huì)放棄事務(wù)執(zhí)行,保證原子性; 命令入隊(duì)時(shí)沒(méi)報(bào)錯(cuò),實(shí)際執(zhí)行時(shí)報(bào)錯(cuò),不保證原子性; EXEC 命令執(zhí)行時(shí)實(shí)例故障,如果開(kāi)啟了 AOF 日志,可以保證原子性。 一致性 一致性會(huì)受到錯(cuò)誤命令、實(shí)例故障發(fā)生時(shí)機(jī)的影響,按照命令出錯(cuò)實(shí)例故障兩個(gè)維度的發(fā)生時(shí)機(jī),可以分三種情況分析。
EXEC 執(zhí)行前,入隊(duì)報(bào)錯(cuò) 事務(wù)會(huì)被放棄執(zhí)行,所以可以保證一致性。
EXEC 執(zhí)行后,實(shí)際執(zhí)行時(shí)報(bào)錯(cuò) 有錯(cuò)誤的執(zhí)行不會(huì)執(zhí)行,正確的指令可以正常執(zhí)行,一致性可以保證。
EXEC 執(zhí)行時(shí),實(shí)例故障 實(shí)例故障后會(huì)進(jìn)行重啟,這就和數(shù)據(jù)恢復(fù)的方式有關(guān)了,我們要根據(jù)實(shí)例是否開(kāi)啟了 RDB 或 AOF 來(lái)分情況討論下。如果我們沒(méi)有開(kāi)啟 RDB 或 AOF,那么,實(shí)例故障重啟后,數(shù)據(jù)都沒(méi)有了,數(shù)據(jù)庫(kù)是一致的。如果我們使用了 RDB 快照,因?yàn)?RDB 快照不會(huì)在事務(wù)執(zhí)行時(shí)執(zhí)行。所以,事務(wù)命令操作的結(jié)果不會(huì)被保存到 RDB 快照中,使用 RDB 快照進(jìn)行恢復(fù)時(shí),數(shù)據(jù)庫(kù)里的數(shù)據(jù)也是一致的。如果我們使用了 AOF 日志,而事務(wù)操作還沒(méi)有被記錄到 AOF 日志時(shí),實(shí)例就發(fā)生了故障,那么,使用 AOF 日志恢復(fù)的數(shù)據(jù)庫(kù)數(shù)據(jù)是一致的。如果只有部分操作被記錄到了 AOF 日志,我們可以使用 redis-check-aof 清除事務(wù)中已經(jīng)完成的操作,數(shù)據(jù)庫(kù)恢復(fù)后也是一致的。
隔離性 事務(wù)執(zhí)行又可以分成命令入隊(duì)(EXEC 命令執(zhí)行前)和命令實(shí)際執(zhí)行(EXEC 命令執(zhí)行后)兩個(gè)階段。所以在并發(fā)執(zhí)行的時(shí)候我們針對(duì)這兩個(gè)階段分兩種情況分析:
并發(fā)操作在 EXEC
命令前執(zhí)行,隔離性需要通過(guò) WATCH
機(jī)制保證; 并發(fā)操作在 EXEC
命令之后,隔離性可以保證。 ? 碼哥,什么是 WATCH 機(jī)制?我們重點(diǎn)來(lái)看第一種情況:一個(gè)事務(wù)的 EXEC 命令還沒(méi)有執(zhí)行時(shí),事務(wù)的命令操作是暫存在命令隊(duì)列中的。此時(shí),如果有其它的并發(fā)操作,同樣的 key 被修改,需要看事務(wù)是否使用了
WATCH
機(jī)制。WATCH 機(jī)制的作用是:在事務(wù)執(zhí)行前,監(jiān)控一個(gè)或多個(gè)鍵的值變化情況,當(dāng)事務(wù)調(diào)用 EXEC 命令執(zhí)行時(shí),WATCH 機(jī)制會(huì)先檢查監(jiān)控的鍵是否被其它客戶端修改了。如果修改了,就放棄事務(wù)執(zhí)行,避免事務(wù)的隔離性被破壞。同時(shí),客戶端可以再次執(zhí)行事務(wù),此時(shí),如果沒(méi)有并發(fā)修改事務(wù)數(shù)據(jù)的操作了,事務(wù)就能正常執(zhí)行,隔離性也得到了保證。
沒(méi)有 WATCH如果沒(méi)有 WATCH 機(jī)制, 在 EXEC 命令執(zhí)行前的并發(fā)操作對(duì)數(shù)據(jù)讀寫(xiě)。當(dāng)執(zhí)行 EXEC 的時(shí)候,事務(wù)內(nèi)部要操作的數(shù)據(jù)已經(jīng)改變,Redis 并沒(méi)有做到事務(wù)之間的隔離。
并發(fā)操作在 EXEC 之后接收?qǐng)?zhí)行至于第二種情況,因?yàn)?Redis 是用單線程執(zhí)行命令,而且,EXEC 命令執(zhí)行后,Redis 會(huì)保證先把命令隊(duì)列中的所有命令執(zhí)行完再執(zhí)行之后的指令。所以,在這種情況下,并發(fā)操作不會(huì)破壞事務(wù)的隔離性。
持久性 如果 Redis 沒(méi)有使用 RDB 或 AOF,那么事務(wù)的持久化屬性肯定得不到保證。如果 Redis 使用了 RDB 模式,那么,在一個(gè)事務(wù)執(zhí)行后,而下一次的 RDB 快照還未執(zhí)行前,如果發(fā)生了實(shí)例宕機(jī),數(shù)據(jù)丟失,這種情況下,事務(wù)修改的數(shù)據(jù)也是不能保證持久化的。如果 Redis 采用了 AOF 模式,因?yàn)?AOF 模式的三種配置選項(xiàng) no、everysec 和 always 都會(huì)存在數(shù)據(jù)丟失的情況。所以,事務(wù)的持久性屬性也還是得不到保證。不管 Redis 采用什么持久化模式,事務(wù)的持久性屬性是得不到保證的。
總結(jié) Redis 具備了一定的原子性,但不支持回滾。 Redis 具備 AC ID 中一致性的概念。點(diǎn)) Redis 具備隔離性。 Redis 無(wú)法保證持久性。 Redis 的事務(wù)機(jī)制可以保證一致性和隔離性,但是無(wú)法保證持久性。不過(guò),因?yàn)?Redis 本身是內(nèi)存數(shù)據(jù)庫(kù),持久性并不是一個(gè)必須的屬性,我們更加關(guān)注的還是原子性、一致性和隔離性這三個(gè)屬性。原子性的情況比較復(fù)雜,當(dāng)事務(wù)中使用的命令語(yǔ)法有誤時(shí),原子性得不到保證,在其它情況下,事務(wù)都可以原子性執(zhí)行。