內(nèi)容依舊來(lái)自<redis深度歷險(xiǎn)>
核心原理
線程IO模型
單線程非阻塞IO
- redis是單線程模型。redis的指令很快,主要就是由于所有的運(yùn)算都在內(nèi)存,省去了磁盤IO的開銷。由于是單線程,時(shí)間復(fù)雜度較高的指令和存儲(chǔ)的key過大,都會(huì)導(dǎo)致redis卡頓。
- 多路復(fù)用:使用的是非阻塞IO,這個(gè)就類似java的NIO。一般我們使用阻塞io進(jìn)行讀取的時(shí)候,read方法需要讀取n個(gè)字節(jié),如果一個(gè)字節(jié)都沒有,線程就會(huì)在那里等待,一定要讀夠n個(gè)字節(jié)才能返回,線程才能做其他事。非阻塞IO就是就是,打開套接字的時(shí)候,讀寫不再阻塞。實(shí)際寫了多少盒讀了多少,會(huì)立馬又返回值告訴程序?qū)嶋H讀寫多少字節(jié)。redis的線程不會(huì)因?yàn)樽x寫而停頓,讀寫完的瞬間,就可以去處理其他業(yè)務(wù)。
- 事件輪詢:非阻塞IO沒有解決的問題就是,線程要讀或者寫的數(shù)據(jù),在讀取了一部分就返回了。這時(shí),線程肯定不能把數(shù)據(jù)直接返回給調(diào)用端。需要一個(gè)什么機(jī)制來(lái)保證相應(yīng)線程數(shù)據(jù)到來(lái)的時(shí)候,線程能夠被通知到。最簡(jiǎn)單的事件輪詢api就是select函數(shù)。輸入是讀寫描述符列表,同時(shí)對(duì)線程調(diào)用還提供了一個(gè)timeout參數(shù)。這個(gè)參數(shù)意味著,線程會(huì)等待timeout值得時(shí)間。如果在等待期間,有任何事件到來(lái),就可以立即返回。拿到事件以后,線程就可以繼續(xù)處理相應(yīng)的事件。這里,需要寫一個(gè)死循環(huán),成為事件循環(huán)
while (true) {
eventList = select(readFds, writeFds, timeout);
for (event in eventList) {
handleEvent(event);
}
}
- 指令隊(duì)列和響應(yīng)隊(duì)列:redis會(huì)將每個(gè)客戶端的套接字都關(guān)聯(lián)一個(gè)指令隊(duì)列,所有的指令都放到隊(duì)列中進(jìn)行順序處理。如果redis有多個(gè)客戶端連接的話,那就是先到的隊(duì)列先處理。響應(yīng)隊(duì)列也是一樣,通過隊(duì)列將結(jié)果返回給客戶端。如果隊(duì)列為空的話,事件輪詢是不是就不應(yīng)該再去輪詢這個(gè)隊(duì)列了呢?redis的做法就是,如果隊(duì)列為空,就把隊(duì)列的文件描述符write_fds進(jìn)行移除,然后移除事件輪詢,等到隊(duì)列有數(shù)據(jù)了,再給這個(gè)隊(duì)列添加寫文件描述符。這樣可以避免redis的select獲取到隊(duì)列以后,發(fā)現(xiàn)沒東西可寫就立即返回。
- 定時(shí)任務(wù):redis除了處理指令以外,還需要處理其他的業(yè)務(wù),比如定時(shí)任務(wù),備份等。redis的定時(shí)任務(wù)存儲(chǔ)在最小堆那里,維護(hù)一個(gè)最小堆所需要的時(shí)間是nlogn,時(shí)間復(fù)雜度不算高,基本線性時(shí)間。在事件循環(huán)的周期里面,redis會(huì)對(duì)最小堆里面的已經(jīng)到時(shí)間點(diǎn)的定時(shí)任務(wù)進(jìn)行處理。處理完畢以后,就會(huì)將下一個(gè)即將要執(zhí)行的定時(shí)任務(wù)的時(shí)間獲取到,這個(gè)時(shí)間就是select函數(shù)這個(gè)線程的睡眠時(shí)間。在這個(gè)時(shí)間區(qū)間之內(nèi),是可以預(yù)期沒有其他任務(wù)需要處理的,可以休眠。但是,如果當(dāng)休眠的時(shí)候有指令到來(lái),select函數(shù)就會(huì)被激活,進(jìn)行下一輪的事件循環(huán)。處理完指令以后,再去堆那里獲取定時(shí)任務(wù),如果有就執(zhí)行,沒有,就刷新timeout。
通信協(xié)議
背景
- redis的作者認(rèn)為,數(shù)據(jù)庫(kù)的瓶頸不在網(wǎng)絡(luò)流量,而在于內(nèi)部的邏輯處理上面。Redis的傳輸協(xié)議是RESP協(xié)議,這個(gè)協(xié)議有很多的字符冗余,會(huì)浪費(fèi)網(wǎng)絡(luò)流量,但是其優(yōu)勢(shì)在于解析性能極好。
最小單元類型,每個(gè)單元結(jié)束時(shí)候以\r\n結(jié)束
- 單行字符串以"+"開頭
- 多行字符串以"$"開頭,后面跟上字符串長(zhǎng)度
- 整數(shù)值以":"開頭,后面跟整數(shù)的字符串形式
- 錯(cuò)誤消息以"-"開頭
- 數(shù)組以"*"開頭,后面跟數(shù)組的長(zhǎng)度
- 單行字符串redis,表示為: +redis\r\n
- 多行字符串hello world,表示為: $11\r\nhello world \r\n
- 整數(shù)100,表示為: :100
- 錯(cuò)誤, -Wrong\r\n
- 數(shù)組[1,2,3],表示為: *3\r\n:1\r\n:2\r\n:3\r\n
- 客戶端發(fā)送的指令和服務(wù)器返回的響應(yīng),也是這五種單元類型的組合
- set指令set a a, 表示為一個(gè)字符串?dāng)?shù)組,*3\r\n
1\r\na\r\n$1\r\na\r\na
上面列舉了這么多個(gè)類型,可以看出redis的傳輸協(xié)議里面有大量冗余的回車換行符。雖然它浪費(fèi)了部分空間,但是勝在簡(jiǎn)潔。這里我需要思考的就是,性能并不總是一切,簡(jiǎn)單性、易理解和易實(shí)現(xiàn)也是要權(quán)衡的問題。
redis持久化
- redis的備份有rbd和aof兩種。這兩種方式都有自己的不足。
- rbd快照全量備份的話,在服務(wù)器宕機(jī)的時(shí)候會(huì)丟失數(shù)據(jù)
- aof增量備份的話,日志文件會(huì)變得無(wú)比巨大,這時(shí)就需要有一個(gè)定時(shí)任務(wù)去對(duì)aof文件進(jìn)行整理。
- 從上面我們知道,redis是單線程程序,線程需要處理指令和定時(shí)任務(wù),進(jìn)行快照備份是需要進(jìn)行文件io的,這個(gè)會(huì)嚴(yán)重拖慢redis服務(wù)器的性能。那么,redis是如何實(shí)現(xiàn)一邊處理線上指令,一邊進(jìn)行快照備份的呢?進(jìn)行快照備份的時(shí)候,是如何解決內(nèi)存數(shù)據(jù)結(jié)構(gòu)改變的問題?
- redis是使用操作系統(tǒng)的多進(jìn)程特性來(lái)進(jìn)行快照持久化的。在要進(jìn)行持久化的時(shí)候,redis會(huì)fork一個(gè)子進(jìn)程,快照持久化就完全交給子進(jìn)程處理。子進(jìn)程和父進(jìn)程共享內(nèi)存里面的代碼段和數(shù)據(jù)段。在子進(jìn)程產(chǎn)生的一瞬間,內(nèi)存的增長(zhǎng)幾乎是沒有明顯變化。
- 使用子進(jìn)程做數(shù)據(jù)持久化,不會(huì)修改現(xiàn)有的內(nèi)存數(shù)據(jù),只是對(duì)數(shù)據(jù)結(jié)構(gòu)進(jìn)行遍歷讀取,然后序列化存儲(chǔ)到磁盤中。如果這時(shí)父進(jìn)程正在修改共享的數(shù)據(jù)的時(shí)候,父進(jìn)程會(huì)對(duì)要修改的頁(yè)面復(fù)制一份,分離出來(lái),子進(jìn)程看到的數(shù)據(jù)還是子進(jìn)程產(chǎn)生時(shí)候的數(shù)據(jù),所以稱為
快照。這樣有頁(yè)面被分離的時(shí)候,內(nèi)存會(huì)有相應(yīng)的增長(zhǎng),但是也不會(huì)超過原來(lái)內(nèi)存的2倍。 - redis的AOF日志存儲(chǔ)的就是服務(wù)器順序指令,只會(huì)記錄修改數(shù)據(jù)的指令。這個(gè)備份是不會(huì)fork一個(gè)子進(jìn)程。redis是先執(zhí)行命令,然后才將日志存盤。為何要這樣呢?這是造成redis不支持事務(wù)回滾的原因,因?yàn)榘l(fā)生異常的時(shí)候,沒有用來(lái)進(jìn)行回滾的日志。這一點(diǎn)和mysql不一樣,mysql是先做日志,再做操作,所以mysql支持回滾。
- redis提供了bgrewriteaof指令用于對(duì)aof日志文件進(jìn)行瘦身,其原理就是開辟一個(gè)子進(jìn)程對(duì)內(nèi)存進(jìn)行遍歷,轉(zhuǎn)換成為一系列的redis操作指令,序列化成為一個(gè)新的aof日志文件中。這個(gè)操作完成以后,再將期間發(fā)生的增量aof文件追加到新的aof文件中,這樣就用新的文件替換舊的文件。
- fsync:aof日志是異步寫到文件中的。這時(shí)候有一個(gè)問題,如果服務(wù)器在寫磁盤的時(shí)候突然宕機(jī),就會(huì)導(dǎo)致內(nèi)容沒有來(lái)得及刷入磁盤,日志進(jìn)行丟失。Linux提供了fsync(寫設(shè)備命令),fwrite只是寫入到緩沖區(qū),加上fsync(fileno(fp))。該函數(shù)返回后,才能保證寫入到了物理介質(zhì)上。只要redis實(shí)時(shí)調(diào)用fsync命令,就能保證日志不丟失。但是,這個(gè)操作就涉及io了,會(huì)很慢。我們有三種設(shè)置,一種是永遠(yuǎn)不調(diào)用fsync(存盤完全交給操作系統(tǒng)),一種是每個(gè)指令都調(diào)用fsync(性能太差),一種設(shè)置是通常間隔1秒就調(diào)用一次fsync。最后一種方式一般用于生產(chǎn)環(huán)境,在性能和安全之間做一個(gè)平衡。所以,aof可能丟失的就是1秒的數(shù)據(jù)
實(shí)際操作代碼如下:
cd /etc
vim redis.conf
修改如下配置
appendonly yes
# The name of the append only file (default: "appendonly.aof")
appendfilename "appendonly.aof"
往下面看,有三種刷盤方式,我們選擇每秒刷一次
# appendfsync always
appendfsync everysec # 一秒調(diào)用一次
# appendfsync no
...
很后面有一行,這個(gè)是redis文件的配置
dir /var/lib/redis
運(yùn)行幾個(gè)命令
set a 1
incr a
set b 2
如此...
接著去到/var/lib/redis文件夾,可以看到appendonly.aof文件已經(jīng)生成,使用less命令進(jìn)行查看,就會(huì)有如下命令
*2
$6
SELECT
$1
0
*3
$3
SET
$1
c
$1
2
*3
$3
SET
$1
v
$1
1
*3
$3
SET
$1
a
$1
1
接下來(lái)嘗試另外一個(gè)命令,bgrewriteaof對(duì)日志進(jìn)行瘦身
dbsize
6
//日志顯示的文件大小
[root@VM_75_157_centos redis]# ll
total 20
-rw-r--r-- 1 root root 347 Jun 20 22:37 appendonly.aof
然后執(zhí)行bgrewriteaof命令:
127.0.0.1:6379> bgrewriteaof
Background append only file rewriting started
redis開啟了子進(jìn)程進(jìn)行瘦身
[root@VM_75_157_centos redis]# ll
total 20
-rw-r--r-- 1 root root 267 Jun 20 22:38 appendonly.aof
文件大小從347降低到了267
- redis的RBD和AOF方式都有優(yōu)缺點(diǎn)。我們究竟采取何種方式呢?在redis4.0之前,我們是很少使用rbd來(lái)重啟服務(wù)器的,這樣會(huì)丟失大量數(shù)據(jù)。通常使用的是aof重放,但是這樣啟動(dòng)時(shí)間就很長(zhǎng)。好在redis4.0帶來(lái)了一個(gè)新的持久化方式,混合持久化。將rbd文件的內(nèi)容和aof的日志文件放在一起。這時(shí)的aof不再是全量的日志,而是
自持久化開始到持久化結(jié)束結(jié)束的時(shí)間發(fā)生的增量aof日志。通常aof這部分的日志很小。然后,在進(jìn)行重啟的時(shí)候,先加載rbd文件的內(nèi)容,然后重放aof日志。這樣,重啟效率就大大提升了。4.0版本的混合持久化默認(rèn)關(guān)閉的,通過aof-use-rdb-preamble配置參數(shù)控制,yes則表示開啟,no表示禁用,默認(rèn)是禁用的,可通過config set修改。
管道
- redis客戶端提供了管道技術(shù),可以批量處理命令,效率有提高。為何會(huì)這樣呢?一般來(lái)說(shuō),我們發(fā)送一條命令給redis服務(wù)器,它就返回了一個(gè)結(jié)果,這樣就是一個(gè)網(wǎng)絡(luò)數(shù)據(jù)包來(lái)回的時(shí)間。write->read的過程。管道是怎么回事呢?管道調(diào)整了指令的執(zhí)行方式,將多個(gè)write命令先緩存起來(lái),然后批量發(fā)送。比如發(fā)送兩個(gè)指令,順序就是write->read->write->read,消耗兩個(gè)數(shù)據(jù)包時(shí)間。使用了管道以后,執(zhí)行順序就會(huì)變成了write->write->read->read,這時(shí)就只是花費(fèi)了一個(gè)網(wǎng)絡(luò)來(lái)回時(shí)間。
public class PiplineTest {
private static int count = 10000;
public static void main(String[] args){
useNormal();
usePipeline();
}
public static void usePipeline(){
ShardedJedis jedis = getShardedJedis();
ShardedJedisPipeline pipeline = jedis.pipelined();
long begin = System.currentTimeMillis();
for(int i = 0;i<count;i++){
pipeline.set("key_"+i,"value_"+i);
}
pipeline.sync();
jedis.close();
System.out.println("usePipeline total time:" + (System.currentTimeMillis() - begin));
}
public static void useNormal(){
ShardedJedis jedis = getShardedJedis();
long begin = System.currentTimeMillis();
for(int i = 0;i<count;i++){
jedis.set("key_"+i,"value_"+i);
}
jedis.close();
System.out.println("useNormal total time:" + (System.currentTimeMillis() - begin));
}
public static ShardedJedis getShardedJedis(){
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(2);
poolConfig.setMaxIdle(1);
poolConfig.setMaxWaitMillis(2000);
poolConfig.setTestOnBorrow(false);
poolConfig.setTestOnReturn(false);
JedisShardInfo info1 = new JedisShardInfo("127.0.0.1",6379);
JedisShardInfo info2 = new JedisShardInfo("127.0.0.1",6379);
ShardedJedisPool pool = new ShardedJedisPool(poolConfig, Arrays.asList(info1,info2));
return pool.getResource();
}
}
消耗時(shí)間
useNormal total time:772
usePipeline total time:112
- 使用管道的確是節(jié)省了時(shí)間。這種情況何時(shí)使用呢?對(duì)于可以允許少量失敗的批量寫入程序可以使用。比如信息群發(fā),漏掉一兩條無(wú)所謂,使用定時(shí)任務(wù)去補(bǔ)就好了。
管道的本質(zhì):網(wǎng)絡(luò)交互的簡(jiǎn)略流程如下
- 客戶端進(jìn)程調(diào)用write將消息寫到操作系統(tǒng)為套接字分配的緩沖區(qū)中
- 客戶端操作系統(tǒng)將緩沖區(qū)的內(nèi)容發(fā)送出去
- 服務(wù)器進(jìn)程將數(shù)據(jù)放在操作系統(tǒng)為套接字分配的緩沖區(qū)中
- 服務(wù)器調(diào)用write將響應(yīng)消息寫到套接字分配的緩沖區(qū)中
- 服務(wù)器將內(nèi)容發(fā)送出去
- 客戶端操作系統(tǒng)將接收到的數(shù)據(jù)放到為套接字分配的緩沖區(qū)中
- 客戶端進(jìn)程調(diào)用read從緩沖區(qū)讀取數(shù)據(jù)返回給上層使用
我們開始以為,客戶端的write操作是要等到對(duì)方收到消息以后才返回的,實(shí)際情況不是這樣。實(shí)際情況是客戶端的write負(fù)責(zé)把數(shù)據(jù)寫到緩沖區(qū)就返回了。剩下的發(fā)送交給操作系統(tǒng)。但是,如果緩沖區(qū)滿了,write操作就要等待緩沖區(qū)空出空間來(lái),這個(gè)才是寫操作IO真正的耗時(shí)。讀取內(nèi)容也是這么回事,讀IO操作的耗時(shí)就是等待緩沖區(qū)有數(shù)據(jù)到來(lái)。 - 對(duì)于單個(gè)命令的set a 1這樣,寫操作幾乎沒有耗時(shí),讀操作就有耗時(shí)了,這時(shí)就要等待網(wǎng)絡(luò)消息的到來(lái)。
- 對(duì)于管道來(lái)說(shuō),連續(xù)的write幾乎不耗時(shí),多個(gè)write也只是寫入到了緩沖區(qū)。第一個(gè)read會(huì)比較耗時(shí),會(huì)等到數(shù)據(jù)回來(lái)。但是,當(dāng)?shù)谝粋€(gè)結(jié)果已經(jīng)返回的時(shí)候,所有的響應(yīng)都回到操作系統(tǒng)內(nèi)核的緩沖區(qū)了,后續(xù)的read就可以直接拿結(jié)果,瞬間返回。
redis事物
普通數(shù)據(jù)庫(kù)的事務(wù)大致如下:
begin();
try{
//業(yè)務(wù)邏輯
....
commit();
} catch(Exception e) {
rollback();
}
redis的事務(wù)有如下的指令來(lái)支持,主要有multi事務(wù)開始,exec事務(wù)執(zhí)行,discard事務(wù)丟棄。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr a
QUEUED
127.0.0.1:6379> incr a
QUEUED
127.0.0.1:6379> exec
1) (integer) 9
2) (integer) 10
如果中途有命令是錯(cuò)誤的呢?
[root@VM_75_157_centos ~]# redis-cli
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incre a
(error) ERR unknown command 'incre'
127.0.0.1:6379> incr a
QUEUED
127.0.0.1:6379> incr a
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
這時(shí)就會(huì)告訴用戶,事務(wù)被丟棄了
a的值并沒有改變。但是,這并沒有確保是所有的指令都沒有執(zhí)行,redis的事務(wù)不支持原子性
redis事務(wù)的執(zhí)行流程就是所有的執(zhí)行在exec指令之前,都不會(huì)執(zhí)行,而是緩存在服務(wù)器的事務(wù)隊(duì)列當(dāng)中。服務(wù)器一旦接收到exec指令,才開始批量執(zhí)行隊(duì)列的指令。之前說(shuō)過redis是單線程,所以可以保證隊(duì)列里面的指令可以得到順序執(zhí)行,不會(huì)被其他指令搶占。保證了一批指令的批量執(zhí)行。
- 探討redis事務(wù)的原子性
(nil)
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set test test
QUEUED
127.0.0.1:6379> incr test
QUEUED
127.0.0.1:6379> set test2 test2
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379> get test
"test"
127.0.0.1:6379> get test2
"test2"
上面的事務(wù),在 第二個(gè)指令執(zhí)行的時(shí)候失敗了。如果有使用mysql的經(jīng)驗(yàn),我們可能認(rèn)為,后續(xù)的get命令,得到的會(huì)是null值。的確,mysql可以對(duì)事務(wù)進(jìn)行回滾。但是,redis后續(xù)的指令都被執(zhí)行了。redis事務(wù)不支持回滾的一個(gè)原因就是redis是先操作指令,然后再寫日志。而mysql是先寫日志,再進(jìn)行操作。所以,發(fā)生錯(cuò)誤的時(shí)候,mysql有可以回滾的日志,而redis沒有。通過上述的操作,我們可以知道redis的事務(wù)不具備原子性,而是僅僅滿足了事務(wù)隔離性種的串行化。
對(duì)事務(wù)的操作,我們是可以進(jìn)行一定的優(yōu)化的,使用的方式就是前面提過的管道。之前的這幾個(gè)命令,一個(gè)命令就消耗了一個(gè)網(wǎng)絡(luò)來(lái)回,我們可以使用管道進(jìn)行優(yōu)化。
watch指令,這個(gè)是redis提供的一種樂觀鎖的實(shí)現(xiàn)。如果有用過關(guān)系型數(shù)據(jù)庫(kù),樂觀鎖的實(shí)現(xiàn)的話,就是在表里面增加一個(gè)version版本號(hào)。在對(duì)某一行進(jìn)行修改的時(shí)候,先select這一行,獲得當(dāng)前的版本號(hào),然后執(zhí)行更新的時(shí)候可以是
update table set a = ? where id = ? and version = 當(dāng)前線程select的版本號(hào)
樂觀鎖可以處理Java程序的多線程并發(fā)修改。redis的watch也是同樣的道理,在事務(wù)開啟之前,先用watch盯住某個(gè)key,然后進(jìn)行事務(wù)操作,如果key在事務(wù)執(zhí)行之前,有被修改過,事務(wù)就執(zhí)行失敗。
127.0.0.1:6379> set books java
OK
127.0.0.1:6379> watch books
OK
127.0.0.1:6379> set books redis
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set books golang
QUEUED
127.0.0.1:6379> exec
(nil)
我們事先watch了books變量,但是,在事務(wù)之前,books被改變了,所以,后面執(zhí)行事務(wù)的時(shí)候,就失敗。redis樂觀鎖的指令順序是watch->multi->exec。
分布式鎖
- 如果redis配置了集群環(huán)境,redis的set指令擴(kuò)展設(shè)置的分布式鎖就會(huì)出現(xiàn)問題,它就變得不是絕對(duì)安全了。例如,redis有主從兩個(gè)節(jié)點(diǎn),線程1在主節(jié)點(diǎn)獲得了一個(gè)鎖。這時(shí)主節(jié)點(diǎn)掛掉了,從節(jié)點(diǎn)升為主節(jié)點(diǎn),這時(shí)新的主節(jié)點(diǎn)并沒有那個(gè)key,線程2請(qǐng)求加鎖的時(shí)候,也會(huì)獲得同一把鎖。
- 這個(gè)問題的解決,需要引入第三方的library,如redlock.使用redlock算法的話,可以保證加鎖成功。它的原理是向大多數(shù)節(jié)點(diǎn)都發(fā)送set(key,value,nx,ex)指令,當(dāng)半數(shù)節(jié)點(diǎn)都返回true的時(shí)候,才認(rèn)為加鎖成功。del也同樣如此。由于要對(duì)多個(gè)節(jié)點(diǎn)進(jìn)行操作,性能會(huì)有一定的下降。
redis key的過期策略
- redis的所有數(shù)據(jù)結(jié)構(gòu)都可以設(shè)置過期時(shí)間,時(shí)間到了,就可以被自動(dòng)刪除。我之前一直很好奇的就是redis的key到底是怎么過期的。使用定時(shí)任務(wù)?可是如果同一時(shí)間太多key要過期,定時(shí)任務(wù)處理不過來(lái)。
- redis會(huì)對(duì)設(shè)置了過期時(shí)間的可以放入一個(gè)獨(dú)立的字典種,定時(shí)任務(wù)會(huì)去變量這個(gè)數(shù)據(jù)結(jié)構(gòu)去刪除過期的key。除了定時(shí)處理以外,redis還提供了惰性刪除的方式。在客戶端訪問訪問key的時(shí)候,會(huì)對(duì)key的過期時(shí)間進(jìn)行檢查,如果發(fā)現(xiàn)過期,就立即刪除。
定時(shí)掃描
- redis默認(rèn)每秒進(jìn)行10次過期掃描,這里不會(huì)檢查所有過期的key,采用的是一種貪心挑選的策略。
- 從字典中隨機(jī)挑選20個(gè)key
- 刪除這20個(gè)key中已經(jīng)過期的key
- 如果過期的key的比例超過25%,就重復(fù)步驟1
掃描策略的時(shí)間配置
cd /etc
vim redis.conf
把文件拉到最后,會(huì)有一行
hz 10
修改這個(gè)值就可以改變定時(shí)過期掃描的頻率,redis支持1~500,但是超過100的話,就不是一個(gè)good idea。
- 這里我們會(huì)想到,如果一個(gè)redis的key在某一個(gè)時(shí)間段集中過期會(huì)怎么樣?會(huì)不會(huì)導(dǎo)致redis卡頓?如果出現(xiàn)這種情況,redis是會(huì)出現(xiàn)卡頓的,但是redis對(duì)過期掃描設(shè)置了時(shí)間上限,默認(rèn)不會(huì)超過25ms。就是說(shuō),當(dāng)客戶端的請(qǐng)求到來(lái),如果這時(shí)redis正在執(zhí)行過期,那么客戶端請(qǐng)求會(huì)等待至少25ms才能返回。這時(shí),就要注意客戶端的超時(shí)時(shí)間設(shè)置得短的話,就有可能會(huì)超時(shí)。
- 避免大量的key集中過期的話,我們可以使用一種隨機(jī)的策略,將時(shí)間分散。
jedis.expire(key, Math.random(86400) + time);
從節(jié)點(diǎn)過期策略
- 從節(jié)點(diǎn)不會(huì)進(jìn)行過期掃描,這個(gè)處理是被動(dòng)的。主節(jié)點(diǎn)在key到期的時(shí)候,會(huì)在aof文件中增加一個(gè)del指令,等到從節(jié)點(diǎn)同步aof以后,從節(jié)點(diǎn)執(zhí)行這個(gè)del指令來(lái)刪除相應(yīng)的key。
- 從節(jié)點(diǎn)同步的延遲,會(huì)導(dǎo)致數(shù)據(jù)在主節(jié)點(diǎn)已經(jīng)被刪除,但是從節(jié)點(diǎn)沒有及時(shí)同步,已經(jīng)過期的key還會(huì)在從節(jié)點(diǎn)查到。之前說(shuō)的分布式鎖在集群環(huán)境下會(huì)不安全,這個(gè)也是一大部分原因。