Redis原理2-高級特性、單線程工作機(jī)制

發(fā)布訂閱模式

列表的局限

通過隊(duì)列的rpush 和lpop 可以實(shí)現(xiàn)消息隊(duì)列(隊(duì)尾進(jìn)隊(duì)頭出),但是消費(fèi)者需要不停地調(diào)用lpop 查看List 中是否有等待處理的消息(比如寫一個while 循環(huán))。為了減少通信的消耗,可以sleep()一段時間再消費(fèi),但是會有兩個問題:
1、如果生產(chǎn)者生產(chǎn)消息的速度遠(yuǎn)大于消費(fèi)者消費(fèi)消息的速度,List 會占用大量的內(nèi)存。
2、消息的實(shí)時性降低。

list 還提供了一個阻塞的命令:blpop,沒有任何元素可以彈出的時候,連接會被阻
塞。

blpop queue 5

基于list 實(shí)現(xiàn)的消息隊(duì)列,不支持一對多的消息分發(fā)。

發(fā)布訂閱模式

除了通過list 實(shí)現(xiàn)消息隊(duì)列之外,Redis 還提供了一組命令實(shí)現(xiàn)發(fā)布/訂閱模式。
這種方式,發(fā)送者和接收者沒有直接關(guān)聯(lián)(實(shí)現(xiàn)了解耦),接收者也不需要持續(xù)嘗試獲取消息。

訂閱頻道

首先,我們有很多的頻道(channel),我們也可以把這個頻道理解成queue。訂閱者可以訂閱一個或者多個頻道。消息的發(fā)布者(生產(chǎn)者)可以給指定的頻道發(fā)布消息。
只要有消息到達(dá)了頻道,所有訂閱了這個頻道的訂閱者都會收到這條消息。
需要注意的注意是,發(fā)出去的消息不會被持久化,因?yàn)樗呀?jīng)從隊(duì)列里面移除了,所以消費(fèi)者只能收到它開始訂閱這個頻道之后發(fā)布的消息。
下面我們來看一下發(fā)布訂閱命令的使用方法。
訂閱者訂閱頻道:可以一次訂閱多個,比如這個客戶端訂閱了3 個頻道。

subscribe channel-1 channel-2 channel-3

發(fā)布者可以向指定頻道發(fā)布消息(并不支持一次向多個頻道發(fā)送消息):

publish channel-1 2673

取消訂閱(不能在訂閱狀態(tài)下使用):

unsubscribe channel-1

按規(guī)則(Pattern)訂閱頻道

支持?和占位符。?代表一個字符,代表0 個或者多個字符。
消費(fèi)端1,關(guān)注運(yùn)動信息:

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 的單個命令是原子性的(比如get set mget mset),如果涉及到多個命令的時候,需要把多個命令作為一個不可分割的處理序列,就需要用到事務(wù)。
例如我們之前說的用setnx 實(shí)現(xiàn)分布式鎖,我們先set,然后設(shè)置對key 設(shè)置expire,防止del 發(fā)生異常的時候鎖不會被釋放,業(yè)務(wù)處理完了以后再del,這三個動作我們就希望它們作為一組命令執(zhí)行。
Redis 的事務(wù)有兩個特點(diǎn):
1、按進(jìn)入隊(duì)列的順序執(zhí)行。
2、不會受到其他客戶端的請求的影響。
Redis 的事務(wù)涉及到四個命令:multi(開啟事務(wù)),exec(執(zhí)行事務(wù)),discard(取消事務(wù)),watch(監(jiān)視)

事務(wù)的用法

案例場景: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ù)不能嵌套,多個multi 命令效果一樣。
multi 執(zhí)行后,客戶端可以繼續(xù)向服務(wù)器發(fā)送任意多條命令, 這些命令不會立即被執(zhí)行, 而是被放到一個隊(duì)列中, 當(dāng)exec 命令被調(diào)用時, 所有隊(duì)列中的命令才會被執(zhí)行。
通過exec 的命令執(zhí)行事務(wù)。如果沒有執(zhí)行exec,所有的命令都不會被執(zhí)行。
如果中途不想執(zhí)行事務(wù)了,怎么辦?
可以調(diào)用discard 可以清空事務(wù)隊(duì)列,放棄執(zhí)行。

multi
set k1 1
set k2 2
set k3 3
discard

watch 命令

在Redis 中還提供了一個watch 命令。
它可以為Redis 事務(wù)提供CAS 樂觀鎖行為(Check and Set / Compare andSwap),也就是多個線程更新變量的時候,會跟原值做比較,只有它沒有被其他線程修改的情況下,才更新成新的值。
我們可以用watch 監(jiān)視一個或者多個key,如果開啟事務(wù)之后,至少有一個被監(jiān)視key 鍵在exec 執(zhí)行之前被修改了, 那么整個事務(wù)都會被取消(key 提前過期除外)??梢杂胾nwatch 取消。

先在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

再新開一個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ā)生錯誤,一種是在執(zhí)行exec 之后發(fā)生錯誤。

在執(zhí)行exec 之前發(fā)生錯誤

比如:入隊(duì)的命令存在語法錯誤,包括參數(shù)數(shù)量,參數(shù)名等等(編譯器錯誤)。
在這種情況下事務(wù)會被拒絕執(zhí)行,也就是隊(duì)列中所有的命令都不會得到執(zhí)行。

在執(zhí)行exec 之后發(fā)生錯誤

比如,類型錯誤,比如對String 使用了Hash 的命令,這是一種運(yùn)行時錯誤。

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)行時異常的情況下,只有錯誤的命令沒有被執(zhí)行,但是其他命令沒有受到影響。
這個顯然不符合我們對原子性的定義,也就是我們沒辦法用Redis 的這種事務(wù)機(jī)制來實(shí)現(xiàn)原子性,保證數(shù)據(jù)的一致。

Lua 腳本

Lua/?lu?/是一種輕量級腳本語言,它是用C 語言編寫的,跟數(shù)據(jù)的存儲過程有點(diǎn)類似。使用Lua 腳本來執(zhí)行Redis 命令的好處:
1、一次發(fā)送多個命令,減少網(wǎng)絡(luò)開銷。
2、Redis 會將整個腳本作為一個整體執(zhí)行,不會被其他請求打斷,保持原子性。
3、對于復(fù)雜的組合命令,我們可以放在文件中,可以實(shí)現(xiàn)程序之間的命令集復(fù)用。

在Redis 中調(diào)用Lua 腳本

使用eval /?'v?l/ 方法,語法格式:

redis> eval lua-script key-num [key1 key2 key3 ....] [value1 value2 value3 ....]
  • eval 代表執(zhí)行Lua 語言的命令。
  • lua-script 代表Lua 語言腳本內(nèi)容。
  • key-num 表示參數(shù)中有多少個key,需要注意的是Redis 中key 是從1 開始的,如果沒有key 的參數(shù),那么寫0。
  • [key1 key2 key3…]是key 作為參數(shù)傳遞給Lua 語言,也可以不填,但是需要和key-num 的個數(shù)對應(yīng)起來。
  • [value1 value2 value3 ….]這些參數(shù)傳遞給Lua 語言,它們是可填可不填的。

示例,返回一個字符串,0 個參數(shù):

redis> eval "return 'Hello World'" 0

在Lua 腳本中調(diào)用Redis 命令

使用redis.call(command, key [param1, param2…])進(jìn)行操作。語法格式:

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)用時用key 表示形參,argv 表示參數(shù)值(實(shí)參)。

設(shè)置鍵值對

在Redis 中調(diào)用Lua 腳本執(zhí)行Redis 命令

redis> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 wei 1234
redis> get wei

以上命令等價于set wei 1234。
在redis-cli 中直接寫Lua 腳本不夠方便,也不能實(shí)現(xiàn)編輯和復(fù)用,通常我們會把腳本放在文件里面,然后執(zhí)行這個文件。

在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"

案例:對IP 進(jìn)行限流

需求:在X 秒內(nèi)只能訪問Y 次。
設(shè)計思路:用key 記錄IP,用value 記錄訪問次數(shù)。
拿到IP 以后,對IP+1。如果是第一次訪問,對key 設(shè)置過期時間(參數(shù)1)。否則判斷次數(shù),超過限定的次數(shù)(參數(shù)2),返回0。如果沒有超過次數(shù)則返回1。超過時間,key 過期之后,可以再次訪問。
KEY[1]是IP, ARGV[1]是過期時間X,ARGV[2]是限制訪問的次數(shù)Y。

-- ip_limit.lua
-- IP 限流,對某個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)用測試(連續(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ù)值,中間要加上一個空格和
    一個逗號,再加上一個空格。
    即:./redis-cli –eval [lua 腳本] [key…]空格,空格[args…]
  • 多個參數(shù)之間用一個空格分割。

緩存Lua 腳本

在腳本比較長的情況下,如果每次調(diào)用腳本都需要把整個腳本傳給Redis 服務(wù)端,會產(chǎn)生比較大的網(wǎng)絡(luò)開銷。為了解決這個問題,Redis 提供了EVALSHA 命令,允許開發(fā)者通過腳本內(nèi)容的SHA1 摘要來執(zhí)行腳本。

Redis 在執(zhí)行script load 命令時會計算腳本的SHA1 摘要并記錄在腳本緩存中,執(zhí)行EVALSHA 命令時Redis 會根據(jù)提供的摘要從腳本緩存中查找對應(yīng)的腳本內(nèi)容,如果找到了則執(zhí)行腳本,否則會返回錯誤:"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"

腳本超時

Redis 的指令執(zhí)行本身是單線程的,這個線程還要執(zhí)行客戶端的Lua 腳本,如果Lua腳本執(zhí)行超時或者陷入了死循環(huán),是不是沒有辦法為客戶端提供服務(wù)了呢?

eval 'while(true) do end' 0

為了防止某個腳本執(zhí)行時間過長導(dǎo)致Redis 無法提供服務(wù), Redis 提供了lua-time-limit 參數(shù)限制腳本的最長運(yùn)行時間,默認(rèn)為5 秒鐘。
lua-time-limit 5000(redis.conf 配置文件中)

當(dāng)腳本運(yùn)行時間超過這一限制后,Redis 將開始接受其他命令但不會執(zhí)行(以確保腳本的原子性,因?yàn)榇藭r腳本并沒有被終止),而是會返回“BUSY”錯誤。
Redis 提供了一個script kill 的命令來中止腳本的執(zhí)行。新開一個客戶端:

script kill

如果當(dāng)前執(zhí)行的Lua 腳本對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 命令來強(qiáng)行終止redis。
shutdown nosave 和shutdown 的區(qū)別在于shutdown nosave 不會進(jìn)行持久化操作,意味著發(fā)生在上一次快照后的數(shù)據(jù)庫修改都會丟失。

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 萬多次set 請求
LPUSH: 51706.31 requests per second —— 每秒鐘處理5 萬多次lpush 請求

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)用


image.png

根據(jù)官方的數(shù)據(jù),Redis 的QPS 可以達(dá)到10 萬左右(每秒請求數(shù))。

Redis 為什么這么快?

總結(jié):1)純內(nèi)存結(jié)構(gòu)、2)單線程、3)多路復(fù)用

  • 內(nèi)存
    KV 結(jié)構(gòu)的內(nèi)存數(shù)據(jù)庫,時間復(fù)雜度O(1)。
    第二個,要實(shí)現(xiàn)這么高的并發(fā)性能,是不是要創(chuàng)建非常多的線程?
    恰恰相反,Redis 是單線程的。
  • 單線程有什么好處呢?
    1、沒有創(chuàng)建線程、銷毀線程帶來的消耗
    2、避免了上線文切換導(dǎo)致的CPU 消耗
    3、避免了線程之間帶來的競爭問題,例如加鎖釋放鎖死鎖等等
  • 異步非阻塞
    異步非阻塞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 不會成為瓶頸,那就順理成章地采用單線程的方案了。

單線程為什么這么快?

因?yàn)镽edis 是基于內(nèi)存的操作,我們先從內(nèi)存開始說起。

虛擬存儲器(虛擬內(nèi)存Vitual Memory)

名詞解釋:主存:內(nèi)存;輔存:磁盤(硬盤)
計算機(jī)主存(內(nèi)存)可看作一個由M 個連續(xù)的字節(jié)大小的單元組成的數(shù)組,每個字節(jié)有一個唯一的地址,這個地址叫做物理地址(PA)。早期的計算機(jī)中,如果CPU 需要內(nèi)存,使用物理尋址,直接訪問主存儲器。
這種方式有幾個弊端:
1、在多用戶多任務(wù)操作系統(tǒng)中,所有的進(jìn)程共享主存,如果每個進(jìn)程都獨(dú)占一塊物理地址空間,主存很快就會被用完。我們希望在不同的時刻,不同的進(jìn)程可以共用同一塊物理地址空間。
2、如果所有進(jìn)程都是直接訪問物理內(nèi)存,那么一個進(jìn)程就可以修改其他進(jìn)程的內(nèi)存數(shù)據(jù),導(dǎo)致物理地址空間被破壞,程序運(yùn)行就會出現(xiàn)異常。
為了解決這些問題,我們就想了一個辦法,在CPU 和主存之間增加一個中間層。CPU不再使用物理地址訪問,而是訪問一個虛擬地址,由這個中間層把地址轉(zhuǎn)換成物理地址,最終獲得數(shù)據(jù)。這個中間層就叫做虛擬存儲器(Virtual Memory)。
具體的操作如下所示:


image.png

在每一個進(jìn)程開始創(chuàng)建的時候,都會分配一段虛擬地址,然后通過虛擬地址和物理地址的映射來獲取真實(shí)數(shù)據(jù),這樣進(jìn)程就不會直接接觸到物理地址,甚至不知道自己調(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)橛貌坏竭@么大的空間,而且會造成很大的系統(tǒng)開銷。Linux 一般用低48 位來表示虛擬地址空間,也就是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ù)的,使得程序編寫、鏈接更加簡單。并且可以對物理內(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)。


image.png

內(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)核空間還是用戶空間,它們都處于虛擬空間中,都是對物理地址的映射。
在Linux 系統(tǒng)中, 內(nèi)核進(jìn)程和用戶進(jìn)程所占的虛擬內(nèi)存比例是1:3。


image.png

當(dāng)進(jìn)程運(yùn)行在內(nèi)核空間時就處于內(nèi)核態(tài),而進(jìn)程運(yùn)行在用戶空間時則處于用戶態(tài)。

進(jìn)程在內(nèi)核空間以執(zhí)行任意命令,調(diào)用系統(tǒng)的一切資源;在用戶空間只能執(zhí)行簡單的運(yùn)算,不能直接調(diào)用系統(tǒng)資源,必須通過系統(tǒng)接口(又稱system call),才能向內(nèi)核發(fā)出指令。
top 命令:


image.png

us 代表CPU 消耗在User space 的時間百分比;
sy 代表CPU 消耗在Kernel space 的時間百分比。

進(jìn)程切換(上下文切換)

任務(wù)操作系統(tǒng)是怎么實(shí)現(xiàn)運(yùn)行遠(yuǎn)大于CPU 數(shù)量的任務(wù)個數(shù)的?當(dāng)然,這些任務(wù)實(shí)際上并不是真的在同時運(yùn)行,而是因?yàn)橄到y(tǒng)通過時間片分片算法,在很短的時間內(nèi),將CPU 輪流分配給它們,造成多任務(wù)同時運(yùn)行的錯覺。
為了控制進(jìn)程的執(zhí)行,內(nèi)核必須有能力掛起正在CPU 上運(yùn)行的進(jìn)程,并恢復(fù)以前掛起的某個進(jìn)程的執(zhí)行。這種行為被稱為進(jìn)程切換。

什么叫上下文?
在每個任務(wù)運(yùn)行前,CPU 都需要知道任務(wù)從哪里加載、又從哪里開始運(yùn)行,也就是說,需要系統(tǒng)事先幫它設(shè)置好CPU 寄存器和程序計數(shù)器(Program Counter),這個叫做CPU 的上下文。

而這些保存下來的上下文,會存儲在系統(tǒng)內(nèi)核中,并在任務(wù)重新調(diào)度執(zhí)行時再次加載進(jìn)來。這樣就能保證任務(wù)原來的狀態(tài)不受影響,讓任務(wù)看起來還是連續(xù)運(yùn)行。
在切換上下文的時候,需要完成一系列的工作,這是一個很消耗資源的操作。

進(jìn)程的阻塞

正在運(yùn)行的進(jìn)程由于提出系統(tǒng)服務(wù)請求(如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)作文件來處理,而Linux 用文件描述符來標(biāo)識每個文件對象。
文件描述符(File Descriptor)是內(nèi)核為了高效管理已被打開的文件所創(chuàng)建的索引,用于指向被打開的文件,所有執(zhí)行I/O 操作的系統(tǒng)調(diào)用都通過文件描述符;文件描述符是一個簡單的非負(fù)整數(shù),用以表明每個被進(jìn)程打開的文件。
Linux 系統(tǒng)里面有三個標(biāo)準(zhǔn)文件描述符。
0:標(biāo)準(zhǔn)輸入(鍵盤);1:標(biāo)準(zhǔn)輸出(顯示器);2:標(biāo)準(zhǔn)錯誤輸出(顯示器)。

傳統(tǒng)I/O 數(shù)據(jù)拷貝

以讀操作為例:

當(dāng)應(yīng)用程序執(zhí)行read 系統(tǒng)調(diào)用讀取文件描述符(FD)的時候,如果這塊數(shù)據(jù)已經(jīng)存在于用戶進(jìn)程的頁內(nèi)存中,就直接從內(nèi)存中讀取數(shù)據(jù)。如果數(shù)據(jù)不存在,則先將數(shù)據(jù)從磁盤加載數(shù)據(jù)到內(nèi)核緩沖區(qū)中,再從內(nèi)核緩沖區(qū)拷貝到用戶進(jìn)程的頁內(nèi)存中。(兩次拷貝,兩次user 和kernel 的上下文切換)。


image.png

I/O 的阻塞到底阻塞在哪里?

Blocking I/O

當(dāng)使用read 或write 對某個文件描述符進(jìn)行過讀寫時,如果當(dāng)前FD 不可讀,系統(tǒng)就不會對其他的操作做出響應(yīng)。從設(shè)備復(fù)制數(shù)據(jù)到內(nèi)核緩沖區(qū)是阻塞的,從內(nèi)核緩沖區(qū)拷貝到用戶空間,也是阻塞的,直到copy complete,內(nèi)核返回結(jié)果,用戶進(jìn)程才解除block 的狀態(tài)。


image.png

為了解決阻塞的問題,我們有幾個思路。
1、在服務(wù)端創(chuàng)建多個線程或者使用線程池,但是在高并發(fā)的情況下需要的線程會很多,系統(tǒng)無法承受,而且創(chuàng)建和釋放線程都需要消耗資源。
2、由請求方定期輪詢,在數(shù)據(jù)準(zhǔn)備完畢后再從內(nèi)核緩存緩沖區(qū)復(fù)制數(shù)據(jù)到用戶空間(非阻塞式I/O),這種方式會存在一定的延遲。
能不能用一個線程處理多個客戶端請求?

I/O 多路復(fù)用(I/O Multiplexing)

I/O 指的是網(wǎng)絡(luò)I/O。
多路指的是多個TCP 連接(Socket 或Channel)。
復(fù)用指的是復(fù)用一個或多個線程。
它的基本原理就是不再由應(yīng)用程序自己監(jiān)視連接,而是由內(nèi)核替應(yīng)用程序監(jiān)視文件描述符。

客戶端在操作的時候,會產(chǎn)生具有不同事件類型的socket。在服務(wù)端,I/O 多路復(fù)用程序(I/O Multiplexing Module)會把消息放入隊(duì)列中,然后通過文件事件分派器(Fileevent Dispatcher),轉(zhuǎn)發(fā)到不同的事件處理器中。


image.png

多路復(fù)用有很多的實(shí)現(xiàn),以select 為例,當(dāng)用戶進(jìn)程調(diào)用了多路復(fù)用器,進(jìn)程會被阻塞。內(nèi)核會監(jiān)視多路復(fù)用器負(fù)責(zé)的所有socket,當(dāng)任何一個socket 的數(shù)據(jù)準(zhǔn)備好了,多路復(fù)用器就會返回。這時候用戶進(jìn)程再調(diào)用read 操作,把數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到用戶空間。

I/O 多路復(fù)用的特點(diǎn)是通過一種機(jī)制一個進(jìn)程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進(jìn)入讀就緒(readable)狀態(tài),select()函數(shù)就可以返回。

Redis 的多路復(fù)用, 提供了select, epoll, evport, kqueue 幾種選擇,在編譯的時候來選擇一種。源碼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ù)都是存儲在內(nèi)存中的,在某些情況下需要對占用的內(nèi)存空間進(jìn)行回收。內(nèi)存回收主要分為兩類,一類是key 過期,一類是內(nèi)存使用達(dá)到上限(max_memory)觸發(fā)內(nèi)存淘汰。

過期策略

  • 定時過期(主動淘汰)
    每個設(shè)置過期時間的key 都需要創(chuàng)建一個定時器,到過期時間就會立即清除。該策略可以立即清除過期的數(shù)據(jù),對內(nèi)存很友好;但是會占用大量的CPU 資源去處理過期的數(shù)據(jù),從而影響緩存的響應(yīng)時間和吞吐量。
  • 惰性過期(被動淘汰)
    只有當(dāng)訪問一個key 時,才會判斷該key 是否已過期,過期則清除。該策略可以最大化地節(jié)省CPU 資源,卻對內(nèi)存非常不友好。極端情況可能出現(xiàn)大量的過期key 沒有再次被訪問,從而不會被清除,占用大量內(nèi)存。
    例如String,在getCommand 里面會調(diào)用expireIfNeeded
    源碼:server.c expireIfNeeded(redisDb *db, robj *key)

第二種情況,每次寫入key 時,發(fā)現(xiàn)內(nèi)存不夠,調(diào)用activeExpireCycle 釋放一部分內(nèi)存。
源碼:expire.c activeExpireCycle(int type)

  • 定期過期
    源碼:server.h
typedef struct redisDb {
dict *dict; /* 所有的鍵值對*/
dict *expires; /* 設(shè)置了過期時間的鍵值對*/
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ù)量的數(shù)據(jù)庫的expires 字典中一定數(shù)量的key,并清除其中已過期的key。該策略是前兩者的一個折中方案。通過調(diào)整定時掃描的時間間隔和每次掃描的限定耗時,可以在不同情況下使得CPU 和內(nèi)存資源達(dá)到最優(yōu)的平衡效果。

Redis 中同時使用了惰性過期和定期過期兩種過期策略。

淘汰策略

Redis 的內(nèi)存淘汰策略,是指當(dāng)內(nèi)存使用達(dá)到最大內(nè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)存。
動態(tài)修改:

redis> config set maxmemory 2GB

到達(dá)最大內(nèi)存以后怎么辦?

# 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.

先從算法來看:
LRU,Least Recently Used:最近最少使用。判斷最近被使用的時間,目前最遠(yuǎn)的數(shù)據(jù)優(yōu)先被淘汰。
LFU,Least Frequently Used,最不常用,4.0 版本新增。
random,隨機(jī)刪除。

volatile-lru 根據(jù)LRU 算法刪除設(shè)置了超時屬性(expire)的鍵,直到騰出足夠內(nèi)存為止。如果沒有
可刪除的鍵對象,回退到noeviction 策略。

allkeys-lru 根據(jù)LRU 算法刪除鍵,不管數(shù)據(jù)有沒有設(shè)置超時屬性,直到騰出足夠內(nèi)存為止。

volatile-lfu 在帶有過期時間的鍵中選擇最不常用的。

allkeys-lfu 在所有的鍵中選擇最不常用的,不管數(shù)據(jù)有沒有設(shè)置超時屬性。

volatile-random 在帶有過期時間的鍵中隨機(jī)選擇。

allkeys-random 隨機(jī)刪除所有鍵,直到騰出足夠內(nèi)存為止。

volatile-ttl 根據(jù)鍵值對象的ttl 屬性,刪除最近將要過期數(shù)據(jù)。如果沒有,回退到noeviction 策略。

noeviction 默認(rèn)策略,不會刪除任何數(shù)據(jù),拒絕所有寫入操作并返回客戶端錯誤信息(error)OOM command not allowed when used memory,此時Redis 只響應(yīng)讀操作。

如果沒有符合前提條件的key 被淘汰,那么volatile-lru、volatile-random 、volatile-ttl 相當(dāng)于noeviction(不做內(nèi)存回收)。
動態(tài)修改淘汰策略:

redis> config set maxmemory-policy volatile-lru

建議使用volatile-lru,在保證正常服務(wù)的情況下,優(yōu)先刪除最近最少使用的key。

  • LRU 淘汰原理
    需要額外的數(shù)據(jù)結(jié)構(gòu)存儲,消耗內(nèi)存。
    Redis LRU 對傳統(tǒng)的LRU 算法進(jìn)行了改良,通過隨機(jī)采樣來調(diào)整算法的精度。
    如果淘汰策略是LRU,則根據(jù)配置的采樣值maxmemory_samples(默認(rèn)是5 個),隨機(jī)從數(shù)據(jù)庫中選擇m 個key, 淘汰其中熱度最低的key 對應(yīng)的緩存數(shù)據(jù)。所以采樣參數(shù)m 配置的數(shù)值越大, 就越能精確的查找到待淘汰的緩存數(shù)據(jù),但是也消耗更多的CPU 計算,執(zhí)行效率降低。

如何找出熱度最低的數(shù)據(jù)?

Redis 中所有對象結(jié)構(gòu)都有一個lru 字段, 且使用了unsigned 的低24 位,這個字段用來記錄對象的熱度。對象被創(chuàng)建時會記錄lru 值。在被訪問的時候也會更新lru 的值。
但是不是獲取系統(tǒng)當(dāng)前的時間戳,而是設(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 的值怎么來的?
Redis 中有個定時處理的函數(shù)serverCron , 默認(rèn)每100 毫秒調(diào)用函數(shù)
updateCachedTime 更新一次全局變量的server.lruclock 的值,它記錄的是當(dāng)前unix時間戳。
源碼: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ù)lookupKey 中更新數(shù)據(jù)的lru 熱度值時,就不用每次調(diào)用系統(tǒng)函數(shù)time,可以提高執(zhí)行效率。
OK,當(dāng)對象里面已經(jīng)有了LRU 字段的值,就可以評估對象的熱度了。
函數(shù)estimateObjectIdleTime 評估指定對象的lru 熱度,思想就是對象的lru 值和全局的server.lruclock 的差值越大(越久沒有得到更新), 該對象熱度越低。
源碼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 位,按秒為單位來表示才能存儲194 天。當(dāng)超過24bit 能表示的最大時間的時候,它會從頭開始計算。
server.h

#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */

在這種情況下,可能會出現(xiàn)對象的lru 大于server.lruclock 的情況,如果這種情況出現(xiàn)那么就兩個相加而不是相減來求最久的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

image.png

除了消耗資源之外,傳統(tǒng)LRU 還有什么問題?

如圖,假設(shè)A 在10 秒內(nèi)被訪問了5 次,而B 在10 秒內(nèi)被訪問了3 次。因?yàn)锽 最后一次被訪問的時間比A 要晚,在同等的情況下,A 反而先被回收。


image.png

要實(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 時,其被分為兩部分:
高16 位用來記錄訪問時間(單位為分鐘,ldt,last decrement time)
低8 位用來記錄訪問頻率,簡稱counter(logc,logistic counter)
counter 是用基于概率的對數(shù)計數(shù)器實(shí)現(xiàn)的,8 位可以表示百萬次的訪問頻率。
對象被讀寫的時候,lfu 的值會被更新。
db.c——lookupKey

void updateLFU(robj *val) {
unsigned long counter = LFUDecrAndReturn(val);
counter = LFULogIncr(counter);
val->lru = (LFUGetTimeInMinutes()<<8) | counter;
}

增長的速率由,lfu-log-factor 越大,counter 增長的越慢
redis.conf 配置文件

# lfu-log-factor 10

如果計數(shù)器只會遞增不會遞減,也不能體現(xiàn)對象的熱度。沒有被訪問的時候,計數(shù)器怎么遞減呢?
減少的值由衰減因子lfu-decay-time(分鐘)來控制,如果值是1 的話,N 分鐘沒有訪問就要減少N。
redis.conf 配置文件

# lfu-decay-time 1

持久化機(jī)制

https://redis.io/topics/persistence
Redis 速度快,很大一部分原因是因?yàn)樗械臄?shù)據(jù)都存儲在內(nèi)存中。如果斷電或者宕機(jī),都會導(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)滿足一定條件的時候,會把當(dāng)前內(nèi)存中的數(shù)據(jù)寫入磁盤,生成一個快照文件dump.rdb。Redis 重啟會通過加載dump.rdb 文件恢復(fù)數(shù)據(jù)。
什么時候?qū)懭雛db 文件?

  • 自動觸發(fā)
    a)配置規(guī)則觸發(fā)。
    redis.conf, SNAPSHOTTING,其中定義了觸發(fā)把數(shù)據(jù)保存到磁盤的觸發(fā)頻率。
    如果不需要RDB 方案,注釋save 或者配置成空字符串""。
save 900 1 # 900 秒內(nèi)至少有一個key 被修改(包括添加)
save 300 10 # 400 秒內(nèi)至少有10 個key 被修改
save 60 10000 # 60 秒內(nèi)至少有10000 個key 被修改

注意上面的配置是不沖突的,只要滿足任意一個都會觸發(fā)。
RDB 文件位置和目錄:

# 文件路徑,
dir ./
# 文件名稱
dbfilename dump.rdb
# 是否是LZF 壓縮rdb 文件
rdbcompression yes
# 開啟數(shù)據(jù)校驗(yàn)
rdbchecksum yes

dir: rdb 文件默認(rèn)在啟動目錄下(相對路徑)config get dir 獲取
dbfilename: 文件名稱
rdbcompression: 開啟壓縮可以節(jié)省存儲空間,但是會消耗一些CPU 的計算時間,默認(rèn)開啟
rdbchecksum: 使用CRC64 算法來進(jìn)行數(shù)據(jù)校驗(yàn),但是這樣做會增加大約10%的性能消耗,如果希望獲取到最大的性能提升,可以關(guān)閉此功能。

為什么停止Redis 服務(wù)的時候沒有save,重啟數(shù)據(jù)還在?
RDB 還有兩種觸發(fā)方式:
b)shutdown 觸發(fā),保證服務(wù)器正常關(guān)閉。
c)flushall,RDB 文件是空的,沒什么意義(刪掉dump.rdb 演示一下)。

  • 手動觸發(fā)
    如果我們需要重啟服務(wù)或者遷移數(shù)據(jù),這個時候就需要手動觸RDB 快照保存。Redis提供了兩條命令:
    a)save
    save 在生成快照的時候會阻塞當(dāng)前Redis 服務(wù)器, Redis 不能處理其他命令。如果內(nèi)存中的數(shù)據(jù)比較多,會造成Redis 長時間的阻塞。生產(chǎn)環(huán)境不建議使用這個命令。
    為了解決這個問題,Redis 提供了第二種方式。
    b)bgsave
    執(zhí)行bgsave 時,Redis 會在后臺異步進(jìn)行快照操作,快照同時還可以響應(yīng)客戶端請求。
    具體操作是Redis 進(jìn)程執(zhí)行fork 操作創(chuàng)建子進(jìn)程(copy-on-write),RDB 持久化過程由子進(jìn)程負(fù)責(zé),完成后自動結(jié)束。它不會記錄fork 之后后續(xù)的命令。阻塞只發(fā)生在fork 階段,一般時間很短。
    用lastsave 命令可以查看最近一次成功生成快照的時間。
  • 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

啟動服務(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

啟動服務(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

啟動服務(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)勢和劣勢
    一、優(yōu)勢
    1.RDB 是一個非常緊湊(compact)的文件,它保存了redis 在某個時間點(diǎn)上的數(shù)據(jù)集。這種文件非常適合用于進(jìn)行備份和災(zāi)難恢復(fù)。
    2.生成RDB 文件的時候,redis 主進(jìn)程會fork()一個子進(jìn)程來處理所有保存工作,主進(jìn)程不需要進(jìn)行任何磁盤IO 操作。
    3.RDB 在恢復(fù)大數(shù)據(jù)集時的速度比AOF 的恢復(fù)速度要快。
    二、劣勢
    1、RDB 方式數(shù)據(jù)沒辦法做到實(shí)時持久化/秒級持久化。因?yàn)閎gsave 每次運(yùn)行都要執(zhí)行fork 操作創(chuàng)建子進(jìn)程,頻繁執(zhí)行成本過高。
    2、在一定間隔時間做一次備份,所以如果redis 意外down 掉的話,就會丟失最后一次快照之后的所有修改(數(shù)據(jù)有丟失)。
    如果數(shù)據(jù)相對來說比較重要,希望將損失降到最小,則可以使用AOF 方式進(jìn)行持久化。

AOF

Append Only File
AOF:Redis 默認(rèn)不開啟。AOF 采用日志的形式來記錄每個寫操作,并追加到文件中。開啟后,執(zhí)行更改Redis 數(shù)據(jù)的命令時,就會把命令寫入到AOF 文件中。
Redis 重啟時會根據(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)的硬盤緩存。什么時候把緩沖區(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,可能會導(dǎo)致丟失這1s 數(shù)據(jù)。通常選擇everysec ,兼顧安全性和效率。

文件越來越大,怎么辦?
由于AOF 持久化是Redis 不斷將寫命令記錄到AOF 文件中,隨著Redis 不斷的進(jìn)行,AOF 的文件會越來越大,文件越大,占用服務(wù)器內(nèi)存越大以及AOF 恢復(fù)要求時間越長。
例如set wei 666,執(zhí)行1000 次,結(jié)果都是wei=666。
為了解決這個問題,Redis 新增了重寫機(jī)制,當(dāng)AOF 文件的大小超過所設(shè)定的閾值時,Redis 就會啟動AOF 文件的內(nèi)容壓縮,只保留可以恢復(fù)數(shù)據(jù)的最小指令集。
可以使用命令bgrewriteaof 來重寫。
AOF 文件重寫并不是對原文件進(jìn)行重新整理,而是直接讀取服務(wù)器現(xiàn)有的鍵值對,然后用一條命令去代替之前記錄這個鍵值對的多條命令,生成一個新的文件后去替換原來的AOF 文件。

# 重寫觸發(fā)機(jī)制
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

aof-rewrite-percentage
默認(rèn)值為100。aof 自動重寫配置,當(dāng)目前aof 文件大小超過上一次重寫的aof 文件大小的百分之多少進(jìn)行重寫,即當(dāng)aof 文件增長到一定大小的時候,Redis 能夠調(diào)用bgrewriteaof對日志文件進(jìn)行重寫。當(dāng)前AOF 文件大小是上次日志重寫得到AOF 文件大小的二倍(設(shè)置為100)時,自動啟動新的日志重寫過程。

auto-aof-rewrite-min-size
默認(rèn)64M。設(shè)置允許重寫的最小aof 文件大小,避免了達(dá)到約定百分比但尺寸仍然很小的情況還要重寫。

重寫過程中,AOF 文件被更改了怎么辦?


image.png

另外有兩個與AOF 相關(guān)的參數(shù):
no-appendfsync-on-rewrite
在aof 重寫或者寫入rdb 文件的時候,會執(zhí)行大量IO,此時對于everysec 和always 的aof模式來說,執(zhí)行fsync 會造成阻塞過長時間,no-appendfsync-on-rewrite 字段設(shè)置為默認(rèn)設(shè)置為no。如果對延遲要求很高的應(yīng)用,這個字段可以設(shè)置為yes,否則還是設(shè)置為no,這樣對持久化特性來說這是更安全的選擇。設(shè)置為yes 表示rewrite 期間對新寫操作不fsync,暫時存在內(nèi)存中,等rewrite 完成后再寫入,默認(rèn)為no,建議修改為yes。Linux 的默認(rèn)fsync策略是30 秒。可能丟失30 秒數(shù)據(jù)。

aof-load-truncated aof
文件可能在尾部是不完整的,當(dāng)redis 啟動的時候,aof 文件的數(shù)據(jù)被載入內(nèi)存。重啟可能發(fā)生在redis 所在的主機(jī)操作系統(tǒng)宕機(jī)后,尤其在ext4 文件系統(tǒng)沒有加上data=ordered選項(xiàng),出現(xiàn)這種現(xiàn)象。redis 宕機(jī)或者異常終止不會造成尾部不完整現(xiàn)象,可以選擇讓redis退出,或者導(dǎo)入盡可能多的數(shù)據(jù)。如果選擇的是yes,當(dāng)截斷的aof 文件被導(dǎo)入的時候,會自動發(fā)布一個log 給客戶端然后load。如果是no,用戶必須手動redis-check-aof 修復(fù)AOF文件才可以。默認(rèn)值為yes。

  • AOF 數(shù)據(jù)恢復(fù)
    重啟Redis 之后就會進(jìn)行AOF 文件的恢復(fù)。

  • AOF 優(yōu)勢與劣勢
    優(yōu)點(diǎn):
    1、AOF 持久化的方法提供了多種的同步頻率,即使使用默認(rèn)的同步頻率每秒同步一次,Redis 最多也就丟失1 秒的數(shù)據(jù)而已。
    缺點(diǎn):
    1、對于具有相同數(shù)據(jù)的的Redis,AOF 文件通常會比RDF 文件體積更大(RDB存的是數(shù)據(jù)快照)。
    2、雖然AOF 提供了多種同步的頻率,默認(rèn)情況下,每秒同步一次的頻率也具有較高的性能。在高并發(fā)的情況下,RDB 比AOF 具好更好的性能保證。

兩種方案比較

那么對于AOF 和RDB 兩種持久化方式,我們應(yīng)該如何選擇呢?

如果可以忍受一小段時間內(nèi)數(shù)據(jù)的丟失,毫無疑問使用RDB 是最好的,定時生成RDB 快照(snapshot)非常便于進(jìn)行數(shù)據(jù)庫備份, 并且RDB 恢復(fù)數(shù)據(jù)集的速度也要比AOF 恢復(fù)的速度要快。
否則就使用AOF 重寫。但是一般情況下建議不要單獨(dú)使用某一種持久化機(jī)制,而是應(yīng)該兩種一起用,在這種情況下,當(dāng)redis 重啟的時候會優(yōu)先載入AOF 文件來恢復(fù)原始的數(shù)據(jù),因?yàn)樵谕ǔG闆r下AOF 文件保存的數(shù)據(jù)集要比RDB 文件保存的數(shù)據(jù)集要完整。

——學(xué)自咕泡學(xué)院

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容