日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

面试官:Redis的事务满足原子性吗?

發(fā)布時間:2024/4/11 数据库 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 面试官:Redis的事务满足原子性吗? 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

談起數(shù)據(jù)庫的事務來,估計很多同學的第一反應都是ACID,而排在ACID中首位的A原子性,要求一個事務中的所有操作,要么全部完成,要么全部不完成。熟悉redis的同學肯定知道,在redis中也存在事務,那么它的事務也滿足原子性嗎?下面我們就來一探究竟。

什么是Redis事務?

和數(shù)據(jù)庫事務類似,redis事務也是用來一次性地執(zhí)行多條命令。使用起來也很簡單,可以用MULTI開啟一個事務,然后將多個命令入隊到事務的隊列中,最后由EXEC命令觸發(fā)事務,執(zhí)行事務中的所有命令。看一個簡單的事務執(zhí)行例子:

127.0.0.1:6379> multi OK 127.0.0.1:6379> set name Hydra QUEUED 127.0.0.1:6379> set age 18 QUEUED 127.0.0.1:6379> incr age QUEUED 127.0.0.1:6379> exec 1) OK 2) OK 3) (integer) 19

可以看到,在指令和操作數(shù)的數(shù)據(jù)類型等都正常的情況下,輸入EXEC后所有命令被執(zhí)行成功。

Redis事務滿足原子性嗎?

如果要驗證redis事務是否滿足原子性,那么需要在redis事務執(zhí)行發(fā)生異常的情況下進行,下面我們分兩種不同類型的錯誤分別測試。

語法錯誤

首先測試命令中有語法錯誤的情況,這種情況多為命令的參數(shù)個數(shù)不正確或輸入的命令本身存在錯誤。下面我們在事務中輸入一個存在格式錯誤的命令,開啟事務并依次輸入下面的命令:

127.0.0.1:6379> multi OK 127.0.0.1:6379> set name Hydra QUEUED 127.0.0.1:6379> incr (error) ERR wrong number of arguments for 'incr' command 127.0.0.1:6379> set age 18 QUEUED

輸入的命令incr后面沒有添加參數(shù),屬于命令格式不對的語法錯誤,這時在命令入隊時就會立刻檢測出錯誤并提示error。使用exec執(zhí)行事務,查看結(jié)果輸出:

127.0.0.1:6379> exec (error) EXECABORT Transaction discarded because of previous errors.

在這種情況下,只要事務中的一條命令有語法錯誤,在執(zhí)行exec后就會直接返回錯誤,包括語法正確的命令在內(nèi)的所有命令都不會被執(zhí)行。對此進行驗證,看一下在事務中其他指令執(zhí)行情況,查看set命令的執(zhí)行結(jié)果,全部為空,說明指令沒有被執(zhí)行。

127.0.0.1:6379> get name (nil) 127.0.0.1:6379> get age (nil)

此外,如果存在命令本身拼寫錯誤、或輸入了一個不存在的命令等情況,也屬于語法錯誤的情況,執(zhí)行事務時會直接報錯。

運行錯誤

運行錯誤是指輸入的指令格式正確,但是在命令執(zhí)行期間出現(xiàn)的錯誤,典型場景是當輸入?yún)?shù)的數(shù)據(jù)類型不符合命令的參數(shù)要求時,就會發(fā)生運行錯誤。例如下面的例子中,對一個string類型的值執(zhí)行列表的操作,報錯如下:

127.0.0.1:6379> set key1 value1 OK 127.0.0.1:6379> lpush key1 value2 (error) WRONGTYPE Operation against a key holding the wrong kind of value

這種錯誤在redis實際執(zhí)行指令前是無法被發(fā)現(xiàn)的,只能當真正執(zhí)行才能夠被發(fā)現(xiàn),因此這樣的命令是可以被事務隊列接收的,不會和上面的語法錯誤一樣立即報錯。

具體看一下當事務中存在運行錯誤的情況,在下面的事務中,嘗試對string類型數(shù)據(jù)進行incr自增操作:

127.0.0.1:6379> multi OK 127.0.0.1:6379> set name Hydra QUEUED 127.0.0.1:6379> set age eighteen QUEUED 127.0.0.1:6379> incr age QUEUED 127.0.0.1:6379> del name QUEUED

redis一直到這里都沒有提示存在錯誤,執(zhí)行exec看一下結(jié)果輸出:

127.0.0.1:6379> exec 1) OK 2) OK 3) (error) ERR value is not an integer or out of range 4) (integer) 1

運行結(jié)果可以看到,雖然incr age這條命令出現(xiàn)了錯誤,但是它前后的命令都正常執(zhí)行了,再看一下這些key對應的值,確實證明了其余指令都執(zhí)行成功:

127.0.0.1:6379> get name (nil) 127.0.0.1:6379> get age "eighteen"

階段性結(jié)論

對上面的事務的運行結(jié)果進行一下分析:

  • 存在語法錯誤的情況下,所有命令都不會執(zhí)行
  • 存在運行錯誤的情況下,除執(zhí)行中出現(xiàn)錯誤的命令外,其他命令都能正常執(zhí)行

通過分析我們知道了redis中的事務是不滿足原子性的,在運行錯誤的情況下,并沒有提供類似數(shù)據(jù)庫中的回滾功能。那么為什么redis不支持回滾呢,官方文檔給出了說明,大意如下:

  • redis命令失敗只會發(fā)生在語法錯誤或數(shù)據(jù)類型錯誤的情況,這一結(jié)果都是由編程過程中的錯誤導致,這種情況應該在開發(fā)環(huán)境中檢測出來,而不是生產(chǎn)環(huán)境
  • 不使用回滾,能使redis內(nèi)部設計更簡單,速度更快
  • 回滾不能避免編程邏輯中的錯誤,如果想要將一個鍵的值增加2卻只增加了1,這種情況即使提供回滾也無法提供幫助

基于以上原因,redis官方選擇了更簡單、更快的方法,不支持錯誤回滾。這樣的話,如果在我們的業(yè)務場景中需要保證原子性,那么就要求了開發(fā)者通過其他手段保證命令全部執(zhí)行成功或失敗,例如在執(zhí)行命令前進行參數(shù)類型的校驗,或在事務執(zhí)行出現(xiàn)錯誤時及時做事務補償。

提到其他方式,相信很多小伙伴都聽說使用Lua腳本來保證操作的原子性,例如在分布式鎖中通常使用的就是Lua腳本,那么,神奇的Lua腳本真的能保證原子性嗎?

簡單的Lua腳本入門

在驗證lua腳本的原子性之前,我們需要對它做一個簡單的了解。redis從2.6版本開始支持執(zhí)行l(wèi)ua腳本,它的功能和事務非常類似,一段lua腳本被視作一條命令執(zhí)行,這樣將多條redis命令寫入lua,即可實現(xiàn)類似事務的執(zhí)行結(jié)果。我們先看一下下面幾個常用的命令。

EVAL 命令

最常用的EVAL用于執(zhí)行一段腳本,它的命令的格式如下:

EVAL script numkeys key [key ...] arg [arg ...]

簡單解釋一下其中的參數(shù):

  • script是一段lua腳本程序
  • numkeys指定后續(xù)參數(shù)有幾個key,如沒有key則為0
  • key [key …]表示腳本中用到的redis中的鍵,在lua腳本中通過KEYS[i]的形式獲取
  • arg [arg …]表示附加參數(shù),在lua腳本中通過ARGV[i]獲取

看一個簡單的例子:

127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 value1 vauel2 1) "key1" 2) "key2" 3) "value1" 4) "vauel2"

在上面的命令中,雙引號中是lua腳本程序,后面的2表示存在兩個key,分別是key1和key2,之后的參數(shù)是附加參數(shù)value1和value2。

如果想要使用lua腳本執(zhí)行set命令,可以寫成這樣:

127.0.0.1:6379> EVAL "redis.call('SET', KEYS[1], ARGV[1]);" 1 name Hydra (nil)

這里使用了redis內(nèi)置的lua函數(shù)redis.call來完成set命令,這里打印的執(zhí)行結(jié)果nil是因為沒有返回值,如果不習慣的話,其實我們可以在腳本中添加return 0;的返回語句。

SCRIPT LOAD 和 EVALSHA命令

這兩個命令放在一起是因為它們一般成對使用。先看SCRIPT LOAD,它用于把腳本加載到緩存中,返回SHA1校驗和,這時候只是緩存了命令,但是命令沒有被馬上執(zhí)行,看一個例子:

127.0.0.1:6379> SCRIPT LOAD "return redis.call('GET', KEYS[1]);" "228d85f44a89b14a5cdb768a29c4c4d907133f56"

這里返回了一個SHA1的校驗和,接下來就可以使用EVALSHA來執(zhí)行腳本了:

127.0.0.1:6379> EVALSHA "228d85f44a89b14a5cdb768a29c4c4d907133f56" 1 name "Hydra"

這里使用這個SHA1值就相當于導入了上面緩存的命令,在之后再拼接numkeys、key、arg等參數(shù),命令就能夠正常執(zhí)行了。

其他命令

使用SCRIPT EXISTS命令判斷腳本是否被緩存:

127.0.0.1:6379> SCRIPT EXISTS 228d85f44a89b14a5cdb768a29c4c4d907133f56 1) (integer) 1

使用SCRIPT FLUSH命令清除redis中的lua腳本緩存:

127.0.0.1:6379> SCRIPT FLUSH OK 127.0.0.1:6379> SCRIPT EXISTS 228d85f44a89b14a5cdb768a29c4c4d907133f56 1) (integer) 0

可以看到,執(zhí)行了SCRIPT FLUSH后,再次通過SHA1值查看腳本時已經(jīng)不存在。最后,還可以使用SCRIPT KILL命令殺死當前正在運行的 lua 腳本,但是只有當腳本沒有執(zhí)行寫操作時才會生效。

從這些操作看來,lua腳本具有下面的優(yōu)點:

  • 多次網(wǎng)絡請求可以在一次請求中完成,減少網(wǎng)絡開銷,減少了網(wǎng)絡延遲
  • 客戶端發(fā)送的腳本會存在redis中,其他客戶端可以復用這一腳本,而不需要再重復編碼完成相同的邏輯

Java代碼中使用lua腳本

在Java代碼中可以使用Jedis中封裝好的API來執(zhí)行l(wèi)ua腳本,下面是一個使用Jedis執(zhí)行l(wèi)ua腳本的例子:

public static void main(String[] args) {Jedis jedis = new Jedis("127.0.0.1", 6379);String script="redis.call('SET', KEYS[1], ARGV[1]);"+"return redis.call('GET', KEYS[1]);";List<String> keys= Arrays.asList("age");List<String> values= Arrays.asList("eighteen");Object result = jedis.eval(script, keys, values);System.out.println(result); }

執(zhí)行上面的代碼,控制臺打印了get命令返回的結(jié)果:

eighteen

簡單的鋪墊完成后,我們來看一下lua腳本究竟能否實現(xiàn)回滾級別的原子性。對上面的代碼進行改造,插入一條運行錯誤的命令:

public static void main(String[] args) {Jedis jedis = new Jedis("127.0.0.1", 6379);String script="redis.call('SET', KEYS[1], ARGV[1]);"+"redis.call('INCR', KEYS[1]);"+"return redis.call('GET', KEYS[1]);";List<String> keys= Arrays.asList("age");List<String> values= Arrays.asList("eighteen");Object result = jedis.eval(script, keys, values);System.out.println(result); }

查看執(zhí)行結(jié)果:

再到客戶端執(zhí)行一下get命令:

127.0.0.1:6379> get age "eighteen"

也就是說,雖然程序拋出了異常,但異常前的命令還是被正常的執(zhí)行了且沒有被回滾。再試試直接在redis客戶端中運行這條指令:

127.0.0.1:6379> flushall OK 127.0.0.1:6379> eval "redis.call('SET', KEYS[1], ARGV[1]);redis.call('INCR', KEYS[1]);return redis.call('GET', KEYS[1])" 1 age eight (error) ERR Error running script (call to f_c2ea9d5c8f60735ecbedb47efd42c834554b9b3b): @user_script:1: ERR value is not an integer or out of range 127.0.0.1:6379> get age "eight"

同樣,錯誤之前的指令仍然沒有被回滾,那么我們之前經(jīng)常聽說的Lua腳本保證原子性操作究竟是怎么回事呢?

其實,在redis中是使用的同一個lua解釋器來執(zhí)行所有命令,也就保證了當一段lua腳本在執(zhí)行時,不會有其他腳本或redis命令同時執(zhí)行,保證了操作不會被其他指令插入或打擾,實現(xiàn)的僅僅是這種程度上的原子操作。

但是遺憾的是,如果lua腳本運行時出錯并中途結(jié)束,之后的操作不會進行,但是之前已經(jīng)發(fā)生的寫操作不會撤銷,所以即使使用了lua腳本,也不能實現(xiàn)類似數(shù)據(jù)庫回滾的原子性。

總結(jié)

以上是生活随笔為你收集整理的面试官:Redis的事务满足原子性吗?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。