【轉】子彈短信內部技術分享:Redis

原理

Redis 是一個內存型「數據庫」,除存儲以外,它還有許多強大的命令,使之遠遠超出了數據庫的定義,因此官方稱之爲「data structure store」,數據結構存儲系統。 經過 Redis 提供的指令,咱們能夠實現緩存、消息隊列、事件通知、排行榜、庫存管理、分佈式鎖等功能。node

基礎結構

Redis 核心是單進程單線程服務,經過 epoll、select 等實現了 IO 多路複用,能夠併發處理網絡事件。redis

數據結構

Redis 提供瞭如下幾種典型的數據結構算法

strings

Redis 實現了名爲 SDS(Simple Dynamic String) 的字符串類型,與 C 字符串區別:spring

  1. 實現字符串拼接,減小內存重分配
  2. 維護了字符串的長度,以便快速獲取及避免緩衝區溢出
  3. 二進制安全,即支持存儲空格(\0)

linkedlist

Redis 實現了雙向無環鏈表,並使用此數據結構實現了 list。sql

Hashtable

Redis 實現了符合自身使用場景的 HashMap,即數組加鏈表的實現。此數據結構實現了 Redis 中的 Hash、Set 數據類型。特色以下:數據庫

  1. 使用 MurmurHash3 Hash 算法,針對規律性強的字符串有更好分佈性。
  2. 新節點插入到表頭而非表尾,由於緩存必定程度上會存在,「後加入的緩存會比先前加入的緩存更容易被訪問」的特色。
  3. 漸進式 rehash。Redis 數據庫自己是個巨大的 Hash 表,每次 rehash 要操做幾百上千萬的 key,漸進式 rehash 則是其中必不可少的保障。 rehash 的方式是維護兩張表和索引,須要 rehash 時將 rehashIndex 置爲 0,而後每次除 insert 操做外,都會將 oldTable 的 rehashIndex 中數據轉移到 newTable 中,直到 rehashIndex == oldTable.length() - 1,再將 rehashIndex 置爲 -1,rehash 完成。

skiplist

跳躍表經過給鏈表分層,實現了平均 O(logN),最壞 O(N) 的時間複雜度。Redis 使用該數據結構實現了 Sorted Set 數據類型。另外 Sorted Set 中還須要使用 HashTable 來實現 O(1) 的查詢。json

intset

整數集合,即只保存整數的集合。Redis 使用該數據結構實現了 Set。數組

ziplist

壓縮列表。壓縮列表是一種犧牲性能節約空間的數據結構,相比鏈表,它節約了指針的空間,Redis 將它做爲 List、Hash、Sorted Set 的實現,並使用 hash-max-ziplist-entries(512)、hash-max-ziplist-value(64)、list-max-ziplist-size(8 Kb)、zset-max-ziplist-entries(128)、zset-max-ziplist-value(64) 配置來決定是否使用 ziplist。緩存

持久化

不管是內存型的數據庫仍是關係型數據庫,宕機、停電後數據沒法恢復都是不可接受的。Redis 有兩種備份數據的方式:安全

AOF

即 Append-Only-File,當開啓備份時,Redis 會建立出一個默認名稱爲 appendonly.aof 的文件。並將內存中全部數據以命令的形式寫入文件中,後續執行新的操做數據的命令時,會放入緩衝區中定時寫入文件(appendfsync 不爲 always 時)。 在 redis.conf 中用如下參數配置 AOF 策略:

appendonly yes/no 是否開啓 AOF 模式
appendfilename appendonly.aof
appendfsync always/everysec/no #寫入磁盤時機,always 表示每次都會同步到磁盤,因爲是同步操做,性能降低嚴重。everysec 表示每秒刷盤。no 表示只放入緩存區中,由操做系統指定刷盤時機(Linux 通常是 30 秒) 複製代碼

當我執行了如下命令時:

set liuzhiguo 123 set liuzhiguo abc set liuzhiguo 456 set liuzhiguo 1231 ex 30 複製代碼

AOF 文件長這樣:

*2 		消息行數
$6 第一條消息長度 SELECT 消息內容 $1 第二條消息長度 0 消息內容 *3 $3 set $9 liuzhiguo $3 123 *3 $3 set $9 liuzhiguo $3 abc *3 $3 set $9 liuzhiguo $3 456 *3 $3 set $9 liuzhiguo $4 1231 *3 $9 PEXPIREAT $9 liuzhiguo $13 1544420872751 複製代碼

能夠看出 AOF 模式是直接將命令寫入文件中,因此在恢復數據時,Redis 會逐條執行命令來恢復數據。因此 AOF 模式恢復數據的效率並不高,並且當重複對一個 key 進行操做時,也須要執行全部操做命令。 針對同一數據重複操做的問題,Redis 提供了 AOF 重寫的功能,即丟棄原有的 appendonly.aof 文件,從新將內存中的數據做爲命令寫入文件中。

RDB

即 Redis DataBase,此持久化模式默認開啓。 開始備份時,Redis 會 fork 出一個子進程(bgsave),建立默認名爲 dump.rdb 的二進制文件,逐個對內存中的數據進行備份。每次備份時都會拋棄原有的 RDB 文件,從新將數據全量備份。 對於備份的時機,在 redis.conf 有如下選項來觸發備份:

save 900 1		900 秒內有 1 次變更
save 300 10		300 秒內有 10 次變更
save 60 10000	60 秒內有 10000 次變更
複製代碼

RDB 因爲體積和自然的指令壓縮能力,恢復數據速度要大大快於 AOF。可是由於每次只能全量備份,資源消耗比 AOF 大,不如 AOF 靈活。而且由於備份時機的不肯定性,數據完整不如 AOF。

RDB-AOF

Redis 在 4.0 以後提出了 RDB-AOF 混合模式持久化,能夠在 redis.conf 中經過 aof-use-rdb-preamble 選項開啓。 此模式下,全量備份、重寫 AOF 時會使用 RDB 格式,隨後執行命令仍是以 AOF 的格式追加到文件中。

這樣一來,恢復數據時性能比單純 AOF 強,全量備份比 AOF 快,備份體積比 AOF 小,部分備份性能比 RDB 高。

高可用

Redis 經過哨兵(Sentinel)與複製的方式實現了高可用

複製

經過在 redis.conf 文件中配置「slaveof ip port」或給運行中的 redis 節點執行命令「slaveof ip port」,便可使得該節點成爲某個 redis 實例的從節點。

從節點(slave)啓動時會向主節點(master)發送 sync 指令,主節點使用 bgsave 方法生成 RDB 文件,並創建緩衝區記錄寫命令。RDB 文件生成會即發送給從節點,從節點開始載入 RDB 文件,此動做同步執行。 從節點完成載入後,主服務器會將緩衝區的記錄發送給從服務器,此後主節點每當有執行命令時,都會傳播給從節點一份。

斷線重連後,從節點再次上線時會向主節點發送 psync 命令執行部分重同步,主節點會將此期間的命令發送給從節點執行。爲實現此功能,主從節點維護了「複製偏移量」。

使用 info 能夠查看複製的狀態:

# Replication role:master connected_slaves:1 slave0:ip=127.0.0.1,port=6380,state=online,offset=280,lag=0 // 從節點信息 master_replid:6088224db78515c7c2cbef387fb90cefd459f0d5 master_repl_offset:280 // 主節點偏移量 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:280 # Replication role:slave master_host:127.0.0.1 master_port:6379 master_link_status:up master_last_io_seconds_ago:1 // 與主節點 1 秒前同步 master_sync_in_progress:0 // 是否在進行 sync 同步 slave_repl_offset:280 // 從節點偏移量 slave_priority:100 slave_read_only:1 connected_slaves:0 master_replid:6088224db78515c7c2cbef387fb90cefd459f0d5 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:280 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:280 複製代碼

Sentinel

爲實現高可用,只有複製是不夠的,還須要主節點服務不可用後,從節點能自動補位。 Redis 經過 Sentinel 來實現節點監控與協調,Sentinel 是一個特殊的 Redis 節點,須要啓動時指定參數 --sentinel 和 sentinel.conf 配置文件,並在配置文件中指定主節點的 ip、host。 Sentinel 啓動後會向主節點發送 info 命令,獲取到相應的從節點信息,並與從節點創建鏈接。 當主節點不響應時,Sentinel 會等待至配置中指定的 timeout 時間,隨後將從節點提高爲主節點。主節點再次啓動時,Sentinel 會向主節點發送 slaveof 命令,要求其成爲從節點。

Sentinel 自己一樣支持高可用,多個 Sentinel 會向每一個主從節點 publish 本身的信息,以此來得知其餘 Sentinel 的存在並創建鏈接。多個 Sentinel 共存時,對主從節點狀態、身份的共識會有更復雜的協調過程,這就是另一個漫長的故事了。

對 Sentinel 的詳細介紹,能夠見:https://redis.io/topics/sentinel ,以及參考《Redis 設計與實現(第二版)》.

集羣

Redis 由於是內存型數據庫,在存儲空間上容易捉襟見肘,因而產生了許多擴容方案。

客戶端分片

如 ShardedJedis,經過在客戶端對 key 進行 hash,再分給指定的節點。 優勢:無需改動 Redis 便可擴容 缺點:只能擴容一次,沒法平滑升級

代理層分片

如 Twemproxy。代理層接收客戶端的請求,代理到對應的 Redis 節點上,一般也是使用一致性 hash 來分片。並因爲代理層能夠統一配置或讀取同一數據源,作到可拓展代理層。 優勢:客戶端無需關心 Redis 服務狀態,也無需分片。 缺點:難以擴容。

Redis Cluster

Redis 本身實現的集羣,可實現無痛擴容,平滑遷移。啓動集羣模式須要在配置文件中配置:

cluster-enabled yes
cluster-config-file nodes.conf 
cluster-node-timeout 1500
複製代碼

集羣模式下,會建立出 16384 個槽,並給集羣中每一個節點分配本身的槽數,槽必須被所有指定才能工做,一個節點最低指定一個槽。因此 Redis 集羣理論上最大是 16384 個節點。

當須要添加/獲取某個 key 時,經過 crc16(key) & 16384 獲得這個 key 應在的槽,隨後找出這個槽所在的節點,若是節點是本身直接執行,不然會返回給客戶端對應的節點的 ip + port。

Redis 集羣是去中心化的,彼此之間狀態同步靠 gossip 協議通訊,集羣的消息有如下幾種類型:

  1. Meet。經過「cluster meet ip port」命令,已有集羣的節點會向新的節點發送邀請,加入現有集羣。
  2. Ping。節點每秒會向集羣中其餘節點發送 ping 消息,消息中帶有本身已知的兩個節點的地址、槽、狀態信息、最後一次通訊時間等。
  3. Pong。節點收到 ping 消息後會回覆 pong 消息,消息中一樣帶有本身已知的兩個節點信息。
  4. Fail。節點 ping 不通某節點後,會向集羣全部節點廣播該節點掛掉的消息。其餘節點收到消息後標記已下線。

因爲去中心化和通訊機制,Redis Cluster 選擇了最終一致性和基本可用。例如當加入新節點時(meet),只有邀請節點和被邀請節點知道這件事,其他節點要等待 ping 消息一層一層擴散。除了 Fail 是當即全網通知的,其餘諸如新節點、節點重上線、從節點選舉成爲主節點、槽變化等,都須要等待被通知到。

所以,因爲 gossip 協議,Redis Cluster 對服務器時間的要求較高,不然時間戳不許確會影響節點判斷消息的有效性。另外節點數量增多後的網絡開銷也會對服務器產生壓力。所以官方推薦最大節點數爲 1000。對於 Redis 集羣的運維,能夠參考 優酷藍鯨近千節點的 Redis 集羣運維經驗總結

優勢:

  1. 真正的彈性擴容縮容。
  2. 擴容期間不影響使用。

缺點:

  1. 缺少管理平臺。
  2. 客戶端要另作兼容。
  3. 部分命令不支持

Redis 經過 Cluster 解決了擴容以後,客戶端該怎麼使用呢? 如 JedisCluster,每次請求前會拉取節點的 cluster info 來計算應該到哪一個節點請求,並須要對錯誤節點返回的 ASK 消息作相應的處理。由此產生的問題是

  1. 每次操做最少請求兩次。
  2. 每次若是隻請求某一個節點,也會造成單點壓力。

對問題 1,解決辦法是客戶端緩存集羣狀態。對問題 2,JedisCluster 支持配置多個節點,拉取節點信息時會隨機選擇某節點以分攤壓力。對問題 2 的處理方式,須要將 Redis 節點信息同步到客戶端配置中,產生了耦合。

另外的問題是,集羣狀態下是不支持 mget、mset 等須要跨節點執行的命令。該問題的解決方案是加一層 Proxy,推薦 優酷土豆的Redis服務平臺化之路,其使用 Nginx + Redis Cluster 的思路使人讚歎,並用請求聚合的方式實現了跨節點執行命令的問題。

阿里雲提供的 Redis 服務一樣實現了集羣模式下的跨節點命令,採用代理 + 分片服務器 + 分片配置服務器(極可能是 zookeeper),可是沒有使用 Redis Cluster 機制,而是本身實現的「分片」,保留了 slot。阿里雲的 Redis 好處是集羣版無需客戶端作兼容,能夠當成單機 Redis 使用,出了問題方便甩鍋。

第三方魔改 Redis

在等待 Redis 出官方集羣方案以前,人們火燒眉毛想要集羣版的 Redis,一些不滿於現狀以及不滿於 Redis Cluster 實現的人們開始對 Redis 進行改造。前面提到的阿里雲 Redis 也屬於魔改後的 Redis。

Codis

Codis 幾乎是最知名的第三方 Redis,對 Redis 進行了大量改造。 其架構爲 zookeeper + proxy + server-group(master + slave),並提供了控制檯以即可視化運維。

經過 zookeeper 記錄可用的 proxy 節點,再使用 Codis 開發組基於 Jedis 修改的 Jodis 客戶端到 zookeeper 中尋找可用的 proxy 節點進行調用。若是使用的是 jedis 或其餘客戶端,則只能到鏈接一個 proxy,或者想辦法鏈接到 zookeeper 獲取節點,再進行輪詢調用。

Codis 支持彈性擴容,分片方式與 Redis Cluster 相似,經過 crc32(key) % 1024 分紅 1024 個槽,每臺實例保存對應槽的數據。

LedisDB 和 SSDB

LedisDB 和 SSDB 很是類似,都是用 LevelDB 底層,從新實現了 Redis,或者說只實現了 Redis 協議。經過多線程 + 硬盤的方式,實現了和單機 Redis 類似的 QPS 性能,並能夠很大程度上對容量進行擴容。 LedisDB/SSDB 與 Redis 的關係,至關於 TiDB 與 MySQL 的關係。

缺點是出了容量上的成本優點,其餘沒有任何優點。

事務

Redis 提供 watchmultiexec 等方法實現樂觀鎖事務。使用事務的流程以下:

  1. watch key1 key2
  2. multi 開啓事務
  3. set key1 value一、set key2 value2,將指令入隊。
  4. exec,執行指令。

若是 multi ~ exec 之間 key1/key2 被其餘客戶端修改過,exec 時會返回 nil, set key1 value一、set key2 value2 均不會執行。 Redis 會保存一個 watch_keys 字典,結構爲: client -> keys、is_dirty。Redis 在處理每個會修改數據的命令時,會檢查 watch_keys 是否存在該 key,若是有,則修改 is_dirty 爲 true。

執行事務的客戶端在執行 exec 時,會檢查 is_dirty 字段,若是發現爲 false,全部的積累的指令會直接丟棄不執行。

事務在 Redis 中的使用場景很少,併發量大的狀況下須要反覆重試,大部分狀況下有更好的使用方式:

Lua

Redis 提供了對 Lua 腳本的支持,原子性執行一系列指令,並能夠寫代碼作邏輯判斷。 例如須要大量插入數據的場景:

for i=1,10000000,1 do local num = math.random(1000000,999999999); redis.call("set",num,i) end 複製代碼

執行一千萬條命令在本機大概用了 12 秒,QPS 83w。 Redis 在執行 Lua 腳本時是單線程,沒法處理其餘請求,這也是 Redis 原子性的緣由。下面是搶紅包時利用該特性實現的 Lua 腳本:

// 該腳本傳入 4 個參數 // KEYS[1] = 未領取的紅包列表 key // KEYS[2] = 已領取的紅包列表 key // KEYS[3] = 紅包已領取人ID列表 key // KEYS[4] = 領取人ID // 檢查領取人是否在已領取列表內 if redis.call('hexists', KEYS[3], KEYS[4]) ~= 0 then return nil else // 取出一個未領取的紅包 local redEnvelop = redis.call('rpop', KEYS[1]); if redEnvelop then // 紅包中的 receiver 填入領取人 ID local x = cjson.decode(redEnvelop); x['receiver'] = KEYS[4]; local re = cjson.encode(x); // 領取人放入已領取人ID列表,將紅包放入已領取紅包列表 redis.call('hset', KEYS[3], KEYS[4], KEYS[4]); redis.call('lpush', KEYS[2], re); // 給相應的 key 續期 if redis.call('llen', KEYS[2]) == 1 then redis.call('expire', KEYS[2], 172800); end if redis.call('hlen', KEYS[3]) == 1 then redis.call('expire', KEYS[3], 172800); end return re; end end return nil 複製代碼

須要注意的是,因爲 Lua 腳本只能在單個 Redis 實例執行,因此在集羣狀態下執行 Lua 時,Redis 會對要執行的 key 進行檢查。爲了保證全部 key 必定在某一臺機器上,Redis 限制了全部 key 都必須在同一個 slot 內才行。

因此針對紅包的場景,對 Lua 中傳入的 key 作了xxx{redpacketId}的處理,以保證全部 key 落在一個 slot 上。

管道(pipeline)

Redis 支持使用管道批量執行命令,再統一返回,減小往返次數,一般用於批量插入數據,批量獲取數據。

實戰

緩存

緩存方式

緩存是 Redis 最多見的場景。一般緩存的過程爲:

  1. 未命中:從數據源中取得數據,放入緩存中。
  2. 命中:返回數據。
  3. 更新:先把數據存入數據庫,再使緩存失效。

不推薦更新數據時同時更新到緩存,由於可能併發更新致使髒數據。見 爲何 Facebook 刪除緩存而不是更新緩存? 以及 Scaling Memcache at Facebook,其中提到「We choose to delete cached data instead of updating it because deletes are idempotent」。 但刪除緩存並非徹底不會致使髒數據,只是機率會相對小不少。

批量查詢

查詢時可能會須要相似 where id in (xx,yy,zz) 的狀況,這時查詢緩存可使用 mget 同時查詢多個 key,能夠大大提升效率。下面是 benchmark 數據:

get 81833.06 requests per second	
mget 10 73475.39 requests per second		  734,753
mget 20 64226.07 requests per second		  642,260			
mget 30 59559.26 requests per second		1,786,770			99%   < 1 milliseconds
mget 50 48995.59 requests per second		2,449,750			99%   < 1.5 milliseconds
mget 100 29214.14 requests per second		2,921,414			99%   < 2.5 milliseconds
mget 200 16730.80 requests per second		3,346,000			99%   < 3 milliseconds
mget 500 7222.30 requests per second		3,611,150			99%   < 9 milliseconds
複製代碼

根據總獲取數據個數、平均響應時間,一般認爲 mget 數量控制在 100 如下是比較均衡的。

按每次 mget 100 與 get 相比,性能至關於提升了 35 倍。再加上跨機器調用往返的時間消耗,實際狀況性能提高極可能 100 倍以上。

分佈式鎖

Redis 能夠經過 SET key randomValue NX EX 30 給某個 key 賦值,並同時判斷 key 是否存在,以及給定過時時間。過時時間要根據業務變化。

釋放鎖能夠直接 del 掉這個 key。可是 del 是有風險的:

例如 A 獲取到鎖,過時時間 30 秒。由於某些緣由 30 秒沒能處理完請求,B 過來也獲取到了鎖。此時 A 處理完執行釋放鎖的操做,就會釋放掉 B 所持有的鎖。

爲了不這個問題,須要判斷 value 是否是 set 時的 value,若是是才執行 del 操做。爲了讓這兩條命令原子性執行,須要使用到 lua 腳本:

- KEYS[1] 爲 鎖名稱,ARGV[1] 爲鎖內容, 即 set 時的 randomValue if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end 複製代碼

另外在 spring-data-redis 的實現中,是沒有 set nx ex 的,因此須要找到 Jedis 或者 Lettuce 調用原生方法。

計數器

Redis 另外一個值得稱道的命令就是自增了,其提供了 incr/incrby/incrbyfloat(string)、hincrby/hincr(hash)、zincrby(zset)方法供不一樣數據類型使用。

經過這些命令能夠實現對庫存的扣減,記錄接口訪問頻次,記錄一篇文章的點贊數、評論數、轉發數,搶紅包扣減數量等。

排行榜

利用 zset 有序列表,好比要計算用戶積分排行榜:

  • zadd/zincrby 保存或自增用戶的積分
  • zrevrank 獲取用戶的排名
  • zscore 獲取用戶的積分
  • zrevrange 獲取排行

消息隊列

利用 list 的 lpush(Left Push) 和 brpop(Blocked Right Pop) 接口能夠實現消息隊列功能:

  1. 將消息 lpush 到隊列中。
  2. 全部實例經過 brpop 監聽隊列並取出數據進行消費。

在消費的過程當中能夠經過配置線程池,根據業務狀況決定消費速率。

異步延遲合併隊列

好比秒殺、搶紅包時,庫存數據須要異步入庫。但僅僅異步入庫也是不夠的,並不會減小對數據庫操做的次數。這時候可能須要將 100 次請求壓縮成一次請求,只取最後的數據落庫。

此類需求則能夠用 zset + list 實現,咱們須要幾個東西:

  1. 須要延遲執行的任務放入 zset 列表中,score 爲須要執行的時間戳。
  2. 後臺起一個線程每秒鐘拉取 zset ,執行 zrangeByScore, score 範圍爲 0 ~ 當前時間戳,若是取到數據則放入執行隊列 list 中,最後 zrem 查出來的數據。
  3. 監聽 list 隊列的執行器,此時開始執行任務。

這也是紅包中的異步更新的實現方式。在搶羣紅包時,若是每次都更新數據庫中的數據,勢必會增長響應時間。使用這種更新方式的話,只在最後一次搶紅包的 30s 後更新,30s 以內發生的數據更新,都只會合併爲 1 條。

事件通知

Redis 提供了publishsubscribe 等命令實現了廣播功能,publish 時能夠將消息通知到某個頻道(channel),此時 subscribe 了這個頻道的節點均能收到消息。

經過這個機制咱們能作到對全節點的事件通知。

好比在積分系統中會將全部活動、抽獎、簽到、搖錢樹等數據庫配置數據放入 JVM 緩存中,以便得到最高的性能。 爲了更新數據,一開始是每分鐘到數據庫更新一次。但問題是每臺實例更新的時機都不一樣,致使請求到 A 實例的數據,與 B 實例上的不一樣。隨後將定時任務的配置改爲了每分鐘的第 0 秒執行,則很大程度上改善了問題。

可是輪詢的方式仍然不夠優雅,絕大部分時候取得的配置並無變化,是無用的請求。更新配置的時機應該是配置發生了變動纔對。

這時就可使用 Redis 廣播,每當數據庫數據發生變化時,經過廣播通知全部節點更新數據,或者乾脆將要更新的數據放入廣播中。

優化

Redis 雖然性能強悍,可是因爲單線程的特性,一旦產生慢查詢,會將全部操做都阻塞住。因此使用上仍須要注意會踩哪些坑。Redis 提供了 slowlog get 查看慢查詢。

常見雷區

  • keys * keys 命令的時間複雜度是 O(n),n 是 Redis 中全部鍵的數量,這個是最多見的性能最差的命令。通常線上都把這個命令 block 掉(在配置中加 rename-command KEYS "")

  • 大 key。一個 key 裏存儲的數據越多,一般性能越差,好比對超大的 List 進行 lindex 和 lrange。另外大 value 在集羣數據遷移時會阻塞可能致使 fail over。甚至在刪除時也會阻塞,例如刪除一個 1kw 數據量的 set,須要耗時 5s。或者在集羣中大 key 會致使集羣內存分配不均勻。因此在使用時須要避免在一個 key 中放入過多數據。

  • bgrewriteaofbgsave,重寫 aof 文件及備份 RDB 文件時,會 fork 出子進程和內存,此期間是阻塞的,取決於 Redis 內存大小和機器性能。因此許多企業的作法是主節點上關閉 aof 和 rdb,只在從節點上備份。

大 key 的拆分

積分系統中存在一個進貢的任務,邀請人可得到被邀請人作任務的獎勵,並在天天凌晨入帳。

對於這個任務,咱們作的第一步優化就是天天將得到了進貢獎勵的用戶,保存在 set 裏,經過 sscan 遍歷須要進貢的用戶,執行任務。以此避免了掃庫,保證每次取得的 userId 都是確切有效的。問題在於萬一子彈短信火了,set 中的 userId 會原來越多,也就遇到了大 key 的問題,須要將 set 拆分爲多個 set。

拆分的思路和 Redis 集羣分片相似,經過 hash(userId) % count 的方式,獲得 0 ~ count 之間的分片數,將其加到本來的 key 上,過程以下:

  1. 經過 hash(userID) % count,獲得分片數,如 16
  2. 本來 key 爲「TRIBUTE:USER:SET:20181225」,再加上分片數即獲得「TRIBUTE:USER:SET:20181225:16」,再將 userId sadd 放入便可
  3. 取出全部 key 時 for 循環從 0-count 拼到 key 上,再針對每一個 key sscan。

使用 Hash

例如保存一篇文章的點贊數、轉發數、評論數時,既能夠保存爲 3 個 value,即 article:like、article:repost、article:comment。也能夠保存爲一個 hash 對象,key 爲 article,hashKey 爲 like、repost、comment。

好處:

  1. 經過一條 hgetall 就能取得所需數據
  2. 節約內存。

使用 value :

# lua for i=1,1000000,1 do redis.call("set","article:like:"..i,1) redis.call("set","article:repost:"..i,1) redis.call("set","article:comment:"..i,1) end # memory used_memory:226568704 used_memory_human:216.07M used_memory_rss:282144768 used_memory_rss_human:269.07M 複製代碼

使用 hash :

# lua for i=1,1000000,1 do redis.call("HMSET","article:"..i, "like", 1, "repost", 1, "comment", 1) end # memory used_memory:121402896 used_memory_human:115.78M used_memory_rss:132640768 used_memory_rss_human:126.50M 複製代碼

value 幾乎多使用了一倍內存。緣由是 hash 類型這時會選擇 ziplist 數據結構實現。

做者:劉志國 連接:https://juejin.im/post/5c77a165e51d453ed335e4e6 來源:掘金 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
相關文章
相關標籤/搜索