新聞中心
如何實(shí)現(xiàn)高可用的 redis 集群
Redis 因具有豐富的數(shù)據(jù)結(jié)構(gòu)和超高的性能以及簡(jiǎn)單的協(xié)議,使其能夠很好的作為數(shù)據(jù)庫(kù)的上游緩存層。但在大規(guī)模的 Redis 使用過(guò)程中,會(huì)受限于多個(gè)方面:?jiǎn)螜C(jī)內(nèi)存有限、帶寬壓力、單點(diǎn)問(wèn)題、不能動(dòng)態(tài)擴(kuò)容等。
我們提供的服務(wù)有:成都網(wǎng)站建設(shè)、成都網(wǎng)站制作、微信公眾號(hào)開(kāi)發(fā)、網(wǎng)站優(yōu)化、網(wǎng)站認(rèn)證、蘆溪ssl等。為超過(guò)千家企事業(yè)單位解決了網(wǎng)站和推廣的問(wèn)題。提供周到的售前咨詢和貼心的售后服務(wù),是有科學(xué)管理、有技術(shù)的蘆溪網(wǎng)站制作公司
基于以上, Redis 集群方案顯得尤為重要。通常有 3 個(gè)途徑:官方 Redis Cluster ;通過(guò) Proxy 分片;客戶端分片 (Smart Client) 。以上三種方案各有利弊。
Redis Cluster( 官方 ) :雖然正式版發(fā)布已經(jīng)有一年多的時(shí)間,但還缺乏最佳實(shí)踐;對(duì)協(xié)議進(jìn)行了較大修改,導(dǎo)致主流客戶端也并非都已支持,部分支持的客戶端也沒(méi)有經(jīng)過(guò)大規(guī)模生產(chǎn)環(huán)境的驗(yàn)證;無(wú)中心化設(shè)計(jì)使整個(gè)系統(tǒng)高度耦合,導(dǎo)致很難對(duì)業(yè)務(wù)進(jìn)行無(wú)痛的升級(jí)。
Proxy :現(xiàn)在很多主流的 Redis 集群都會(huì)使用 Proxy 方式,例如早已開(kāi)源的 Codis 。這種方案有很多優(yōu)點(diǎn),因?yàn)橹С衷?redis 協(xié)議,所以客戶端不需要升級(jí),對(duì)業(yè)務(wù)比較友好。并且升級(jí)相對(duì)平滑,可以起多個(gè) Proxy 后,逐個(gè)進(jìn)行升級(jí)。但是缺點(diǎn)是,因?yàn)闀?huì)多一次跳轉(zhuǎn),平均會(huì)有 30% 左右的性能開(kāi)銷。而且因?yàn)樵蛻舳耸菬o(wú)法一次綁定多個(gè) Proxy ,連接的 Proxy 如果掛了還是需要人工參與。除非類似 Smart Client 一樣封裝原有客戶端,支持重連到其他 Proxy ,但這也就帶來(lái)了客戶端分片方式的一些缺點(diǎn)。并且雖然 Proxy 可以使用多個(gè),并且可以動(dòng)態(tài)增加 proxy 增加性能,但是所有客戶端都是共用所有 proxy ,那么一些異常的服務(wù)有可能影響到其他服務(wù)。為每個(gè)服務(wù)獨(dú)立搭建 proxy ,也會(huì)給部署帶來(lái)額外的工作。
而我們選擇了第三種方案,客戶端分片 (Smart Client) ??蛻舳朔制啾?Proxy 擁有更好的性能,及更低的延遲。當(dāng)然也有缺點(diǎn),就是升級(jí)需要重啟客戶端,而且我們需要維護(hù)多個(gè)語(yǔ)言的版本,但我們更愛(ài)高性能。
下面我們來(lái)介紹一下我們的Redis集群:
概貌:
如圖0所示,
我們的 Redis 集群一共由四個(gè)角色組成:
Zookeeper :保存所有 redis 集群的實(shí)例地址, redis 實(shí)例按照約定在特定路徑寫(xiě)入自身地址,客戶端根據(jù)這個(gè)約定查找 redis 實(shí)例地址,進(jìn)行讀寫(xiě)。
Redis 實(shí)例:我們修改了 redis 源碼,當(dāng) redis 啟動(dòng)或主從切換時(shí),按照約定自動(dòng)把地址寫(xiě)到 zookeeper 特定路徑上。
Sentinel : redis 自帶的主從切換工具,我們通過(guò) sentinel 實(shí)現(xiàn)集群高可用。
客戶端( Smart Client ):客戶端通過(guò)約定查找 redis 實(shí)例在 ZooKeeper 中寫(xiě)入的地址。并且根據(jù)集群的 group 數(shù),進(jìn)行一致性哈希計(jì)算,確定 key 唯一落入的 group ,隨后對(duì)這個(gè) group 的主庫(kù)進(jìn)行操作。客戶端會(huì)在Z ooKeeper 設(shè)置監(jiān)視,當(dāng)某個(gè) group 的主庫(kù)發(fā)生變化時(shí),Z ooKeeper 會(huì)主動(dòng)通知客戶端,客戶端會(huì)更新對(duì)應(yīng) group 的最新主庫(kù)。
我們的Redis 集群是以業(yè)務(wù)為單位進(jìn)行劃分的,不同業(yè)務(wù)使用不同集群(即業(yè)務(wù)和集群是一對(duì)一關(guān)系)。一個(gè) Redis 集群會(huì)由多個(gè) group 組成 ( 一個(gè) group 由一個(gè)主從對(duì) redis 實(shí)例組成 ) 。即 group 越多,可以部署在更多的機(jī)器上,可利用的內(nèi)存、帶寬也會(huì)更多。在圖0中,這個(gè)業(yè)務(wù)使用的 redis 集群由 2 個(gè) group 組成,每個(gè) group 由一對(duì)主從實(shí)例組成。
Failover
如圖1所示,
當(dāng) redis 啟動(dòng)時(shí),會(huì) 把自己的 IP:Port 寫(xiě)入到 ZooKeeper 中。其中的 主實(shí)例模式啟動(dòng)時(shí)會(huì)在 /redis/ 業(yè)務(wù)名 / 組名 永久節(jié)點(diǎn)寫(xiě)入自己的 IP:Port (如果節(jié)點(diǎn)不存在則創(chuàng)建)。由 主模式 變成 從模式 時(shí),會(huì)創(chuàng)建 /redis/ 業(yè)務(wù)名 / 組名 /slaves/ip:port 臨時(shí)節(jié) 點(diǎn),并寫(xiě)入自己的 IP:Port (如果相同節(jié)點(diǎn)已經(jīng)存在,則先刪除,再創(chuàng)建)。而從實(shí)例 模式 啟動(dòng)時(shí)會(huì)創(chuàng)建 /redis/ 業(yè)務(wù)名 / 組名 /slaves/ip:port 臨時(shí)節(jié)點(diǎn),并寫(xiě)入自己的 ip:port (如果相同節(jié)點(diǎn)已經(jīng)存在,則先刪除,再創(chuàng)建)。由 從模式 變成 主模式 時(shí),先刪除 /redis/ 業(yè)務(wù)名 / 組名 /slaves/ip:port 臨時(shí)節(jié)點(diǎn),并在 /redis/ 業(yè)務(wù)名 / 組名 永久節(jié)點(diǎn)寫(xiě)入自己的 IP:Port 。
ZooKeeper 會(huì)一直保存當(dāng)前有效的 主從實(shí)例 IP:Port 信息。至于主從自動(dòng)切換過(guò)程,使用 redis 自帶的 sentinel 實(shí)現(xiàn),現(xiàn)設(shè)置為超過(guò) 30s 主 server 無(wú)響應(yīng),則由 sentinel 進(jìn)行主從實(shí)例的切換,切換后就會(huì)觸發(fā)以主、從實(shí)例通過(guò)以上提到的一系列動(dòng)作,從而完成最終的切換。
而客戶端側(cè)通過(guò)給定業(yè)務(wù)名下的所有 groupName 進(jìn)行一致性哈希計(jì)算,確定 key 落入哪個(gè)組。 客戶端啟動(dòng)時(shí),會(huì)從 ZooKeeper 獲取指定業(yè)務(wù)名下所有 group 的 主從 IP:Port ,并在 ZooKeeper 中設(shè)置監(jiān)視(監(jiān)視的作用是當(dāng) ZooKeeper 的節(jié)點(diǎn)發(fā)生變化時(shí),會(huì)主動(dòng)通知客戶端)。若客戶端從 Zookeeper 收到節(jié)點(diǎn)變化通知,會(huì)重新獲取最新的 主從 I:Port ,并重新設(shè)置監(jiān)視( ZooKeeper 監(jiān)視是一次性的)。通過(guò)此方法,客戶端可以實(shí)時(shí)獲知當(dāng)前可訪問(wèn)最新的 主從 IP:Port 信息。
因?yàn)槲覀兊乃?redis 實(shí)例信息都按照約定保存在 ZooKeeper 上,所以不需要針對(duì)每個(gè)實(shí)例部署監(jiān)控,我們編寫(xiě)了一個(gè)可以自動(dòng)通過(guò) ZooKeeper 獲取所有 redis 實(shí)例信息,并且監(jiān)控 cpu 、 qps 、內(nèi)存、主從延遲、主從切換、連接數(shù)等的工具。
發(fā)展:
現(xiàn)在 redis 集群在某些業(yè)務(wù)內(nèi)存需求超過(guò)預(yù)期很多后,無(wú)法通過(guò)動(dòng)態(tài)擴(kuò)容進(jìn)行擴(kuò)展。所以我們正在做動(dòng)態(tài)擴(kuò)容的支持。原先的客戶端我們是通過(guò)一致性哈希進(jìn)行 key 的
路由策略,但這種方式在動(dòng)態(tài)擴(kuò)容時(shí)會(huì)略顯復(fù)雜,所以我們決定采用實(shí)現(xiàn)起來(lái)相對(duì)簡(jiǎn)單的預(yù)分片方式。一致性哈希的好處是可以無(wú)限擴(kuò)容,而預(yù)分片則不是。預(yù)分片
時(shí)我們會(huì)在初始化階段指定一個(gè)集群的所有分片數(shù)量,這個(gè)數(shù)量一旦指定就不能再做改變,這個(gè)預(yù)分片數(shù)量就是后續(xù)可以擴(kuò)容到最大的 redis 實(shí)例數(shù)。假設(shè)預(yù)分片 128 個(gè) slot ,每個(gè)實(shí)例 10G 也可以達(dá)到 TB 級(jí)別的集群,對(duì)于未來(lái)數(shù)據(jù)增長(zhǎng)很大的集群我們可以預(yù)分片 1024 ,基本可以滿足所有大容量?jī)?nèi)存需求了。
原先我們的 redis 集群有四種角色, Smart Client, redis , sentinel , ZooKeeper 。為了支持動(dòng)態(tài)擴(kuò)容,我們?cè)黾恿艘粋€(gè)角色, redis_cluster_manager (以下簡(jiǎn)稱 manager ),用于管理 redis 集群。主要工作是初始化集群(即預(yù)分片),增加實(shí)例后負(fù)責(zé)修改Z ooKeeper 狀態(tài),待客戶端做好準(zhǔn)備后遷移數(shù)據(jù)到新增實(shí)例上。為了盡量減少數(shù)據(jù)遷移期間對(duì)現(xiàn)性能帶來(lái)的影響,我們每次只會(huì)遷移一個(gè)分片的數(shù)據(jù),待遷移完成,再進(jìn)行下一個(gè)分片的遷移。
如圖2所示
相比原先的方案,多了 slots 、M anager Lock 、 clients 、M igrating Clients 節(jié)點(diǎn)。
Slots: 所有分片會(huì)把自身信息寫(xiě)入到 slots 節(jié)點(diǎn)下面。 Manager 在初始化集群時(shí),根據(jù)設(shè)置的分片數(shù),以及集群下的 group 數(shù),進(jìn)行預(yù)分片操作,把所有分片均勻分配給已有 group 。分片的信息由一個(gè) json 串組成,記錄有分片的狀態(tài) (stats) ,當(dāng)前擁有此分片的 group(src) ,需要遷移到的 group(dst) 。分片的狀態(tài)一共有三種: online 、 pre_migrate 、 migrating 。
Online 指這個(gè)分片處于正常狀態(tài),這時(shí) dst 是空值,客戶端根據(jù) src 的 group 進(jìn)行讀寫(xiě)。
Pre_migrate 是指這個(gè)分片被 manager 標(biāo)記為需要遷移,此時(shí) dst 仍然為空, manager 在等所有 client 都已經(jīng)準(zhǔn)備就緒,因?yàn)?ZooKeeper 回掉所有客戶端有時(shí)間差,所以如果某些 client 沒(méi)有準(zhǔn)備就緒的時(shí)候 manager 進(jìn)行了數(shù)據(jù)遷移,那么就會(huì)有數(shù)據(jù)丟失。
Migrating 是 manager 確認(rèn)了所有客戶端都已經(jīng)做好遷移準(zhǔn)備后,在 dst 寫(xiě)入此分片需要遷移的目標(biāo) group 。待遷移完成,會(huì)在 src 寫(xiě)入目標(biāo) group_name , dst 設(shè)為空, stats 設(shè)為 online 。
Manager Lock: 因?yàn)槲覀兪敲看沃辉试S遷移一個(gè) slot ,所以不允許超過(guò)一個(gè) manager 操作一個(gè)集群。所以 manager 在操作集群前,會(huì)在M anager Lock 下注冊(cè)臨時(shí)節(jié)點(diǎn),代表這個(gè)集群已經(jīng)有 manager 在操作了,這樣其他 manager 想要操作這個(gè)集群時(shí)就會(huì)自動(dòng)退出。
Clients 和M igrating Clients 是為了讓 manager 知道客戶端是否已經(jīng)準(zhǔn)備就緒的節(jié)點(diǎn)??蛻舳送ㄟ^(guò) uid 代表自己,格式是 客戶端語(yǔ)言 _ 主機(jī)名 _pid 。當(dāng)集群沒(méi)有進(jìn)行遷移,即所有分片都是 online 的時(shí)候,客戶端會(huì)在 clients 下創(chuàng)建 uid 的臨時(shí)節(jié)點(diǎn)。
當(dāng)某個(gè) slot 從 online 變成 pre_migrate 后,客戶端會(huì)刪除 clients 下的 uid 臨時(shí)節(jié)點(diǎn),然后在M igrating Clients 創(chuàng)建 uid 臨時(shí)節(jié)點(diǎn)。注意,因?yàn)樾枰WC數(shù)據(jù)不丟失,從 pre_migrate 到 migrating 期間,這個(gè) slot 是被鎖定的,即所有對(duì)這個(gè) slot 的讀寫(xiě)都會(huì)被阻塞。所以 mananger 會(huì)最多等待 10s ,確認(rèn)所有客戶端都已經(jīng)切換到準(zhǔn)備就緒狀態(tài),如果發(fā)現(xiàn)某個(gè)客戶端一直未準(zhǔn)備就緒,那么 mananger 會(huì)放棄此次遷移,把 slot 狀態(tài)由 pre_migrate 改為 online 。如果客戶端發(fā)現(xiàn) slot 狀態(tài)由 pre_migrate 變成 online 了,那么會(huì)刪除 migrating_clients 下的 uid 節(jié)點(diǎn),在 clients 下重新創(chuàng)建 uid 節(jié)點(diǎn)。還需要注意的一點(diǎn)是,有可能一個(gè)客戶剛啟動(dòng),并且正在往 clients 下創(chuàng)建 uid 節(jié)點(diǎn),但是因?yàn)榫W(wǎng)絡(luò)延遲還沒(méi)創(chuàng)建完成,導(dǎo)致 manager 未確認(rèn)到這個(gè) client 是否準(zhǔn)備就緒,所以 mananger 把 slot 改為 pre_migrate 后會(huì)等待 1s 再確認(rèn)所有客戶端是否準(zhǔn)備就緒。
如果 Manager 看到 clients 下已經(jīng)沒(méi)有客戶端的話(都已經(jīng)準(zhǔn)備就緒),會(huì)把 slot 狀態(tài)改為 migrating 。 Slot 變成 migrating 后,鎖定也隨之解除, manager 會(huì)遍歷 src group 的數(shù)據(jù),把對(duì)應(yīng) slot 的數(shù)據(jù)遷移到 dst group 里??蛻舳嗽?migrating 期間如果有讀寫(xiě) migrating slot 的 key ,那么客戶端會(huì)先把這個(gè) key 從 src group 遷移到 dst group ,然后再做讀寫(xiě)操作。即這期間客戶端性能會(huì)有所下降。這也是為什么每次只遷移一個(gè) slot 的原因。這樣即使只有 128 個(gè)分片的集群,在遷移期間受到性能影響的 key 也只有 1/128 ,是可以接受的。
Manager 發(fā)現(xiàn)已經(jīng)把 slot 已經(jīng)遷移完畢了,會(huì)在 src 寫(xiě)入目標(biāo) group_name , dst 設(shè)為空, stats 設(shè)為 online ??蛻舳艘矂h除 migrating_clients 下的 uid ,在 clients 下創(chuàng)建 uid 節(jié)點(diǎn)。
Redis數(shù)據(jù)庫(kù)適合使用于哪些應(yīng)用場(chǎng)景
redis開(kāi)創(chuàng)了一種新的數(shù)據(jù)存儲(chǔ)思路,使用redis,我們不用在面對(duì)功能單調(diào)的數(shù)據(jù)庫(kù)時(shí),把精力放在如何把大象放進(jìn)冰箱這樣的問(wèn)題上,而是利用redis靈活多變的數(shù)據(jù)結(jié)構(gòu)和數(shù)據(jù)操作,為不同的大象構(gòu)建不同的冰箱。
redis常用數(shù)據(jù)類型
redis最為常用的數(shù)據(jù)類型主要有以下五種:
string
hash
list
set
sorted set
在具體描述這幾種數(shù)據(jù)類型之前,我們先通過(guò)一張圖了解下redis內(nèi)部?jī)?nèi)存管理中是如何描述這些不同數(shù)據(jù)類型的:
首先redis內(nèi)部使用一個(gè)redisobject對(duì)象來(lái)表示所有的key和value,redisobject最主要的信息如上圖所示:type代表一
個(gè)value對(duì)象具體是何種數(shù)據(jù)類型,encoding是不同數(shù)據(jù)類型在redis內(nèi)部的存儲(chǔ)方式,比如:type=string代表value存儲(chǔ)的是
一個(gè)普通字符串,那么對(duì)應(yīng)的encoding可以是raw或者是int,如果是int則代表實(shí)際redis內(nèi)部是按數(shù)值型類存儲(chǔ)和表示這個(gè)字符串的,當(dāng)然
前提是這個(gè)字符串本身可以用數(shù)值表示,比如:"123"
"456"這樣的字符串。
這里需要特殊說(shuō)明一下vm字段,只有打開(kāi)了redis的虛擬內(nèi)存功能,此字段才會(huì)真正的分配內(nèi)存,該功能默認(rèn)是關(guān)閉狀態(tài)的,該功能會(huì)在后面具體描述。通過(guò)
上圖我們可以發(fā)現(xiàn)redis使用redisobject來(lái)表示所有的key/value數(shù)據(jù)是比較浪費(fèi)內(nèi)存的,當(dāng)然這些內(nèi)存管理成本的付出主要也是為了給
redis不同數(shù)據(jù)類型提供一個(gè)統(tǒng)一的管理接口,實(shí)際作者也提供了多種方法幫助我們盡量節(jié)省內(nèi)存使用,我們隨后會(huì)具體討論。
下面我們先來(lái)逐一的分析下這五種數(shù)據(jù)類型的使用和內(nèi)部實(shí)現(xiàn)方式:
string
常用命令:
set,get,decr,incr,mget 等。
應(yīng)用場(chǎng)景:
string是最常用的一種數(shù)據(jù)類型,普通的key/value存儲(chǔ)都可以歸為此類,這里就不所做解釋了。
實(shí)現(xiàn)方式:
string在redis內(nèi)部存儲(chǔ)默認(rèn)就是一個(gè)字符串,被redisobject所引用,當(dāng)遇到incr,decr等操作時(shí)會(huì)轉(zhuǎn)成數(shù)值型進(jìn)行計(jì)算,此時(shí)redisobject的encoding字段為int。
hash
常用命令:
hget,hset,hgetall 等。
應(yīng)用場(chǎng)景:
我們簡(jiǎn)單舉個(gè)實(shí)例來(lái)描述下hash的應(yīng)用場(chǎng)景,比如我們要存儲(chǔ)一個(gè)用戶信息對(duì)象數(shù)據(jù),包含以下信息:
用戶id為查找的key,存儲(chǔ)的value用戶對(duì)象包含姓名,年齡,生日等信息,如果用普通的key/value結(jié)構(gòu)來(lái)存儲(chǔ),主要有以下2種存儲(chǔ)方式:
第一種方式將用戶id作為查找key,把其他信息封裝成一個(gè)對(duì)象以序列化的方式存儲(chǔ),這種方式的缺點(diǎn)是,增加了序列化/反序列化的開(kāi)銷,并且在需要修改其中一項(xiàng)信息時(shí),需要把整個(gè)對(duì)象取回,并且修改操作需要對(duì)并發(fā)進(jìn)行保護(hù),引入cas等復(fù)雜問(wèn)題。
第二種方法是這個(gè)用戶信息對(duì)象有多少成員就存成多少個(gè)key-value對(duì)兒,用用戶id+對(duì)應(yīng)屬性的名稱作為唯一標(biāo)識(shí)來(lái)取得對(duì)應(yīng)屬性的值,雖然省去了序列化開(kāi)銷和并發(fā)問(wèn)題,但是用戶id為重復(fù)存儲(chǔ),如果存在大量這樣的數(shù)據(jù),內(nèi)存浪費(fèi)還是非??捎^的。
那么redis提供的hash很好的解決了這個(gè)問(wèn)題,redis的hash實(shí)際是內(nèi)部存儲(chǔ)的value為一個(gè)hashmap,并提供了直接存取這個(gè)map成員的接口,如下圖:
也就是說(shuō),key仍然是用戶id,
value是一個(gè)map,這個(gè)map的key是成員的屬性名,value是屬性值,這樣對(duì)數(shù)據(jù)的修改和存取都可以直接通過(guò)其內(nèi)部map的key(redis里稱內(nèi)部map的key為field),
也就是通過(guò) key(用戶id) + field(屬性標(biāo)簽)
就可以操作對(duì)應(yīng)屬性數(shù)據(jù)了,既不需要重復(fù)存儲(chǔ)數(shù)據(jù),也不會(huì)帶來(lái)序列化和并發(fā)修改控制的問(wèn)題。很好的解決了問(wèn)題。
這里同時(shí)需要注意,redis提供了接口(hgetall)可以直接取到全部的屬性數(shù)據(jù),但是如果內(nèi)部map的成員很多,那么涉及到遍歷整個(gè)內(nèi)部map的
操作,由于redis單線程模型的緣故,這個(gè)遍歷操作可能會(huì)比較耗時(shí),而另其它客戶端的請(qǐng)求完全不響應(yīng),這點(diǎn)需要格外注意。
實(shí)現(xiàn)方式:
上面已經(jīng)說(shuō)到redis
hash對(duì)應(yīng)value內(nèi)部實(shí)際就是一個(gè)hashmap,實(shí)際這里會(huì)有2種不同實(shí)現(xiàn),這個(gè)hash的成員比較少時(shí)redis為了節(jié)省內(nèi)存會(huì)采用類似一維數(shù)組的方式來(lái)緊湊存儲(chǔ),而不會(huì)采用真正的hashmap結(jié)構(gòu),對(duì)應(yīng)的value
redisobject的encoding為zipmap,當(dāng)成員數(shù)量增大時(shí)會(huì)自動(dòng)轉(zhuǎn)成真正的hashmap,此時(shí)encoding為ht。
list
常用命令:
lpush,rpush,lpop,rpop,lrange等。
應(yīng)用場(chǎng)景:
redis
list的應(yīng)用場(chǎng)景非常多,也是redis最重要的數(shù)據(jù)結(jié)構(gòu)之一,比如twitter的關(guān)注列表,粉絲列表等都可以用redis的list結(jié)構(gòu)來(lái)實(shí)現(xiàn),比較好理解,這里不再重復(fù)。
實(shí)現(xiàn)方式:
redis
list的實(shí)現(xiàn)為一個(gè)雙向鏈表,即可以支持反向查找和遍歷,更方便操作,不過(guò)帶來(lái)了部分額外的內(nèi)存開(kāi)銷,redis內(nèi)部的很多實(shí)現(xiàn),包括發(fā)送緩沖隊(duì)列等也都是用的這個(gè)數(shù)據(jù)結(jié)構(gòu)。
set
常用命令:
sadd,spop,smembers,sunion 等。
應(yīng)用場(chǎng)景:
redis
set對(duì)外提供的功能與list類似是一個(gè)列表的功能,特殊之處在于set是可以自動(dòng)排重的,當(dāng)你需要存儲(chǔ)一個(gè)列表數(shù)據(jù),又不希望出現(xiàn)重復(fù)數(shù)據(jù)時(shí),set是一個(gè)很好的選擇,并且set提供了判斷某個(gè)成員是否在一個(gè)set集合內(nèi)的重要接口,這個(gè)也是list所不能提供的。
實(shí)現(xiàn)方式:
set 的內(nèi)部實(shí)現(xiàn)是一個(gè)
value永遠(yuǎn)為null的hashmap,實(shí)際就是通過(guò)計(jì)算hash的方式來(lái)快速排重的,這也是set能提供判斷一個(gè)成員是否在集合內(nèi)的原因。
sorted set
常用命令:
zadd,zrange,zrem,zcard等
使用場(chǎng)景:
redis sorted set的使用場(chǎng)景與set類似,區(qū)別是set不是自動(dòng)有序的,而sorted
set可以通過(guò)用戶額外提供一個(gè)優(yōu)先級(jí)(score)的參數(shù)來(lái)為成員排序,并且是插入有序的,即自動(dòng)排序。當(dāng)你需要一個(gè)有序的并且不重復(fù)的集合列表,那么可以選擇sorted
set數(shù)據(jù)結(jié)構(gòu),比如twitter 的public
風(fēng)控系統(tǒng)實(shí)踐之感: drools 和 redis
需求:
開(kāi)發(fā)一個(gè)風(fēng)控系統(tǒng),系統(tǒng)包括, 規(guī)則引擎和計(jì)算引擎, 主要的內(nèi)容如下:
1. 規(guī)則的增刪改和實(shí)時(shí)生效, 規(guī)則的分類執(zhí)行
2. 按照一定的緯度計(jì)算累計(jì)值,比如按照 IP, 用戶 id, 賬戶 等緯度。
3. 需要支持滑動(dòng)窗口,滾動(dòng)窗口,長(zhǎng)度窗口等
遇到的問(wèn)題主要有以下幾點(diǎn):
1. redis 做流計(jì)算太過(guò)勉強(qiáng),一是根據(jù)業(yè)務(wù)上的需求,需要統(tǒng)計(jì)的key 至少有幾億個(gè),最多也有幾十億個(gè),另外redis 中需要存儲(chǔ)少量的交易的信息。估算下來(lái)量也是非常可觀
2. redis 中 hot key 特別明顯,比如按照商戶的緯度去統(tǒng)計(jì),如果不對(duì)商戶的key 進(jìn)行拆封,像盒馬那種流量的商戶,對(duì)redis 的壓力是非常大的。
我們采用的是redis 的cluster 模式,這樣的話redis hot key 對(duì)redis 影響會(huì)更大。對(duì)其進(jìn)行拆分是非常必要的,比如 按照小時(shí)拆分。?
3. 流式計(jì)算中,一個(gè)是亂序?qū)е吕奂拥挠?jì)算不準(zhǔn)確(有負(fù)值),另外一個(gè)是消息延遲. 當(dāng)時(shí)我們嘗試使用flink 中的水印的概念去解決問(wèn)題,發(fā)現(xiàn)并不適合。這個(gè)坑也是我們實(shí)踐過(guò)后才發(fā)現(xiàn)的。
最痛苦的經(jīng)歷是亂序和延遲消息的解決,現(xiàn)在是采取糾正的方式解決。
規(guī)則引擎
規(guī)則引擎我們選用了drools,簡(jiǎn)單的探索了drools core, drools DRL, drools CEP 等,但是回頭看看,針對(duì)drools的使用缺點(diǎn)還是很多, 而且很明顯,暫時(shí)還沒(méi)有替換的打算.?
1. 使用 drools CEP 如何做分布式? 我們發(fā)現(xiàn)drools CEP中的幾種窗口都是內(nèi)存計(jì)算的,應(yīng)用到分布式中就沒(méi)有很好的辦法,幾乎做不到,除非drools 也去集成redis等這種分布式緩存。
2. 使用drools 覺(jué)得很笨重,因?yàn)橐蕾嚤容^多,二是我們只用到了 drools 中的 if else 等判斷,許多其它的功能基本就用不到,因?yàn)?1 中解決不了分布式的問(wèn)題。所以從這點(diǎn)來(lái)說(shuō)drools 已經(jīng)廢了,根本不用在創(chuàng)建kiesession 這種 重量級(jí)的東西。
3. drools中支持的運(yùn)算符不是特別充分,比如像 log 運(yùn)算,sum, max, avg 這種的運(yùn)算等都是不支持的. DRL 語(yǔ)言對(duì)業(yè)務(wù)人員來(lái)說(shuō)不是非常的友好。
4. 另外drools 中的 連續(xù),非連續(xù)的規(guī)則,沒(méi)有看出來(lái)如何配置,至少flink cep 是有這樣API的。
綜上所描述,不得不吐槽下 drools真是無(wú)語(yǔ),也許了解的很簡(jiǎn)單,還有別的方式,另外drools workbench 也是很無(wú)語(yǔ),很復(fù)雜,估計(jì)drools 廠商想通過(guò)這種方式掙錢。
總體感覺(jué),如果有別的選擇,最好不要選用drools,分布式的問(wèn)題沒(méi)有解決,就等于廢了,因?yàn)楦鞣N分布式窗口都需要我們自己去實(shí)現(xiàn)。怎么辦呢??
規(guī)則引擎最后還是采用了drools,根據(jù)具體的業(yè)務(wù)含義創(chuàng)建不同的kiesession,? drools 起到了if else 判斷的作用,至于滾動(dòng)窗口,長(zhǎng)度窗口和滑動(dòng)窗口都通過(guò)redis來(lái)做計(jì)算。遇到頭疼的問(wèn)題,是
1. 根據(jù)不同的統(tǒng)計(jì)緯度,大概計(jì)算了下,需要幾十億個(gè)key,在redis 中做計(jì)算
2. 滑動(dòng)窗口暫時(shí)靠 redis的zsort 的數(shù)據(jù)結(jié)構(gòu),性能不是非常好
3. 熱點(diǎn)key 的問(wèn)題,特別對(duì)于大商戶的熱點(diǎn)key 的問(wèn)題,需要做拆分,拆分起來(lái)是比較復(fù)雜的
4. 消息延遲和消息亂序問(wèn)題。
所以計(jì)算引擎的需求一般是
1. 計(jì)算很快,大幾百個(gè)規(guī)則,能夠很快的計(jì)算出準(zhǔn)確的結(jié)果來(lái)
2. 計(jì)算準(zhǔn)確率,當(dāng)面對(duì)亂序和延遲消息的時(shí)候,如何計(jì)算的更加準(zhǔn)確
3. 計(jì)算的量的問(wèn)題,正如前面提到的,幾十億個(gè)key,另外還需要存儲(chǔ)一些信息,計(jì)算的中間狀態(tài)等,如何在redis 中丟失,就會(huì)造成計(jì)算不準(zhǔn)確。
基于以上的問(wèn)題,關(guān)鍵是如何做的更好,優(yōu)化的更好,說(shuō)實(shí)話,我沒(méi)有找到答案,可以做的就是不斷的優(yōu)化redis 計(jì)算(暫時(shí)不能上大數(shù)據(jù),比如flink, spark 等),減少redis 的操作帶來(lái)的網(wǎng)絡(luò)開(kāi)銷。
其實(shí)最后還要提一下,如果能采用內(nèi)存計(jì)算,不用分布式計(jì)算,會(huì)不會(huì)速度更快點(diǎn),比如根據(jù)業(yè)務(wù)來(lái)做分片,這樣在各個(gè)實(shí)例統(tǒng)計(jì)的中間值就不用匯總,那么每個(gè)實(shí)例只需要內(nèi)存計(jì)算就好,不需要訪問(wèn)redis而帶來(lái)的網(wǎng)絡(luò)開(kāi)銷。但是這樣做也會(huì)帶來(lái)架構(gòu)層面的調(diào)整,比如 如何做 fault tolerance, 如何做 狀態(tài)持久化, 等一系列的問(wèn)題。?
從使用redis結(jié)果來(lái)看,效果也不是那么差,不考慮非常熱點(diǎn)key 的情況下,最高tps 也達(dá)到6000多(2 臺(tái)機(jī)器,16core,32G 內(nèi)存), 一般公司的業(yè)務(wù)其實(shí)是可以滿足的,對(duì)于非常熱點(diǎn)的key,后續(xù)的優(yōu)化是繼續(xù)拆分.
一個(gè)好的風(fēng)控系統(tǒng)是非常難的,做以筆記,以希望不斷成長(zhǎng)
新聞標(biāo)題:關(guān)于zblogredis的信息
當(dāng)前路徑:http://fisionsoft.com.cn/article/ddsjied.html