分佈式專題-分佈式緩存技術之Redis02-Redis的原理分析

前言

關於Redis,一共分爲4節,本節我們分析一下Redis的內部原理:

  1. 初步瞭解分佈式緩存技術Redis的使用
    主要會以數據結構爲導向去了解Redis
  2. Redis內部的原理揭祕
    Redis的內部原理、Lua腳本的結合使用、Redis的回收策略、Redis持久化原理
  3. 瞭解分佈式Redis
    分佈式Redis、主從、哨兵、分片、企業級集羣方案
  4. Redis的應用實戰
    Redis的應用實戰,如何在實際應用中去使用redis

過期時間設置

在Redis中提供了Expire命令設置一個鍵的過期時間,到期以後Redis會自動刪除它。這個在我們實際使用過程中用得非常多。

EXPIRE命令的使用方法爲

EXPIRE key seconds

其中seconds 參數表示鍵的過期時間,單位爲秒。

EXPIRE 返回值爲1表示設置成功,0表示設置失敗或者鍵不存在

如果向知道一個鍵還有多久時間被刪除,可以使用TTL命令

TTL key

當鍵不存在時,TTL命令會返回-2

而對於沒有給指定鍵設置過期時間的,通過TTL命令會返回-1

如果向取消鍵的過期時間設置(使該鍵恢復成爲永久的),可以使用PERSIST命令,如果該命令執行成功或者成功清除了過期時間,則返回1 。 否則返回0(鍵不存在或者本身就是永久的)

EXPIRE命令的seconds命令必須是整數,所以最小單位是1秒,如果向要更精確的控制鍵的過期時間可以使用PEXPIRE命令,當然實際過程中用秒的單位就夠了。 PEXPIRE命令的單位是毫秒。即PEXPIRE key 1000與EXPIRE key 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)時

根據配置規則進行自動快照

Redis允許用戶自定義快照條件,當符合快照條件時,Redis會自動執行快照操作。快照的條件可以由用戶在配置文件中配置。配置格式如下

save

第一個參數是時間窗口,第二個是鍵的個數,也就是說,在第一個時間參數配置範圍內被更改的鍵的個數大於後面的changes時,即符合快照條件。redis默認配置了三個規則

save 900 1

save 300 10

save 60 10000

每條快照規則佔一行,每條規則之間是“或”的關係。 在900秒(15分)內有一個以上的鍵被更改則進行快照。

用戶執行SAVE或BGSAVE命令

除了讓Redis自動進行快照以外,當我們對服務進行重啓或者服務器遷移我們需要人工去幹預備份。redis提供了兩條命令來完成這個任務

  1. save命令

當執行save命令時,Redis同步做快照操作,在快照執行過程中會阻塞所有來自客戶端的請求。當redis內存中的數據較多時,通過該命令將導致Redis較長時間的不響應。所以不建議在生產環境上使用這個命令,而是推薦使用bgsave命令

  1. bgsave命令

bgsave命令可以在後臺異步地進行快照操作,快照的同時服務器還可以繼續響應來自客戶端的請求。執行BGSAVE後,Redis會立即返回ok表示開始執行快照操作。

通過LASTSAVE命令可以獲取最近一次成功執行快照的時間; (自動快照採用的是異步快照操作)

執行FLUSHALL命令

該命令在前面講過,會清除redis在內存中的所有數據。執行該命令後,只要redis中配置的快照規則不爲空,也就是save 的規則存在。redis就會執行一次快照操作。不管規則是什麼樣的都會執行。如果沒有定義快照規則,就不會執行快照操作

執行復制時

該操作主要是在主從模式下,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-rewrite-percentage 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更適合被淘汰,我們可以自己控制

Tips

實際上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實戰》,大家可以先下載去看看。

附贈《Redis實戰的PDF版電子書
提取碼:9lr8

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章