發(fā)布訂閱模式
列表的局限
通過隊(duì)列的rpush 和lpop 可以實(shí)現(xiàn)消息隊(duì)列(隊(duì)尾進(jìn)隊(duì)頭出),但是消費(fèi)者需要不停地調(diào)用lpop 查看List 中是否有等待處理的消息(比如寫一個(gè)while 循環(huán))灸姊。為了減少通信的消耗瞻离,可以sleep()一段時(shí)間再消費(fèi)碎浇,但是會(huì)有兩個(gè)問題:
1、如果生產(chǎn)者生產(chǎn)消息的速度遠(yuǎn)大于消費(fèi)者消費(fèi)消息的速度璃俗,List 會(huì)占用大量的內(nèi)存奴璃。
2、消息的實(shí)時(shí)性降低城豁。
list 還提供了一個(gè)阻塞的命令:blpop苟穆,沒有任何元素可以彈出的時(shí)候,連接會(huì)被阻
塞唱星。
blpop queue 5
基于list 實(shí)現(xiàn)的消息隊(duì)列雳旅,不支持一對(duì)多的消息分發(fā)。
發(fā)布訂閱模式
除了通過list 實(shí)現(xiàn)消息隊(duì)列之外间聊,Redis 還提供了一組命令實(shí)現(xiàn)發(fā)布/訂閱模式攒盈。
這種方式,發(fā)送者和接收者沒有直接關(guān)聯(lián)(實(shí)現(xiàn)了解耦)甸饱,接收者也不需要持續(xù)嘗試獲取消息沦童。
訂閱頻道
首先仑濒,我們有很多的頻道(channel)叹话,我們也可以把這個(gè)頻道理解成queue。訂閱者可以訂閱一個(gè)或者多個(gè)頻道墩瞳。消息的發(fā)布者(生產(chǎn)者)可以給指定的頻道發(fā)布消息驼壶。
只要有消息到達(dá)了頻道,所有訂閱了這個(gè)頻道的訂閱者都會(huì)收到這條消息喉酌。
需要注意的注意是热凹,發(fā)出去的消息不會(huì)被持久化,因?yàn)樗呀?jīng)從隊(duì)列里面移除了泪电,所以消費(fèi)者只能收到它開始訂閱這個(gè)頻道之后發(fā)布的消息般妙。
下面我們來(lái)看一下發(fā)布訂閱命令的使用方法。
訂閱者訂閱頻道:可以一次訂閱多個(gè)相速,比如這個(gè)客戶端訂閱了3 個(gè)頻道碟渺。
subscribe channel-1 channel-2 channel-3
發(fā)布者可以向指定頻道發(fā)布消息(并不支持一次向多個(gè)頻道發(fā)送消息):
publish channel-1 2673
取消訂閱(不能在訂閱狀態(tài)下使用):
unsubscribe channel-1
按規(guī)則(Pattern)訂閱頻道
支持?和占位符。?代表一個(gè)字符突诬,代表0 個(gè)或者多個(gè)字符苫拍。
消費(fèi)端1,關(guān)注運(yùn)動(dòng)信息:
psubscribe *sport
消費(fèi)端2旺隙,關(guān)注所有新聞:
psubscribe news*
消費(fèi)端3绒极,關(guān)注天氣新聞:
psubscribe news-weather
生產(chǎn)者,發(fā)布3 條信息
publish news-sport yaoming
publish news-music jaychou
publish news-weather rain
Redis 事務(wù)
為什么要用事務(wù)
Redis 的單個(gè)命令是原子性的(比如get set mget mset)蔬捷,如果涉及到多個(gè)命令的時(shí)候垄提,需要把多個(gè)命令作為一個(gè)不可分割的處理序列,就需要用到事務(wù)。
例如我們之前說(shuō)的用setnx 實(shí)現(xiàn)分布式鎖铡俐,我們先set摘昌,然后設(shè)置對(duì)key 設(shè)置expire,防止del 發(fā)生異常的時(shí)候鎖不會(huì)被釋放高蜂,業(yè)務(wù)處理完了以后再del聪黎,這三個(gè)動(dòng)作我們就希望它們作為一組命令執(zhí)行。
Redis 的事務(wù)有兩個(gè)特點(diǎn):
1备恤、按進(jìn)入隊(duì)列的順序執(zhí)行稿饰。
2、不會(huì)受到其他客戶端的請(qǐng)求的影響露泊。
Redis 的事務(wù)涉及到四個(gè)命令:multi(開啟事務(wù))棠枉,exec(執(zhí)行事務(wù))株灸,discard(取消事務(wù)),watch(監(jiān)視)
事務(wù)的用法
案例場(chǎng)景:a 和b 各有1000 元,a 需要向b 轉(zhuǎn)賬100 元背零。
a 的賬戶余額減少100 元,b 的賬戶余額增加100 元粘勒。
127.0.0.1:6379> set a 1000
OK
127.0.0.1:6379> set b 1000
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby a 100
QUEUED
127.0.0.1:6379> incrby b 100
QUEUED
127.0.0.1:6379> exec
1) (integer) 900
2) (integer) 1100
127.0.0.1:6379> get a
"900"
127.0.0.1:6379> get b
"1100"
通過multi 的命令開啟事務(wù)对蒲。事務(wù)不能嵌套,多個(gè)multi 命令效果一樣川蒙。
multi 執(zhí)行后蚜厉,客戶端可以繼續(xù)向服務(wù)器發(fā)送任意多條命令, 這些命令不會(huì)立即被執(zhí)行畜眨, 而是被放到一個(gè)隊(duì)列中昼牛, 當(dāng)exec 命令被調(diào)用時(shí), 所有隊(duì)列中的命令才會(huì)被執(zhí)行康聂。
通過exec 的命令執(zhí)行事務(wù)贰健。如果沒有執(zhí)行exec,所有的命令都不會(huì)被執(zhí)行恬汁。
如果中途不想執(zhí)行事務(wù)了伶椿,怎么辦?
可以調(diào)用discard 可以清空事務(wù)隊(duì)列蕊连,放棄執(zhí)行悬垃。
multi
set k1 1
set k2 2
set k3 3
discard
watch 命令
在Redis 中還提供了一個(gè)watch 命令。
它可以為Redis 事務(wù)提供CAS 樂觀鎖行為(Check and Set / Compare andSwap)甘苍,也就是多個(gè)線程更新變量的時(shí)候尝蠕,會(huì)跟原值做比較,只有它沒有被其他線程修改的情況下载庭,才更新成新的值看彼。
我們可以用watch 監(jiān)視一個(gè)或者多個(gè)key廊佩,如果開啟事務(wù)之后,至少有一個(gè)被監(jiān)視key 鍵在exec 執(zhí)行之前被修改了靖榕, 那么整個(gè)事務(wù)都會(huì)被取消(key 提前過期除外)标锄。可以用unwatch 取消茁计。
先在client 1 執(zhí)行
127.0.0.1:6379> set balance 1000
OK
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incrby balance 100
QUEUED
再新開一個(gè)client 2執(zhí)行
127.0.0.1:6379> decrby balance 100
(integer) 900
回到client 1 執(zhí)行
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get balance
"900"
事務(wù)可能遇到的問題
我們把事務(wù)執(zhí)行遇到的問題分成兩種料皇,一種是在執(zhí)行exec 之前發(fā)生錯(cuò)誤,一種是在執(zhí)行exec 之后發(fā)生錯(cuò)誤星压。
在執(zhí)行exec 之前發(fā)生錯(cuò)誤
比如:入隊(duì)的命令存在語(yǔ)法錯(cuò)誤践剂,包括參數(shù)數(shù)量,參數(shù)名等等(編譯器錯(cuò)誤)娜膘。
在這種情況下事務(wù)會(huì)被拒絕執(zhí)行逊脯,也就是隊(duì)列中所有的命令都不會(huì)得到執(zhí)行。
在執(zhí)行exec 之后發(fā)生錯(cuò)誤
比如竣贪,類型錯(cuò)誤军洼,比如對(duì)String 使用了Hash 的命令,這是一種運(yùn)行時(shí)錯(cuò)誤演怎。
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 1
QUEUED
127.0.0.1:6379> hset k1 a b
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> get k1
"1"
最后我們發(fā)現(xiàn)set k1 1 的命令是成功的匕争,也就是在這種發(fā)生了運(yùn)行時(shí)異常的情況下,只有錯(cuò)誤的命令沒有被執(zhí)行颤枪,但是其他命令沒有受到影響汗捡。
這個(gè)顯然不符合我們對(duì)原子性的定義淑际,也就是我們沒辦法用Redis 的這種事務(wù)機(jī)制來(lái)實(shí)現(xiàn)原子性畏纲,保證數(shù)據(jù)的一致。
Lua 腳本
Lua/?lu?/是一種輕量級(jí)腳本語(yǔ)言春缕,它是用C 語(yǔ)言編寫的盗胀,跟數(shù)據(jù)的存儲(chǔ)過程有點(diǎn)類似。使用Lua 腳本來(lái)執(zhí)行Redis 命令的好處:
1锄贼、一次發(fā)送多個(gè)命令票灰,減少網(wǎng)絡(luò)開銷。
2宅荤、Redis 會(huì)將整個(gè)腳本作為一個(gè)整體執(zhí)行屑迂,不會(huì)被其他請(qǐng)求打斷,保持原子性冯键。
3惹盼、對(duì)于復(fù)雜的組合命令,我們可以放在文件中惫确,可以實(shí)現(xiàn)程序之間的命令集復(fù)用手报。
在Redis 中調(diào)用Lua 腳本
使用eval /?'v?l/ 方法蚯舱,語(yǔ)法格式:
redis> eval lua-script key-num [key1 key2 key3 ....] [value1 value2 value3 ....]
- eval 代表執(zhí)行Lua 語(yǔ)言的命令。
- lua-script 代表Lua 語(yǔ)言腳本內(nèi)容掩蛤。
- key-num 表示參數(shù)中有多少個(gè)key枉昏,需要注意的是Redis 中key 是從1 開始的,如果沒有key 的參數(shù)揍鸟,那么寫0兄裂。
- [key1 key2 key3…]是key 作為參數(shù)傳遞給Lua 語(yǔ)言,也可以不填阳藻,但是需要和key-num 的個(gè)數(shù)對(duì)應(yīng)起來(lái)懦窘。
- [value1 value2 value3 ….]這些參數(shù)傳遞給Lua 語(yǔ)言,它們是可填可不填的稚配。
示例畅涂,返回一個(gè)字符串,0 個(gè)參數(shù):
redis> eval "return 'Hello World'" 0
在Lua 腳本中調(diào)用Redis 命令
使用redis.call(command, key [param1, param2…])進(jìn)行操作道川。語(yǔ)法格式:
redis> eval "redis.call('set',KEYS[1],ARGV[1])" 1 lua-key lua-value
- command 是命令午衰,包括set、get冒萄、del 等臊岸。
- key 是被操作的鍵。
- param1,param2…代表給key 的參數(shù)尊流。
注意跟Java 不一樣帅戒,定義只有形參,調(diào)用只有實(shí)參崖技。
Lua 是在調(diào)用時(shí)用key 表示形參逻住,argv 表示參數(shù)值(實(shí)參)。
設(shè)置鍵值對(duì)
在Redis 中調(diào)用Lua 腳本執(zhí)行Redis 命令
redis> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 wei 1234
redis> get wei
以上命令等價(jià)于set wei 1234迎献。
在redis-cli 中直接寫Lua 腳本不夠方便瞎访,也不能實(shí)現(xiàn)編輯和復(fù)用,通常我們會(huì)把腳本放在文件里面吁恍,然后執(zhí)行這個(gè)文件扒秸。
在Redis 中調(diào)用Lua 腳本文件中的命令,操作Redis
創(chuàng)建Lua 腳本文件:
cd /usr/local/soft/redis5.0.5/src
vim wei.lua
redis.call('set','wei','lua666')
return redis.call('get','wei')
在Redis 客戶端中調(diào)用Lua 腳本
cd /usr/local/soft/redis5.0.5/src
redis-cli --eval wei.lua 0
得到返回值:
"lua666"
案例:對(duì)IP 進(jìn)行限流
需求:在X 秒內(nèi)只能訪問Y 次冀瓦。
設(shè)計(jì)思路:用key 記錄IP伴奥,用value 記錄訪問次數(shù)。
拿到IP 以后翼闽,對(duì)IP+1拾徙。如果是第一次訪問,對(duì)key 設(shè)置過期時(shí)間(參數(shù)1)肄程。否則判斷次數(shù)锣吼,超過限定的次數(shù)(參數(shù)2)选浑,返回0。如果沒有超過次數(shù)則返回1玄叠。超過時(shí)間古徒,key 過期之后,可以再次訪問读恃。
KEY[1]是IP隧膘, ARGV[1]是過期時(shí)間X,ARGV[2]是限制訪問的次數(shù)Y寺惫。
-- ip_limit.lua
-- IP 限流疹吃,對(duì)某個(gè)IP 頻率進(jìn)行限制,6 秒鐘訪問10 次
local num=redis.call('incr',KEYS[1])
if tonumber(num)==1 then
redis.call('expire',KEYS[1],ARGV[1])
return 1
elseif tonumber(num)>tonumber(ARGV[2]) then
return 0
else
return 1
end
6 秒鐘內(nèi)限制訪問10 次西雀,調(diào)用測(cè)試(連續(xù)調(diào)用10 次):
./redis-cli --eval "ip_limit.lua" app:ip:limit:192.168.1.102 , 6 10
- app:ip:limit:192.168.1.102 是key 值萨驶,后面是參數(shù)值,中間要加上一個(gè)空格和
一個(gè)逗號(hào)艇肴,再加上一個(gè)空格腔呜。
即:./redis-cli –eval [lua 腳本] [key…]空格,空格[args…] - 多個(gè)參數(shù)之間用一個(gè)空格分割。
緩存Lua 腳本
在腳本比較長(zhǎng)的情況下再悼,如果每次調(diào)用腳本都需要把整個(gè)腳本傳給Redis 服務(wù)端核畴,會(huì)產(chǎn)生比較大的網(wǎng)絡(luò)開銷。為了解決這個(gè)問題冲九,Redis 提供了EVALSHA 命令谤草,允許開發(fā)者通過腳本內(nèi)容的SHA1 摘要來(lái)執(zhí)行腳本。
Redis 在執(zhí)行script load 命令時(shí)會(huì)計(jì)算腳本的SHA1 摘要并記錄在腳本緩存中莺奸,執(zhí)行EVALSHA 命令時(shí)Redis 會(huì)根據(jù)提供的摘要從腳本緩存中查找對(duì)應(yīng)的腳本內(nèi)容丑孩,如果找到了則執(zhí)行腳本,否則會(huì)返回錯(cuò)誤:"NOSCRIPT No matching script. Please useEVAL."
127.0.0.1:6379> script load "return 'Hello World'"
"470877a599ac74fbfda41caa908de682c5fc7d4b"
127.0.0.1:6379> evalsha "470877a599ac74fbfda41caa908de682c5fc7d4b" 0
"Hello World"
腳本超時(shí)
Redis 的指令執(zhí)行本身是單線程的憾筏,這個(gè)線程還要執(zhí)行客戶端的Lua 腳本嚎杨,如果Lua腳本執(zhí)行超時(shí)或者陷入了死循環(huán),是不是沒有辦法為客戶端提供服務(wù)了呢氧腰?
eval 'while(true) do end' 0
為了防止某個(gè)腳本執(zhí)行時(shí)間過長(zhǎng)導(dǎo)致Redis 無(wú)法提供服務(wù), Redis 提供了lua-time-limit 參數(shù)限制腳本的最長(zhǎng)運(yùn)行時(shí)間刨肃,默認(rèn)為5 秒鐘古拴。
lua-time-limit 5000(redis.conf 配置文件中)
當(dāng)腳本運(yùn)行時(shí)間超過這一限制后,Redis 將開始接受其他命令但不會(huì)執(zhí)行(以確保腳本的原子性真友,因?yàn)榇藭r(shí)腳本并沒有被終止)黄痪,而是會(huì)返回“BUSY”錯(cuò)誤。
Redis 提供了一個(gè)script kill 的命令來(lái)中止腳本的執(zhí)行盔然。新開一個(gè)客戶端:
script kill
如果當(dāng)前執(zhí)行的Lua 腳本對(duì)Redis 的數(shù)據(jù)進(jìn)行了修改(SET桅打、DEL 等)是嗜,那么通過script kill 命令是不能終止腳本運(yùn)行的。
127.0.0.1:6379> eval "redis.call('set','wei','666') while true do end" 0
因?yàn)橐WC腳本運(yùn)行的原子性挺尾,如果腳本執(zhí)行了一部分終止鹅搪,那就違背了腳本原子性的要求。最終要保證腳本要么都執(zhí)行遭铺,要么都不執(zhí)行丽柿。
127.0.0.1:6379> script kill
(error) UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the scripttermination or kill the server in a hard way using the SHUTDOWN NOSAVE command.
遇到這種情況,只能通過shutdown nosave 命令來(lái)強(qiáng)行終止redis魂挂。
shutdown nosave 和shutdown 的區(qū)別在于shutdown nosave 不會(huì)進(jìn)行持久化操作甫题,意味著發(fā)生在上一次快照后的數(shù)據(jù)庫(kù)修改都會(huì)丟失。
Redis 為什么這么快
Redis 到底有多快涂召?
https://redis.io/topics/benchmarks
cd /usr/local/soft/redis-5.0.5/src
redis-benchmark -t set,lpush -n 100000 -q
結(jié)果(本地虛擬機(jī)):
SET: 51813.47 requests per second —— 每秒鐘處理5 萬(wàn)多次set 請(qǐng)求
LPUSH: 51706.31 requests per second —— 每秒鐘處理5 萬(wàn)多次lpush 請(qǐng)求
redis-benchmark -n 100000 -q script load "redis.call('set','foo','bar')"
結(jié)果(本地虛擬機(jī)):
script load redis.call('set','foo','bar'): 46816.48 requests per second —— 每秒鐘46000 次lua 腳本調(diào)用
根據(jù)官方的數(shù)據(jù)坠非,Redis 的QPS 可以達(dá)到10 萬(wàn)左右(每秒請(qǐng)求數(shù))。
Redis 為什么這么快果正?
總結(jié):1)純內(nèi)存結(jié)構(gòu)麻顶、2)單線程、3)多路復(fù)用
- 內(nèi)存
KV 結(jié)構(gòu)的內(nèi)存數(shù)據(jù)庫(kù)舱卡,時(shí)間復(fù)雜度O(1)辅肾。
第二個(gè),要實(shí)現(xiàn)這么高的并發(fā)性能轮锥,是不是要?jiǎng)?chuàng)建非常多的線程矫钓?
恰恰相反,Redis 是單線程的舍杜。 - 單線程有什么好處呢新娜?
1、沒有創(chuàng)建線程既绩、銷毀線程帶來(lái)的消耗
2概龄、避免了上線文切換導(dǎo)致的CPU 消耗
3、避免了線程之間帶來(lái)的競(jìng)爭(zhēng)問題饲握,例如加鎖釋放鎖死鎖等等 - 異步非阻塞
異步非阻塞I/O私杜,多路復(fù)用處理并發(fā)連接
Redis 為什么是單線程的?
不是白白浪費(fèi)了CPU 的資源嗎救欧?
https://redis.io/topics/faq#redis-is-single-threaded-how-can-i-exploit-multiple-cpu--cores
因?yàn)閱尉€程已經(jīng)夠用了衰粹,CPU 不是redis 的瓶頸。Redis 的瓶頸最有可能是機(jī)器內(nèi)存或者網(wǎng)絡(luò)帶寬笆怠。既然單線程容易實(shí)現(xiàn)铝耻,而且CPU 不會(huì)成為瓶頸,那就順理成章地采用單線程的方案了蹬刷。
單線程為什么這么快瓢捉?
因?yàn)镽edis 是基于內(nèi)存的操作频丘,我們先從內(nèi)存開始說(shuō)起。
虛擬存儲(chǔ)器(虛擬內(nèi)存Vitual Memory)
名詞解釋:主存:內(nèi)存泡态;輔存:磁盤(硬盤)
計(jì)算機(jī)主存(內(nèi)存)可看作一個(gè)由M 個(gè)連續(xù)的字節(jié)大小的單元組成的數(shù)組搂漠,每個(gè)字節(jié)有一個(gè)唯一的地址,這個(gè)地址叫做物理地址(PA)兽赁。早期的計(jì)算機(jī)中状答,如果CPU 需要內(nèi)存,使用物理尋址刀崖,直接訪問主存儲(chǔ)器惊科。
這種方式有幾個(gè)弊端:
1、在多用戶多任務(wù)操作系統(tǒng)中亮钦,所有的進(jìn)程共享主存馆截,如果每個(gè)進(jìn)程都獨(dú)占一塊物理地址空間,主存很快就會(huì)被用完蜂莉。我們希望在不同的時(shí)刻蜡娶,不同的進(jìn)程可以共用同一塊物理地址空間。
2映穗、如果所有進(jìn)程都是直接訪問物理內(nèi)存窖张,那么一個(gè)進(jìn)程就可以修改其他進(jìn)程的內(nèi)存數(shù)據(jù),導(dǎo)致物理地址空間被破壞蚁滋,程序運(yùn)行就會(huì)出現(xiàn)異常宿接。
為了解決這些問題,我們就想了一個(gè)辦法辕录,在CPU 和主存之間增加一個(gè)中間層睦霎。CPU不再使用物理地址訪問,而是訪問一個(gè)虛擬地址走诞,由這個(gè)中間層把地址轉(zhuǎn)換成物理地址副女,最終獲得數(shù)據(jù)。這個(gè)中間層就叫做虛擬存儲(chǔ)器(Virtual Memory)蚣旱。
具體的操作如下所示:
在每一個(gè)進(jìn)程開始創(chuàng)建的時(shí)候碑幅,都會(huì)分配一段虛擬地址,然后通過虛擬地址和物理地址的映射來(lái)獲取真實(shí)數(shù)據(jù)姻锁,這樣進(jìn)程就不會(huì)直接接觸到物理地址枕赵,甚至不知道自己調(diào)用的哪塊物理地址的數(shù)據(jù)。
目前位隶,大多數(shù)操作系統(tǒng)都使用了虛擬內(nèi)存,如Windows 系統(tǒng)的虛擬內(nèi)存开皿、Linux 系統(tǒng)的交換空間等等涧黄。Windows 的虛擬內(nèi)存(pagefile.sys)是磁盤空間的一部分篮昧。
在32 位的系統(tǒng)上,虛擬地址空間大小是2^32bit=4G笋妥。在64 位系統(tǒng)上懊昨,最大虛擬地址空間大小是多少?是不是2^64bit=1024*1014TB=1024PB=16EB春宣?實(shí)際上沒有用到64 位酵颁,因?yàn)橛貌坏竭@么大的空間,而且會(huì)造成很大的系統(tǒng)開銷月帝。Linux 一般用低48 位來(lái)表示虛擬地址空間躏惋,也就是2^48bit=256T。
cat /proc/cpuinfo
address sizes : 40 bits physical, 48 bits virtual
實(shí)際的物理內(nèi)存可能遠(yuǎn)遠(yuǎn)小于虛擬內(nèi)存的大小嚷辅。
總結(jié):引入虛擬內(nèi)存簿姨,可以提供更大的地址空間,并且地址空間是連續(xù)的簸搞,使得程序編寫扁位、鏈接更加簡(jiǎn)單。并且可以對(duì)物理內(nèi)存進(jìn)行隔離趁俊,不同的進(jìn)程操作互不影響域仇。還可以通過把同一塊物理內(nèi)存映射到不同的虛擬地址空間實(shí)現(xiàn)內(nèi)存共享。
用戶空間和內(nèi)核空間
為了避免用戶進(jìn)程直接操作內(nèi)核寺擂,保證內(nèi)核安全暇务,操作系統(tǒng)將虛擬內(nèi)存劃分為兩部分,一部分是內(nèi)核空間(Kernel-space)/?k??nl /沽讹,一部分是用戶空間(User-space)般卑。
內(nèi)核是操作系統(tǒng)的核心,獨(dú)立于普通的應(yīng)用程序爽雄,可以訪問受保護(hù)的內(nèi)存空間蝠检,也有訪問底層硬件設(shè)備的權(quán)限。
內(nèi)核空間中存放的是內(nèi)核代碼和數(shù)據(jù)挚瘟,而進(jìn)程的用戶空間中存放的是用戶程序的代碼和數(shù)據(jù)叹谁。不管是內(nèi)核空間還是用戶空間,它們都處于虛擬空間中乘盖,都是對(duì)物理地址的映射焰檩。
在Linux 系統(tǒng)中, 內(nèi)核進(jìn)程和用戶進(jìn)程所占的虛擬內(nèi)存比例是1:3。
當(dāng)進(jìn)程運(yùn)行在內(nèi)核空間時(shí)就處于內(nèi)核態(tài)订框,而進(jìn)程運(yùn)行在用戶空間時(shí)則處于用戶態(tài)析苫。
進(jìn)程在內(nèi)核空間以執(zhí)行任意命令,調(diào)用系統(tǒng)的一切資源;在用戶空間只能執(zhí)行簡(jiǎn)單的運(yùn)算衩侥,不能直接調(diào)用系統(tǒng)資源国旷,必須通過系統(tǒng)接口(又稱system call),才能向內(nèi)核發(fā)出指令茫死。
top 命令:
us 代表CPU 消耗在User space 的時(shí)間百分比;
sy 代表CPU 消耗在Kernel space 的時(shí)間百分比跪但。
進(jìn)程切換(上下文切換)
任務(wù)操作系統(tǒng)是怎么實(shí)現(xiàn)運(yùn)行遠(yuǎn)大于CPU 數(shù)量的任務(wù)個(gè)數(shù)的?當(dāng)然峦萎,這些任務(wù)實(shí)際上并不是真的在同時(shí)運(yùn)行屡久,而是因?yàn)橄到y(tǒng)通過時(shí)間片分片算法,在很短的時(shí)間內(nèi)爱榔,將CPU 輪流分配給它們被环,造成多任務(wù)同時(shí)運(yùn)行的錯(cuò)覺。
為了控制進(jìn)程的執(zhí)行搓蚪,內(nèi)核必須有能力掛起正在CPU 上運(yùn)行的進(jìn)程蛤售,并恢復(fù)以前掛起的某個(gè)進(jìn)程的執(zhí)行。這種行為被稱為進(jìn)程切換妒潭。
什么叫上下文悴能?
在每個(gè)任務(wù)運(yùn)行前,CPU 都需要知道任務(wù)從哪里加載雳灾、又從哪里開始運(yùn)行漠酿,也就是說(shuō),需要系統(tǒng)事先幫它設(shè)置好CPU 寄存器和程序計(jì)數(shù)器(Program Counter)谎亩,這個(gè)叫做CPU 的上下文炒嘲。
而這些保存下來(lái)的上下文,會(huì)存儲(chǔ)在系統(tǒng)內(nèi)核中匈庭,并在任務(wù)重新調(diào)度執(zhí)行時(shí)再次加載進(jìn)來(lái)夫凸。這樣就能保證任務(wù)原來(lái)的狀態(tài)不受影響,讓任務(wù)看起來(lái)還是連續(xù)運(yùn)行阱持。
在切換上下文的時(shí)候夭拌,需要完成一系列的工作,這是一個(gè)很消耗資源的操作衷咽。
進(jìn)程的阻塞
正在運(yùn)行的進(jìn)程由于提出系統(tǒng)服務(wù)請(qǐng)求(如I/O 操作)鸽扁,但因?yàn)槟撤N原因未得到操作系統(tǒng)的立即響應(yīng),該進(jìn)程只能把自己變成阻塞狀態(tài)镶骗,等待相應(yīng)的事件出現(xiàn)后才被喚醒桶现。
進(jìn)程在阻塞狀態(tài)不占用CPU 資源。
文件描述符FD
Linux 系統(tǒng)將所有設(shè)備都當(dāng)作文件來(lái)處理鼎姊,而Linux 用文件描述符來(lái)標(biāo)識(shí)每個(gè)文件對(duì)象骡和。
文件描述符(File Descriptor)是內(nèi)核為了高效管理已被打開的文件所創(chuàng)建的索引相赁,用于指向被打開的文件,所有執(zhí)行I/O 操作的系統(tǒng)調(diào)用都通過文件描述符即横;文件描述符是一個(gè)簡(jiǎn)單的非負(fù)整數(shù)噪生,用以表明每個(gè)被進(jìn)程打開的文件裆赵。
Linux 系統(tǒng)里面有三個(gè)標(biāo)準(zhǔn)文件描述符东囚。
0:標(biāo)準(zhǔn)輸入(鍵盤);1:標(biāo)準(zhǔn)輸出(顯示器)战授;2:標(biāo)準(zhǔn)錯(cuò)誤輸出(顯示器)页藻。
傳統(tǒng)I/O 數(shù)據(jù)拷貝
以讀操作為例:
當(dāng)應(yīng)用程序執(zhí)行read 系統(tǒng)調(diào)用讀取文件描述符(FD)的時(shí)候,如果這塊數(shù)據(jù)已經(jīng)存在于用戶進(jìn)程的頁(yè)內(nèi)存中植兰,就直接從內(nèi)存中讀取數(shù)據(jù)份帐。如果數(shù)據(jù)不存在,則先將數(shù)據(jù)從磁盤加載數(shù)據(jù)到內(nèi)核緩沖區(qū)中楣导,再?gòu)膬?nèi)核緩沖區(qū)拷貝到用戶進(jìn)程的頁(yè)內(nèi)存中废境。(兩次拷貝,兩次user 和kernel 的上下文切換)筒繁。
I/O 的阻塞到底阻塞在哪里噩凹?
Blocking I/O
當(dāng)使用read 或write 對(duì)某個(gè)文件描述符進(jìn)行過讀寫時(shí),如果當(dāng)前FD 不可讀毡咏,系統(tǒng)就不會(huì)對(duì)其他的操作做出響應(yīng)驮宴。從設(shè)備復(fù)制數(shù)據(jù)到內(nèi)核緩沖區(qū)是阻塞的,從內(nèi)核緩沖區(qū)拷貝到用戶空間呕缭,也是阻塞的堵泽,直到copy complete,內(nèi)核返回結(jié)果恢总,用戶進(jìn)程才解除block 的狀態(tài)迎罗。
為了解決阻塞的問題,我們有幾個(gè)思路片仿。
1纹安、在服務(wù)端創(chuàng)建多個(gè)線程或者使用線程池,但是在高并發(fā)的情況下需要的線程會(huì)很多滋戳,系統(tǒng)無(wú)法承受钻蔑,而且創(chuàng)建和釋放線程都需要消耗資源。
2奸鸯、由請(qǐng)求方定期輪詢咪笑,在數(shù)據(jù)準(zhǔn)備完畢后再?gòu)膬?nèi)核緩存緩沖區(qū)復(fù)制數(shù)據(jù)到用戶空間(非阻塞式I/O),這種方式會(huì)存在一定的延遲娄涩。
能不能用一個(gè)線程處理多個(gè)客戶端請(qǐng)求窗怒?
I/O 多路復(fù)用(I/O Multiplexing)
I/O 指的是網(wǎng)絡(luò)I/O映跟。
多路指的是多個(gè)TCP 連接(Socket 或Channel)。
復(fù)用指的是復(fù)用一個(gè)或多個(gè)線程扬虚。
它的基本原理就是不再由應(yīng)用程序自己監(jiān)視連接努隙,而是由內(nèi)核替應(yīng)用程序監(jiān)視文件描述符。
客戶端在操作的時(shí)候辜昵,會(huì)產(chǎn)生具有不同事件類型的socket荸镊。在服務(wù)端,I/O 多路復(fù)用程序(I/O Multiplexing Module)會(huì)把消息放入隊(duì)列中堪置,然后通過文件事件分派器(Fileevent Dispatcher)躬存,轉(zhuǎn)發(fā)到不同的事件處理器中。
多路復(fù)用有很多的實(shí)現(xiàn)舀锨,以select 為例岭洲,當(dāng)用戶進(jìn)程調(diào)用了多路復(fù)用器,進(jìn)程會(huì)被阻塞坎匿。內(nèi)核會(huì)監(jiān)視多路復(fù)用器負(fù)責(zé)的所有socket盾剩,當(dāng)任何一個(gè)socket 的數(shù)據(jù)準(zhǔn)備好了,多路復(fù)用器就會(huì)返回替蔬。這時(shí)候用戶進(jìn)程再調(diào)用read 操作告私,把數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到用戶空間。
I/O 多路復(fù)用的特點(diǎn)是通過一種機(jī)制一個(gè)進(jìn)程能同時(shí)等待多個(gè)文件描述符进栽,而這些文件描述符(套接字描述符)其中的任意一個(gè)進(jìn)入讀就緒(readable)狀態(tài)德挣,select()函數(shù)就可以返回。
Redis 的多路復(fù)用快毛, 提供了select, epoll, evport, kqueue 幾種選擇格嗅,在編譯的時(shí)候來(lái)選擇一種。源碼ae.c
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif
evport 是Solaris 系統(tǒng)內(nèi)核提供支持的唠帝;
epoll 是LINUX 系統(tǒng)內(nèi)核提供支持的屯掖;
kqueue 是Mac 系統(tǒng)提供支持的;
select 是POSIX 提供的襟衰,一般的操作系統(tǒng)都有支撐(保底方案)贴铜;
源碼:ae_epoll.c、ae_select.c瀑晒、ae_kqueue.c绍坝、ae_evport.c
內(nèi)存回收
Reids 所有的數(shù)據(jù)都是存儲(chǔ)在內(nèi)存中的,在某些情況下需要對(duì)占用的內(nèi)存空間進(jìn)行回收苔悦。內(nèi)存回收主要分為兩類轩褐,一類是key 過期,一類是內(nèi)存使用達(dá)到上限(max_memory)觸發(fā)內(nèi)存淘汰玖详。
過期策略
- 定時(shí)過期(主動(dòng)淘汰)
每個(gè)設(shè)置過期時(shí)間的key 都需要?jiǎng)?chuàng)建一個(gè)定時(shí)器把介,到過期時(shí)間就會(huì)立即清除勤讽。該策略可以立即清除過期的數(shù)據(jù),對(duì)內(nèi)存很友好拗踢;但是會(huì)占用大量的CPU 資源去處理過期的數(shù)據(jù)脚牍,從而影響緩存的響應(yīng)時(shí)間和吞吐量。 - 惰性過期(被動(dòng)淘汰)
只有當(dāng)訪問一個(gè)key 時(shí)巢墅,才會(huì)判斷該key 是否已過期诸狭,過期則清除。該策略可以最大化地節(jié)省CPU 資源砂缩,卻對(duì)內(nèi)存非常不友好作谚。極端情況可能出現(xiàn)大量的過期key 沒有再次被訪問,從而不會(huì)被清除庵芭,占用大量?jī)?nèi)存。
例如String雀监,在getCommand 里面會(huì)調(diào)用expireIfNeeded
源碼:server.c expireIfNeeded(redisDb *db, robj *key)
第二種情況双吆,每次寫入key 時(shí),發(fā)現(xiàn)內(nèi)存不夠会前,調(diào)用activeExpireCycle 釋放一部分內(nèi)存好乐。
源碼:expire.c activeExpireCycle(int type)
- 定期過期
源碼:server.h
typedef struct redisDb {
dict *dict; /* 所有的鍵值對(duì)*/
dict *expires; /* 設(shè)置了過期時(shí)間的鍵值對(duì)*/
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
每隔一定的時(shí)間,會(huì)掃描一定數(shù)量的數(shù)據(jù)庫(kù)的expires 字典中一定數(shù)量的key瓦宜,并清除其中已過期的key蔚万。該策略是前兩者的一個(gè)折中方案。通過調(diào)整定時(shí)掃描的時(shí)間間隔和每次掃描的限定耗時(shí)临庇,可以在不同情況下使得CPU 和內(nèi)存資源達(dá)到最優(yōu)的平衡效果反璃。
Redis 中同時(shí)使用了惰性過期和定期過期兩種過期策略。
淘汰策略
Redis 的內(nèi)存淘汰策略假夺,是指當(dāng)內(nèi)存使用達(dá)到最大內(nèi)存極限時(shí)淮蜈,需要使用淘汰算法來(lái)決定清理掉哪些數(shù)據(jù),以保證新數(shù)據(jù)的存入已卷。
- 最大內(nèi)存設(shè)置
redis.conf 參數(shù)配置:
# maxmemory <bytes>
如果不設(shè)置maxmemory 或者設(shè)置為0梧田,64 位系統(tǒng)不限制內(nèi)存,32 位系統(tǒng)最多使用3GB 內(nèi)存侧蘸。
動(dòng)態(tài)修改:
redis> config set maxmemory 2GB
到達(dá)最大內(nèi)存以后怎么辦裁眯?
- 淘汰策略
https://redis.io/topics/lru-cache
redis.conf
# maxmemory-policy noeviction
# volatile-lru -> Evict using approximated LRU among the keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key among the ones with an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.
先從算法來(lái)看:
LRU,Least Recently Used:最近最少使用讳癌。判斷最近被使用的時(shí)間穿稳,目前最遠(yuǎn)的數(shù)據(jù)優(yōu)先被淘汰。
LFU析桥,Least Frequently Used司草,最不常用艰垂,4.0 版本新增。
random埋虹,隨機(jī)刪除猜憎。
volatile-lru 根據(jù)LRU 算法刪除設(shè)置了超時(shí)屬性(expire)的鍵壕吹,直到騰出足夠內(nèi)存為止本缠。如果沒有
可刪除的鍵對(duì)象,回退到noeviction 策略肮疗。
allkeys-lru 根據(jù)LRU 算法刪除鍵爬泥,不管數(shù)據(jù)有沒有設(shè)置超時(shí)屬性柬讨,直到騰出足夠內(nèi)存為止。
volatile-lfu 在帶有過期時(shí)間的鍵中選擇最不常用的袍啡。
allkeys-lfu 在所有的鍵中選擇最不常用的踩官,不管數(shù)據(jù)有沒有設(shè)置超時(shí)屬性。
volatile-random 在帶有過期時(shí)間的鍵中隨機(jī)選擇境输。
allkeys-random 隨機(jī)刪除所有鍵蔗牡,直到騰出足夠內(nèi)存為止。
volatile-ttl 根據(jù)鍵值對(duì)象的ttl 屬性嗅剖,刪除最近將要過期數(shù)據(jù)辩越。如果沒有,回退到noeviction 策略信粮。
noeviction 默認(rèn)策略黔攒,不會(huì)刪除任何數(shù)據(jù),拒絕所有寫入操作并返回客戶端錯(cuò)誤信息(error)OOM command not allowed when used memory强缘,此時(shí)Redis 只響應(yīng)讀操作督惰。
如果沒有符合前提條件的key 被淘汰,那么volatile-lru欺旧、volatile-random 姑丑、volatile-ttl 相當(dāng)于noeviction(不做內(nèi)存回收)。
動(dòng)態(tài)修改淘汰策略:
redis> config set maxmemory-policy volatile-lru
建議使用volatile-lru辞友,在保證正常服務(wù)的情況下栅哀,優(yōu)先刪除最近最少使用的key。
- LRU 淘汰原理
需要額外的數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)称龙,消耗內(nèi)存留拾。
Redis LRU 對(duì)傳統(tǒng)的LRU 算法進(jìn)行了改良,通過隨機(jī)采樣來(lái)調(diào)整算法的精度鲫尊。
如果淘汰策略是LRU痴柔,則根據(jù)配置的采樣值maxmemory_samples(默認(rèn)是5 個(gè)),隨機(jī)從數(shù)據(jù)庫(kù)中選擇m 個(gè)key, 淘汰其中熱度最低的key 對(duì)應(yīng)的緩存數(shù)據(jù)。所以采樣參數(shù)m 配置的數(shù)值越大, 就越能精確的查找到待淘汰的緩存數(shù)據(jù),但是也消耗更多的CPU 計(jì)算,執(zhí)行效率降低疫向。
如何找出熱度最低的數(shù)據(jù)咳蔚?
Redis 中所有對(duì)象結(jié)構(gòu)都有一個(gè)lru 字段, 且使用了unsigned 的低24 位豪嚎,這個(gè)字段用來(lái)記錄對(duì)象的熱度。對(duì)象被創(chuàng)建時(shí)會(huì)記錄lru 值谈火。在被訪問的時(shí)候也會(huì)更新lru 的值侈询。
但是不是獲取系統(tǒng)當(dāng)前的時(shí)間戳,而是設(shè)置為全局變量server.lruclock 的值糯耍。
源碼:server.h
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
} robj;
server.lruclock 的值怎么來(lái)的扔字?
Redis 中有個(gè)定時(shí)處理的函數(shù)serverCron , 默認(rèn)每100 毫秒調(diào)用函數(shù)
updateCachedTime 更新一次全局變量的server.lruclock 的值温技,它記錄的是當(dāng)前unix時(shí)間戳革为。
源碼:server.c
void updateCachedTime(void) {
time_t unixtime = time(NULL);
atomicSet(server.unixtime,unixtime);
server.mstime = mstime();
struct tm tm;
localtime_r(&server.unixtime,&tm);
server.daylight_active = tm.tm_isdst;
}
為什么不獲取精確的時(shí)間而是放在全局變量中?不會(huì)有延遲的問題嗎舵鳞?
這樣函數(shù)lookupKey 中更新數(shù)據(jù)的lru 熱度值時(shí),就不用每次調(diào)用系統(tǒng)函數(shù)time震檩,可以提高執(zhí)行效率。
OK系任,當(dāng)對(duì)象里面已經(jīng)有了LRU 字段的值恳蹲,就可以評(píng)估對(duì)象的熱度了。
函數(shù)estimateObjectIdleTime 評(píng)估指定對(duì)象的lru 熱度俩滥,思想就是對(duì)象的lru 值和全局的server.lruclock 的差值越大(越久沒有得到更新), 該對(duì)象熱度越低贺奠。
源碼evict.c
/* Given an object returns the min number of milliseconds the object was never
* requested, using an approximated LRU algorithm. */
unsigned long long estimateObjectIdleTime(robj *o) {
unsigned long long lruclock = LRU_CLOCK();
if (lruclock >= o->lru) {
return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;
} else {
return (lruclock + (LRU_CLOCK_MAX - o->lru)) *
LRU_CLOCK_RESOLUTION;
}
}
server.lruclock 只有24 位霜旧,按秒為單位來(lái)表示才能存儲(chǔ)194 天。當(dāng)超過24bit 能表示的最大時(shí)間的時(shí)候儡率,它會(huì)從頭開始計(jì)算挂据。
server.h
#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */
在這種情況下,可能會(huì)出現(xiàn)對(duì)象的lru 大于server.lruclock 的情況儿普,如果這種情況出現(xiàn)那么就兩個(gè)相加而不是相減來(lái)求最久的key崎逃。
為什么不用常規(guī)的哈希表+雙向鏈表的方式實(shí)現(xiàn)?需要額外的數(shù)據(jù)結(jié)構(gòu)眉孩,消耗資源个绍。
而Redis LRU 算法在sample 為10 的情況下,已經(jīng)能接近傳統(tǒng)LRU 算法了浪汪。
https://redis.io/topics/lru-cache
除了消耗資源之外巴柿,傳統(tǒng)LRU 還有什么問題?
如圖死遭,假設(shè)A 在10 秒內(nèi)被訪問了5 次广恢,而B 在10 秒內(nèi)被訪問了3 次。因?yàn)锽 最后一次被訪問的時(shí)間比A 要晚呀潭,在同等的情況下钉迷,A 反而先被回收至非。
要實(shí)現(xiàn)基于訪問頻率的淘汰機(jī)制,怎么做糠聪?
- LFU
server.h
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
} robj;
當(dāng)這24 bits 用作LFU 時(shí)荒椭,其被分為兩部分:
高16 位用來(lái)記錄訪問時(shí)間(單位為分鐘,ldt枷颊,last decrement time)
低8 位用來(lái)記錄訪問頻率戳杀,簡(jiǎn)稱counter(logc,logistic counter)
counter 是用基于概率的對(duì)數(shù)計(jì)數(shù)器實(shí)現(xiàn)的夭苗,8 位可以表示百萬(wàn)次的訪問頻率信卡。
對(duì)象被讀寫的時(shí)候,lfu 的值會(huì)被更新题造。
db.c——lookupKey
void updateLFU(robj *val) {
unsigned long counter = LFUDecrAndReturn(val);
counter = LFULogIncr(counter);
val->lru = (LFUGetTimeInMinutes()<<8) | counter;
}
增長(zhǎng)的速率由傍菇,lfu-log-factor 越大,counter 增長(zhǎng)的越慢
redis.conf 配置文件
# lfu-log-factor 10
如果計(jì)數(shù)器只會(huì)遞增不會(huì)遞減界赔,也不能體現(xiàn)對(duì)象的熱度丢习。沒有被訪問的時(shí)候,計(jì)數(shù)器怎么遞減呢淮悼?
減少的值由衰減因子lfu-decay-time(分鐘)來(lái)控制咐低,如果值是1 的話,N 分鐘沒有訪問就要減少N袜腥。
redis.conf 配置文件
# lfu-decay-time 1
持久化機(jī)制
https://redis.io/topics/persistence
Redis 速度快见擦,很大一部分原因是因?yàn)樗械臄?shù)據(jù)都存儲(chǔ)在內(nèi)存中。如果斷電或者宕機(jī)羹令,都會(huì)導(dǎo)致內(nèi)存中的數(shù)據(jù)丟失鲤屡。為了實(shí)現(xiàn)重啟后數(shù)據(jù)不丟失,Redis 提供了兩種持久化的方案福侈,一種是RDB 快照(Redis DataBase)酒来,一種是AOF(Append Only File)。
RDB
RDB 是Redis 默認(rèn)的持久化方案肪凛。當(dāng)滿足一定條件的時(shí)候堰汉,會(huì)把當(dāng)前內(nèi)存中的數(shù)據(jù)寫入磁盤,生成一個(gè)快照文件dump.rdb显拜。Redis 重啟會(huì)通過加載dump.rdb 文件恢復(fù)數(shù)據(jù)衡奥。
什么時(shí)候?qū)懭雛db 文件?
- 自動(dòng)觸發(fā)
a)配置規(guī)則觸發(fā)远荠。
redis.conf矮固, SNAPSHOTTING,其中定義了觸發(fā)把數(shù)據(jù)保存到磁盤的觸發(fā)頻率。
如果不需要RDB 方案档址,注釋save 或者配置成空字符串""盹兢。
save 900 1 # 900 秒內(nèi)至少有一個(gè)key 被修改(包括添加)
save 300 10 # 400 秒內(nèi)至少有10 個(gè)key 被修改
save 60 10000 # 60 秒內(nèi)至少有10000 個(gè)key 被修改
注意上面的配置是不沖突的,只要滿足任意一個(gè)都會(huì)觸發(fā)守伸。
RDB 文件位置和目錄:
# 文件路徑绎秒,
dir ./
# 文件名稱
dbfilename dump.rdb
# 是否是LZF 壓縮rdb 文件
rdbcompression yes
# 開啟數(shù)據(jù)校驗(yàn)
rdbchecksum yes
dir: rdb 文件默認(rèn)在啟動(dòng)目錄下(相對(duì)路徑)config get dir 獲取
dbfilename: 文件名稱
rdbcompression: 開啟壓縮可以節(jié)省存儲(chǔ)空間,但是會(huì)消耗一些CPU 的計(jì)算時(shí)間尼摹,默認(rèn)開啟
rdbchecksum: 使用CRC64 算法來(lái)進(jìn)行數(shù)據(jù)校驗(yàn)见芹,但是這樣做會(huì)增加大約10%的性能消耗,如果希望獲取到最大的性能提升蠢涝,可以關(guān)閉此功能玄呛。
為什么停止Redis 服務(wù)的時(shí)候沒有save,重啟數(shù)據(jù)還在和二?
RDB 還有兩種觸發(fā)方式:
b)shutdown 觸發(fā)徘铝,保證服務(wù)器正常關(guān)閉。
c)flushall惯吕,RDB 文件是空的惕它,沒什么意義(刪掉dump.rdb 演示一下)。
- 手動(dòng)觸發(fā)
如果我們需要重啟服務(wù)或者遷移數(shù)據(jù)废登,這個(gè)時(shí)候就需要手動(dòng)觸RDB 快照保存淹魄。Redis提供了兩條命令:
a)save
save 在生成快照的時(shí)候會(huì)阻塞當(dāng)前Redis 服務(wù)器, Redis 不能處理其他命令堡距。如果內(nèi)存中的數(shù)據(jù)比較多揭北,會(huì)造成Redis 長(zhǎng)時(shí)間的阻塞。生產(chǎn)環(huán)境不建議使用這個(gè)命令吏颖。
為了解決這個(gè)問題,Redis 提供了第二種方式恨樟。
b)bgsave
執(zhí)行bgsave 時(shí)半醉,Redis 會(huì)在后臺(tái)異步進(jìn)行快照操作,快照同時(shí)還可以響應(yīng)客戶端請(qǐng)求劝术。
具體操作是Redis 進(jìn)程執(zhí)行fork 操作創(chuàng)建子進(jìn)程(copy-on-write)缩多,RDB 持久化過程由子進(jìn)程負(fù)責(zé),完成后自動(dòng)結(jié)束养晋。它不會(huì)記錄fork 之后后續(xù)的命令衬吆。阻塞只發(fā)生在fork 階段,一般時(shí)間很短绳泉。
用lastsave 命令可以查看最近一次成功生成快照的時(shí)間逊抡。 - RDB 數(shù)據(jù)的恢復(fù)
1、shutdown 持久化
添加鍵值
redis> set k1 1
redis> set k2 2
redis> set k3 3
redis> set k4 4
redis> set k5 5
停服務(wù)器零酪,觸發(fā)save
redis> shutdown
備份dump.rdb 文件
cp dump.rdb dump.rdb.bak
啟動(dòng)服務(wù)器
/usr/local/soft/redis-5.0.5/src/redis-server /usr/local/soft/redis-5.0.5/redis.conf
數(shù)據(jù)都在:
redis> keys *
2冒嫡、模擬數(shù)據(jù)丟失
模擬數(shù)據(jù)丟失拇勃,觸發(fā)save
redis> flushall
停服務(wù)器
redis> shutdown
啟動(dòng)服務(wù)器
/usr/local/soft/redis-5.0.5/src/redis-server /usr/local/soft/redis-5.0.5/redis.conf
啥都沒有:
redis> keys *
3、通過備份文件恢復(fù)數(shù)據(jù)
停服務(wù)器
redis> shutdown
重命名備份文件
mv dump.rdb.bak dump.rdb
啟動(dòng)服務(wù)器
/usr/local/soft/redis-5.0.5/src/redis-server /usr/local/soft/redis-5.0.5/redis.conf
查看數(shù)據(jù):
redis> keys *
- RDB 文件的優(yōu)勢(shì)和劣勢(shì)
一孝凌、優(yōu)勢(shì)
1.RDB 是一個(gè)非常緊湊(compact)的文件方咆,它保存了redis 在某個(gè)時(shí)間點(diǎn)上的數(shù)據(jù)集。這種文件非常適合用于進(jìn)行備份和災(zāi)難恢復(fù)蟀架。
2.生成RDB 文件的時(shí)候瓣赂,redis 主進(jìn)程會(huì)fork()一個(gè)子進(jìn)程來(lái)處理所有保存工作,主進(jìn)程不需要進(jìn)行任何磁盤IO 操作片拍。
3.RDB 在恢復(fù)大數(shù)據(jù)集時(shí)的速度比AOF 的恢復(fù)速度要快煌集。
二、劣勢(shì)
1穆碎、RDB 方式數(shù)據(jù)沒辦法做到實(shí)時(shí)持久化/秒級(jí)持久化牙勘。因?yàn)閎gsave 每次運(yùn)行都要執(zhí)行fork 操作創(chuàng)建子進(jìn)程,頻繁執(zhí)行成本過高所禀。
2方面、在一定間隔時(shí)間做一次備份,所以如果redis 意外down 掉的話色徘,就會(huì)丟失最后一次快照之后的所有修改(數(shù)據(jù)有丟失)恭金。
如果數(shù)據(jù)相對(duì)來(lái)說(shuō)比較重要,希望將損失降到最小褂策,則可以使用AOF 方式進(jìn)行持久化横腿。
AOF
Append Only File
AOF:Redis 默認(rèn)不開啟。AOF 采用日志的形式來(lái)記錄每個(gè)寫操作斤寂,并追加到文件中耿焊。開啟后,執(zhí)行更改Redis 數(shù)據(jù)的命令時(shí)遍搞,就會(huì)把命令寫入到AOF 文件中罗侯。
Redis 重啟時(shí)會(huì)根據(jù)日志文件的內(nèi)容把寫指令從前到后執(zhí)行一次以完成數(shù)據(jù)的恢復(fù)工作。
- AOF 配置
配置文件redis.conf
# 開關(guān)
appendonly no
# 文件名
appendfilename "appendonly.aof"
appendonly: Redis 默認(rèn)只開啟RDB 持久化溪猿,開啟AOF 需要修改為yes
appendfilename: "appendonly.aof" 路徑也是通過dir 參數(shù)配置config get dir
由于操作系統(tǒng)的緩存機(jī)制钩杰,AOF 數(shù)據(jù)并沒有真正地寫入硬盤,而是進(jìn)入了系統(tǒng)的硬盤緩存诊县。什么時(shí)候把緩沖區(qū)的內(nèi)容寫入到AOF 文件讲弄?
appendfsync everysec
AOF 持久化策略(硬盤緩存到磁盤),默認(rèn)everysec
no 表示不執(zhí)行fsync依痊,由操作系統(tǒng)保證數(shù)據(jù)同步到磁盤避除,速度最快,但是不太安全;
always 表示每次寫入都執(zhí)行fsync驹饺,以保證數(shù)據(jù)同步到磁盤钳枕,效率很低;
everysec 表示每秒執(zhí)行一次fsync赏壹,可能會(huì)導(dǎo)致丟失這1s 數(shù)據(jù)鱼炒。通常選擇everysec ,兼顧安全性和效率蝌借。
文件越來(lái)越大昔瞧,怎么辦?
由于AOF 持久化是Redis 不斷將寫命令記錄到AOF 文件中菩佑,隨著Redis 不斷的進(jìn)行自晰,AOF 的文件會(huì)越來(lái)越大,文件越大稍坯,占用服務(wù)器內(nèi)存越大以及AOF 恢復(fù)要求時(shí)間越長(zhǎng)酬荞。
例如set wei 666,執(zhí)行1000 次瞧哟,結(jié)果都是wei=666混巧。
為了解決這個(gè)問題,Redis 新增了重寫機(jī)制勤揩,當(dāng)AOF 文件的大小超過所設(shè)定的閾值時(shí)咧党,Redis 就會(huì)啟動(dòng)AOF 文件的內(nèi)容壓縮,只保留可以恢復(fù)數(shù)據(jù)的最小指令集陨亡。
可以使用命令bgrewriteaof 來(lái)重寫傍衡。
AOF 文件重寫并不是對(duì)原文件進(jìn)行重新整理,而是直接讀取服務(wù)器現(xiàn)有的鍵值對(duì)负蠕,然后用一條命令去代替之前記錄這個(gè)鍵值對(duì)的多條命令蛙埂,生成一個(gè)新的文件后去替換原來(lái)的AOF 文件。
# 重寫觸發(fā)機(jī)制
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-rewrite-percentage
默認(rèn)值為100遮糖。aof 自動(dòng)重寫配置箱残,當(dāng)目前aof 文件大小超過上一次重寫的aof 文件大小的百分之多少進(jìn)行重寫,即當(dāng)aof 文件增長(zhǎng)到一定大小的時(shí)候止吁,Redis 能夠調(diào)用bgrewriteaof對(duì)日志文件進(jìn)行重寫。當(dāng)前AOF 文件大小是上次日志重寫得到AOF 文件大小的二倍(設(shè)置為100)時(shí)燎悍,自動(dòng)啟動(dòng)新的日志重寫過程敬惦。
auto-aof-rewrite-min-size
默認(rèn)64M。設(shè)置允許重寫的最小aof 文件大小谈山,避免了達(dá)到約定百分比但尺寸仍然很小的情況還要重寫俄删。
重寫過程中,AOF 文件被更改了怎么辦?
另外有兩個(gè)與AOF 相關(guān)的參數(shù):
no-appendfsync-on-rewrite
在aof 重寫或者寫入rdb 文件的時(shí)候畴椰,會(huì)執(zhí)行大量IO臊诊,此時(shí)對(duì)于everysec 和always 的aof模式來(lái)說(shuō),執(zhí)行fsync 會(huì)造成阻塞過長(zhǎng)時(shí)間斜脂,no-appendfsync-on-rewrite 字段設(shè)置為默認(rèn)設(shè)置為no抓艳。如果對(duì)延遲要求很高的應(yīng)用,這個(gè)字段可以設(shè)置為yes帚戳,否則還是設(shè)置為no玷或,這樣對(duì)持久化特性來(lái)說(shuō)這是更安全的選擇。設(shè)置為yes 表示rewrite 期間對(duì)新寫操作不fsync,暫時(shí)存在內(nèi)存中,等rewrite 完成后再寫入片任,默認(rèn)為no偏友,建議修改為yes。Linux 的默認(rèn)fsync策略是30 秒对供∥凰可能丟失30 秒數(shù)據(jù)。
aof-load-truncated aof
文件可能在尾部是不完整的产场,當(dāng)redis 啟動(dòng)的時(shí)候鹅髓,aof 文件的數(shù)據(jù)被載入內(nèi)存。重啟可能發(fā)生在redis 所在的主機(jī)操作系統(tǒng)宕機(jī)后涝动,尤其在ext4 文件系統(tǒng)沒有加上data=ordered選項(xiàng)迈勋,出現(xiàn)這種現(xiàn)象。redis 宕機(jī)或者異常終止不會(huì)造成尾部不完整現(xiàn)象醋粟,可以選擇讓redis退出靡菇,或者導(dǎo)入盡可能多的數(shù)據(jù)。如果選擇的是yes米愿,當(dāng)截?cái)嗟腶of 文件被導(dǎo)入的時(shí)候厦凤,會(huì)自動(dòng)發(fā)布一個(gè)log 給客戶端然后load。如果是no育苟,用戶必須手動(dòng)redis-check-aof 修復(fù)AOF文件才可以较鼓。默認(rèn)值為yes。
AOF 數(shù)據(jù)恢復(fù)
重啟Redis 之后就會(huì)進(jìn)行AOF 文件的恢復(fù)违柏。AOF 優(yōu)勢(shì)與劣勢(shì)
優(yōu)點(diǎn):
1博烂、AOF 持久化的方法提供了多種的同步頻率,即使使用默認(rèn)的同步頻率每秒同步一次漱竖,Redis 最多也就丟失1 秒的數(shù)據(jù)而已禽篱。
缺點(diǎn):
1、對(duì)于具有相同數(shù)據(jù)的的Redis馍惹,AOF 文件通常會(huì)比RDF 文件體積更大(RDB存的是數(shù)據(jù)快照)躺率。
2玛界、雖然AOF 提供了多種同步的頻率,默認(rèn)情況下悼吱,每秒同步一次的頻率也具有較高的性能慎框。在高并發(fā)的情況下,RDB 比AOF 具好更好的性能保證后添。
兩種方案比較
那么對(duì)于AOF 和RDB 兩種持久化方式笨枯,我們應(yīng)該如何選擇呢?
如果可以忍受一小段時(shí)間內(nèi)數(shù)據(jù)的丟失吕朵,毫無(wú)疑問使用RDB 是最好的猎醇,定時(shí)生成RDB 快照(snapshot)非常便于進(jìn)行數(shù)據(jù)庫(kù)備份, 并且RDB 恢復(fù)數(shù)據(jù)集的速度也要比AOF 恢復(fù)的速度要快努溃。
否則就使用AOF 重寫硫嘶。但是一般情況下建議不要單獨(dú)使用某一種持久化機(jī)制,而是應(yīng)該兩種一起用梧税,在這種情況下,當(dāng)redis 重啟的時(shí)候會(huì)優(yōu)先載入AOF 文件來(lái)恢復(fù)原始的數(shù)據(jù)沦疾,因?yàn)樵谕ǔG闆r下AOF 文件保存的數(shù)據(jù)集要比RDB 文件保存的數(shù)據(jù)集要完整。
——學(xué)自咕泡學(xué)院