redis(3)--redis原理分析
過期時間設置
在Redis中提供了Expire命令設置一個鍵的過期時間,到期以後Redis會自動刪除它。這個在我們實際使用過程中用得非常多。
EXPIRE命令的使用方法為
EXPIRE key seconds
其中seconds 參數表示鍵的過期時間,單位為秒。
EXPIRE 返回值為1表示設置成功,0表示設置失敗或者鍵不存在
如果向知道一個鍵還有多久時間被刪除,可以使用TTL命令
TTL key
當鍵不存在時,TTL命令會返回-2,而對於沒有給指定鍵設置過期時間的,通過TTL命令會返回-1
如果向取消鍵的過期時間設置(使該鍵恢復成為永久的),可以使用PERSIST命令,如果該命令執行成功或者成功清除了過期時間,則返回1 。 否則返回0(鍵不存在或者本身就是永久的)
PEXPIRE命令的單位是毫秒。即PEXPIRE key 1000與EXPIREkey 1相等;對應的PTTL以毫秒單位獲取鍵的剩余有效時間
還有一個針對字符串獨有的過期時間設置方式
setex(String key,int seconds,String value)
過期刪除的原理
Redis 中的主鍵失效是如何實現的,即失效的主鍵是如何刪除的?實際上,Redis 刪除失效主鍵的方法主要有兩種:
消極方法(passive way)
在主鍵被訪問時如果發現它已經失效,那麽就刪除它
積極方法(active way)
周期性地從設置了失效時間的主鍵中選擇一部分失效的主鍵刪除
對於那些從未被查詢的key,即便它們已經過期,被動方式也無法清除。因此Redis會周期性地隨機測試一些key,已過期的key將會被刪掉。Redis每秒會進行10次操作,具體的流程:
\1. 隨機測試 20 個帶有timeout信息的key;
\2. 刪除其中已經過期的key;
\3. 如果超過25%的key被刪除,則重復執行步驟1;
這是一個簡單的概率算法(trivial probabilistic algorithm),基於假設我們隨機抽取的key代表了全部的key空間
Redis發布訂閱
Redis提供了發布訂閱功能,可以用於消息的傳輸,Redis提供了一組命令可以讓開發者實現“發布/訂閱”模式(publish/subscribe) . 該模式同樣可以實現進程間的消息傳遞,它的實現原理是:
發布/訂閱模式包含兩種角色,分別是發布者和訂閱者。訂閱者可以訂閱一個或多個頻道,而發布者可以向指定的頻道發送消息,所有訂閱此頻道的訂閱者都會收到該消息
發布者發布消息的命令是PUBLISH, 用法是
PUBLISH channel message
比如向channel.1發一條消息:hello
PUBLISH channel.1 “hello”
這樣就實現了消息的發送,該命令的返回值表示接收到這條消息的訂閱者數量。因為在執行這條命令的時候還沒有訂閱者訂閱該頻道,所以返回為0. 另外值得註意的是消息發送出去不會持久化,如果發送之前沒有訂閱者,那麽後
續再有訂閱者訂閱該頻道,之前的消息就收不到了
訂閱者訂閱消息的命令是
SUBSCRIBE channel [channel …]
該命令同時可以訂閱多個頻道,比如訂閱channel.1的頻道。 SUBSCRIBE channel.1
執行SUBSCRIBE命令後客戶端會進入訂閱狀態
結構圖
channel分兩類,一個是普通channel、另一個是pattern channel(規則匹配), producer1發布了一條消息【publish abc hello】,redis server發給abc這個普通channel上的所有訂閱者,同時abc也匹配上了pattern
channel的名字,所以這條消息也會同時發送給pattern channel *bc上的所有訂閱者
Redis的數據是如何持久化的?
Redis支持兩種方式的持久化,一種是RDB方式、另一種是AOF(append-only-file)方式。前者會根據指定的規則“定時”將內存中的數據存儲在硬盤上,而後者在每次執行命令後將命令本身記錄下來。
兩種持久化方式可以單獨使用其中一種,也可以將這兩種方式結合使用
RDB方式
當符合一定條件時,Redis會單獨創建(fork)一個子進程來進行持久化,會先將數據寫入到一個臨時文件中,等到持久化過程都結束了,再用這個臨時文件替換上次持久化好的文件。整個過程中,主進程是不進行任何IO操作的,這就確保了極高的性能。
如果需要進行大規模數據的恢復,且對於數據恢復的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺點是最後一次持久化後的數據可能丟失
--fork的作用是復制一個與當前進程一樣的進程。新進程的所有數據(變量、環境變量、程序計數器等)數值都和原進程一致,但是是一個全新的進程,並作為原進程的子進程
Redis會在以下幾種情況下對數據進行快照
\1. 根據配置規則進行自動快照
\2. 用戶執行SAVE或者GBSAVE命令
\3. 執行FLUSHALL命令
\4. 執行復制(replication)時
1.根據配置規則進行自動快照
Redis允許用戶自定義快照條件,當符合快照條件時,Redis會自動執行快照操作。快照的條件可以由用戶在配置文件中配置。配置格式如下
save
第一個參數是時間窗口,第二個是鍵的個數,也就是說,在第一個時間參數配置範圍內被更改的鍵的個數大於後面的changes時,即符合快照條件。redis默認配置了三個規則
save 900 1
save 300 10
save 60 10000
每條快照規則占一行,每條規則之間是“或”的關系。 在900秒(15分)內有一個以上的鍵被更改則進行快照
2.用戶執行SAVE或BGSAVE命令
除了讓Redis自動進行快照以外,當我們對服務進行重啟或者服務器遷移我們需要人工去幹預備份。redis提供了兩條命令來完成這個任務
\1. save命令
當執行save命令時,Redis同步做快照操作,在快照執行過程中會阻塞所有來自客戶端的請求。當redis內存中的數據較多時,通過該命令將導致Redis較長時間的不響應。所以不建議在生產環境上使用這個命令,而是推薦使用bgsave命令
\2. bgsave命令
bgsave命令可以在後臺異步地進行快照操作,快照的同時服務器還可以繼續響應來自客戶端的請求。執行BGSAVE後,Redis會立即返回ok表示開始執行快照操作。
通過LASTSAVE命令可以獲取最近一次成功執行快照的時間; (自動快照采用的是異步快照操作)
3.執行FLUSHALL命令
該命令在前面講過,會清除redis在內存中的所有數據。執行該命令後,只要redis中配置的快照規則不為空,也就是save 的規則存在。redis就會執行一次快照操作。不管規則是什麽樣的都會執行。如果沒有定義快照規則,就不會執行快照操作
4.執行復制時
該操作主要是在主從模式下,redis會在復制初始化時進行自動快照。這個會在後面講到;
這裏只需要了解當執行復制操作時,及時沒有定義自動快照規則,並且沒有手動執行過快照操作,它仍然會生成RDB快照文件
AOF方式
當使用Redis存儲非臨時數據時,一般需要打開AOF持久化來降低進程終止導致的數據丟失。AOF可以將Redis執行的每一條寫命令追加到硬盤文件中,這一過程會降低Redis的性能,但大部分情況下這個影響是能夠接受的,另外使用較快的硬盤可以提高AOF的性能
開啟AOF
默認情況下Redis沒有開啟AOF(append only file)方式的持久化,可以通過appendonly參數啟用,在redis.conf 中找到 appendonly yes
開啟AOF持久化後每執行一條會更改Redis中的數據的命令後,Redis就會將該命令寫入硬盤中的AOF文件。AOF文件的保存位置和RDB文件的位置相同,都是通過dir參數設置的,
默認的文件名是apendonly.aof. 可以在redis.conf中的屬性 appendfilename appendonlyh.aof修改
AOF的實現
AOF文件以純文本的形式記錄Redis執行的寫命令例如開啟AOF持久化的情況下執行如下4條命令
set foo 1
set foo 2
set foo 3
get
redis 會將前3條命令寫入AOF文件中,通過vim的方式可以看到aof文件中的內容
我們會發現AOF文件的內容正是Redis發送的原始通信協議的內容,從內容中我們發現Redis只記錄了3條命令。然後這時有一個問題是前面2條命令其實是冗余的,因為這兩條的執行結果都會被第三條命令覆蓋。
隨著執行的命令越來越多,AOF文件的大小也會越來越大,其實內存中實際的數據可能沒有多少,那這樣就會造成磁盤空間以及redis數據還原的過程比較長的問題。因此我們希望Redis可以自動優化
AOF文件,就上面這個例子來說,前面兩條是可以被刪除的。 而實際上Redis也考慮到了,可以配置一個條件,
每當達到一定條件時Redis就會自動重寫AOF文件,這個條件的配置問 auto-aof-rewritepercentage 100 auto-aof-rewrite-min-size 64mb
auto-aof-rewrite-percentage 表示的是當目前的AOF文件大小超過上一次重寫時的AOF文件大小的百分之多少時會再次進行重寫,如果之前沒有重寫過,則以啟動時AOF文件大小為依據
auto-aof-rewrite-min-size 表示限制了允許重寫的最小AOF文件大小,通常在AOF文件很小的情況下即使其中有很多冗余的命令我們也並不太關心。
另外,還可以通過BGREWRITEAOF 命令手動執行AOF,執行完以後冗余的命令已經被刪除了
在啟動時,Redis會逐個執行AOF文件中的命令來將硬盤中的數據載入到內存中,載入的速度相對於RDB會慢一些
AOF的重寫原理
Redis 可以在 AOF 文件體積變得過大時,自動地在後臺對 AOF 進行重寫: 重寫後的新 AOF 文件包含了恢復當前數據集所需的最小命令集合。
重寫的流程是這樣,主進程會fork一個子進程出來進行AOF重寫,這個重寫過程並不是基於原有的aof文件來做的,而是有點類似於快照的方式,全量遍歷內存中的數據,然後逐個序列到aof文件中。
在fork子進程這個過程中,服務端仍然可以對外提供服務,那這個時候重寫的aof文件的數據和redis內存數據不一致了怎麽辦?不用擔心,這個過程中,主進程的數據更新操作,會緩存到aof_rewrite_buf中,也就是單獨開辟一塊緩存來存儲重寫期間收到的命令,
當子進程重寫完以後再把緩存中的數據追加到新的aof文件。當所有的數據全部追加到新的aof文件中後,把新的aof文件重命名為,此後所有的操作都會被寫入新的aof文件。
如果在rewrite過程中出現故障,不會影響原來aof文件的正常工作,只有當rewrite完成後才會切換文件。因此這個rewrite過程是比較可靠的
Redis內存回收策略
Redis中提供了多種內存回收策略,當內存容量不足時,為了保證程序的運行,這時就不得不淘汰內存中的一些對象,釋放這些對象占用的空間,那麽選擇淘汰哪些對象呢?
其中,默認的策略為noeviction策略,當內存使用達到閾值的時候,所有引起申請內存的命令會報錯,如下還有其他的策略:
allkeys-lru:從數據集(server.db[i].dict)中挑選最近最少使用的數據淘汰
適合的場景: 如果我們的應用對緩存的訪問都是相對熱點數據,那麽可以選擇這個策略
allkeys-random:隨機移除某個key。
適合的場景:如果我們的應用對於緩存key的訪問概率相等,則可以使用這個策略
volatile-random:從已設置過期時間的數據集(server.db[i].expires)中任意選擇數據淘汰。
volatile-lru:從已設置過期時間的數據集(server.db[i].expires)中挑選最近最少使用的數據淘汰。
volatile-ttl:從已設置過期時間的數據集(server.db[i].expires)中挑選將要過期的數據淘汰
適合場景:這種策略使得我們可以向Redis提示哪些key更適合被淘汰,我們可以自己控制
實際上Redis實現的LRU並不是可靠的LRU,也就是名義上我們使用LRU算法淘汰內存數據,但是實際上被淘汰的鍵並不一定是真正的最少使用的數據,這裏涉及到一個權衡的問題,如果需要在所有的數據中搜索最符合條件的數據,
那麽一定會增加系統的開銷,Redis是單線程的,所以耗時的操作會謹慎一些。為了在一定成本內實現相對的LRU,早期的Redis版本是基於采樣的LRU,也就是放棄了從所有數據中搜索解改為采樣空間搜索最優解。
Redis3.0版本之後,Redis作者對於基於采樣的LRU進行了一些優化,目的是在一定的成本內讓結果更靠近真實的LRU。
Redis是單進程單線程?性能為什麽這麽快
Redis采用了一種非常簡單的做法,單線程來處理來自所有客戶端的並發請求,Redis把任務封閉在一個線程中從而避免了線程安全問題;redis為什麽是單線程?
官方的解釋是,CPU並不是Redis的瓶頸所在,Redis的瓶頸主要在機器的內存和網絡的帶寬。那麽Redis能不能處理高並發請求呢?當然是可以的,至於怎麽實現的,我們來具體了解一下。 【註意並發不等於並行,並發性I/O
流,意味著能夠讓一個計算單元來處理來自多個客戶端的流請求。並行性,意味著服務器能夠同時執行幾個事情,具有多個計算單元】
多路復用
Redis 是跑在單線程中的,所有的操作都是按照順序線性執行的,但是由於讀寫操作等待用戶輸入或輸出都是阻塞的,
所以 I/O 操作在一般情況下往往不能直接返回,這會導致某一文件的 I/O 阻塞導致整個進程無法對其它客戶提供服務,而 I/O 多路復用就是為了解決這個問題而出現的。
了解多路復用之前,先簡單了解下幾種I/O模型
(1)同步阻塞IO(Blocking IO):即傳統的IO模型。
(2)同步非阻塞IO(Non-blocking IO):默認創建的socket都是阻塞的,非阻塞IO要求socket被設置為NONBLOCK。
(3)IO多路復用(IO Multiplexing):即經典的Reactor設計模式,也稱為異步阻塞IO,Java中的Selector和Linux中的epoll都是這種模型。
(4)異步IO(Asynchronous IO):即經典的Proactor設計模式,也稱為異步非阻塞IO。
同步和異步、阻塞和非阻塞,到底是什麽意思,感覺原理都差不多,我來簡單解釋一下
同步和異步,指的是用戶線程和內核的交互方式
阻塞和非阻塞,指用戶線程調用內核IO操作的方式是阻塞還是非阻塞
就像在Java中使用多線程做異步處理的概念,通過多線程去執行一個流程,主線程可以不用等待。而阻塞和非阻塞我們可以理解為假如在同步流程或者異步流程中做IO操作,如果緩沖區數據還沒準備好,IO的這個過程會阻塞,
這個在之前講TCP協議的時候有講過
在Redis中使用Lua腳本
我們在使用redis的時候,會面臨一些問題,比如
原子性問題
前面我們講過,redis雖然是單一線程的,當時仍然會存在線程安全問題,當然,這個線程安全問題不是來源安於Redis服務器內部。而是Redis作為數據服務器,是提供給多個客戶端使用的。
多個客戶端的操作就相當於同一個進程下的多個線程,如果多個客戶端之間沒有做好數據的同步策略,就會產生數據不一致的問題。舉個簡單的例子
多個客戶端的命令之間沒有做請求同步,導致實際執行順序可能會不一致,最終的結果也就無法滿足原子性了。
效率問題
redis本身的吞吐量是非常高的,因為它首先是基於內存的數據庫。在實際使用過程中,有一個非常重要的因素影響redis的吞吐量,那就是網絡。
我們在使用redis實現某些特定功能的時候,很可能需要多個命令或者多個數據類型的交互才能完成,那麽這種多次網絡請求對性能影響比較大。
當然redis也做了一些優化,比如提供了pipeline管道操作,但是它有一定的局限性,就是執行的多個命令和響應之間是不存在相互依賴關系的。
所以我們需要一種機制能夠編寫一些具有業務邏輯的命令,減少網絡請求
Lua
Redis中內嵌了對Lua環境的支持,允許開發者使用Lua語言編寫腳本傳到Redis中執行,Redis客戶端可以使用Lua腳本,直接在服務端原子的執行多個Redis命令。
使用腳本的好處:
\1. 減少網絡開銷,在Lua腳本中可以把多個命令放在同一個腳本中運行
\2. 原子操作,redis會將整個腳本作為一個整體執行,中間不會被其他命令插入。換句話說,編寫腳本的過程中無需擔心會出現競態條件
\3. 復用性,客戶端發送的腳本會永遠存儲在redis中,這意味著其他客戶端可以復用這一腳本來完成同樣的邏輯
Lua是一個高效的輕量級腳本語言(javascript、shell、sql、python、ruby…),用標準C語言編寫並以源代碼形式開放, 其設計目的是為了嵌入應用程序中,從而為應用程序提供靈活的擴展和定制功能;
Redis與Lua
先初步的認識一下在redis中如何結合lua來完成一些簡單的操作
在Lua腳本中調用Redis命令
在Lua腳本中調用Redis命令,可以使用redis.call函數調用。比如我們調用string類型的命令
redis.call(‘set’,’hello’,’world’)
local value=redis.call(‘get’,’hello’)
redis.call 函數的返回值就是redis命令的執行結果。前面我們介紹過redis的5中類型的數據返回的值的類型也都不一樣。redis.call函數會將這5種類型的返回值轉化對應的Lua的數據類型
從Lua腳本中獲得返回值
在很多情況下我們都需要腳本可以有返回值,畢竟這個腳本也是一個我們所編寫的命令集,我們可以像調用其他redis內置命令一樣調用我們自己寫的腳本,所以同樣redis會自動將腳本返回值的Lua數據類型轉化為Redis的返回值類型。
在腳本中可以使用return 語句將值返回給redis客戶端,通過return語句來執行,如果沒有執行return,默認返回為nil。
EVAL命令的格式是
[EVAL][腳本內容] [key參數的數量][key …] [arg …]
可以通過key和arg這兩個參數向腳本中傳遞數據,他們的值可以在腳本中分別使用KEYS和ARGV 這兩個類型的全局變量訪問。比如我們通過腳本實現一個set命令,通過在redis客戶端中調用,那麽執行的語句是:
lua腳本的內容為: return redis.call(‘set’,KEYS[1],ARGV[1]) //KEYS和ARGV必須大寫
eval "return redis.call(‘set‘,KEYS[1],ARGV[1])" 1 lua1 hello
註意:EVAL命令是根據 key參數的數量-也就是上面例子中的1來將後面所有參數分別存入腳本中KEYS和ARGV兩個表類型的全局變量。當腳本不需要任何參數時也不能省略這個參數。如果沒有參數則為0
EVALSHA命令
考慮到我們通過eval執行lua腳本,腳本比較長的情況下,每次調用腳本都需要把整個腳本傳給redis,比較占用帶寬。為了解決這個問題,redis提供了EVALSHA命令允許開發者通過腳本內容的SHA1摘要來執行腳本。
該命令的用法和EVAL一樣,只不過是將腳本內容替換成腳本內容的SHA1摘要
\1. Redis在執行EVAL命令時會計算腳本的SHA1摘要並記錄在腳本緩存中
\2. 執行EVALSHA命令時Redis會根據提供的摘要從腳本緩存中查找對應的腳本內容,如果找到了就執行腳本,否則返回“NOSCRIPT No matching script,Please use EVAL”
通過以下案例來演示EVALSHA命令的效果
script load "return redis.call(‘get‘,‘lua1‘)" 將腳本加入緩存並生成sha1命令
evalsha "a5a402e90df3eaeca2ff03d56d99982e05cf6574" 0
我們在調用eval命令之前,先執行evalsha命令,如果提示腳本不存在,則再調用eval命令
redis(3)--redis原理分析