Redis緩衝之Set使用及redis遇到的一些問題

來源:互聯網
上載者:User

Redis緩衝Set使用

在Redis中,我們可以將Set類型看作為沒有排序的字元集合,和List類型一樣,我們也可以在該類型的資料值上執行添加、刪除或判斷某一元素是否存在等操作。需要說明的是,這些操作的時間複雜度為O(1),即常量時間內完成次操作。Set可包含的最大元素數量是4294967295。

和List類型不同的是,Set集合中不允許出現重複的元素,這一點和C++標準庫中的set容器是完全相同的。換句話說,如果多次添加相同元素,Set中將僅保留該元素的一份拷貝。和List類型相比,Set類型在功能上還存在著一個非常重要的特性,即在伺服器端完成多個Sets之間的彙總計算操作,如unions、intersections和differences。由於這些操作均在服務端完成,因此效率極高,而且也節省了大量的網路IO開銷。

Redis做緩衝Set可能到的的比較多

開啟redis伺服器:
'''


開啟redis用戶端:


這就是一個set集合!

至於redisset的命令小夥伴們可以參考(http://redisdoc.com)

下面分享redis在.net中的使用方法

1,獲得集合

// 擷取sortset表中setId中的所有keys,倒序擷取public List<string> GetAllItemsFromSortedSetDesc(string setId){    List<string> result = ExecuteCommand<List<string>>(client =>    {        return client.GetAllItemsFromSortedSetDesc(setId);    });    return result;}public List<string> GetAllItemsFromSortedSet(string setId){    List<string> result = ExecuteCommand<List<string>>(client =>    {        return client.GetAllItemsFromSortedSet(setId);    });    return result;}// 擷取sortset表中setId中的所有keys,valuespublic IDictionary<string, double> GetAllWithScoresFromSortedSet(string setId){    IDictionary<string, double> result = ExecuteCommand<IDictionary<string, double>>(client =>    {        return client.GetAllWithScoresFromSortedSet(setId);        //return client.GetFromHash<Dictionary<string, string>>(hashID);    });    return result;}

2,刪除某個set

// 刪除某個KEY的值,成功返回TRUEpublic bool RemoveKey(string key){    bool result = false;    result = ExecuteCommand<bool>(client =>         {             return client.Remove(key);         });    return result;} // 刪除Set資料中的某個為item的值public bool RemoveItemFromSet(string setId, string item){    byte[] bvalue = System.Text.Encoding.UTF8.GetBytes(item);    bool result = ExecuteCommand<bool>(client =>    {        var rc = client as RedisClient;        if (rc != null)        {            return rc.SRem(setId, bvalue) == 1;        }        return false;    });    return result;}


3,搜尋

//搜尋keypublic List<string> SearchKeys(string pattern){    List<string> result = ExecuteCommand<List<string>>(client =>    {        return client.SearchKeys(pattern);    });    return result;}


4,增加某個元素到set

public bool AddItemToSet(string setId, string item) {     byte[] bvalue = System.Text.Encoding.UTF8.GetBytes(item);     bool result = ExecuteCommand<bool>(client =>     {         var rc = client as RedisClient;         if (rc != null)         {             return rc.SAdd(setId, bvalue) == 1;         }         return false;     });     return result;  }


這裡只分享幾個方法,其實還有很多關於set的操作方法。

利用Redis提供的Sets資料結構,可以儲存一些集合性的資料,比如在微博應用中,可以將一個使用者所有的關注人存在一個集合中,將其所有粉絲存在一個集合。Redis還為集合提供了求交集、並集、差集等操作,可以非常方便的實現如共同關注、共同喜好、二度好友等功能,對上面的所有集合操作,你還可以使用不同的命令選擇將結果返回給用戶端還是存集到一個新的集合中。 


在redis使用過程遇到的一些問題的總結


tpn(taobao push notification)在使用redis計算訊息未讀數的過程中,遇到了一系列的問題,下面把這個過程整理了一下,也讓大家瞭解這個糾結的過程,供大家以後使用redis或者做類似的功能時進行參考

redis在 tpn裡面主要是用於計算移動千牛(Android、IOS)上的訊息未讀數。tpn的未讀訊息數是基於bizId維度,即同一個bizId(每條訊息的業務id,如果商品id、訂單id等),即使有多條訊息,未讀數也只能算1。因此在接收訊息,計算移動千牛未讀數的過程中,就需要對bizId去重,這個去重的功能就是通過redis來實現的。隨著訊息量的不斷上漲,這個基於redis的去重方案也不斷變化。

一、基於redis Set結構的未讀數計算

前面說到的tpn未讀數計算的最大特點就是基於bizId去重,在java裡面,我們很容易想到利用HashMap或者HashSet來判重,因此最初tpn就是利用redis的Set結構來進行判重。主要利用了redis set結構的這兩個命令:SADD和SCARD

SADD key member [member....]:將一個或多個 member 元素加入到集合 key 當中,已經存在於集合的 member 元素將被忽略。假如 key 不存在,則建立一個只包含 member 元素作成員的集合。 如果member元素不在集合裡面,則返回1;如果member元素已經存在於集合當中,則返回0。

SCARD key:返回集合 key 中元素的數量。

有了這兩個命令,計算未讀數的步驟就是這樣的:


tpn會為使用者保留7天內的訊息,也就是說儲存到redis set結構中的bizId失效時間是7天,同時使用者在查看訊息後,就會把其對應的redis set清空(即如果一個使用者連續幾天都不查看千牛的訊息,那麼其對應的redis set集合裡面就會儲存大量的bizid)。tpn總共有6台redis機器,每台機器上部署5個redis執行個體,每個執行個體的maxmemory設為1G,總共30G的記憶體用於存放訊息bizId。在tpn的早期,由於使用者量不多,訊息量也不大,redis的記憶體完全可以存放7天內的所有訊息bizId,因此這個方案work的很好。但隨著全網大多數活躍賣家開始使用千牛,tpn的訊息量也隨之暴漲,越來越多的訊息bizId給redis帶來了極大的壓力,在訊息高峰期,tpn的日誌裡會有大量的redis timeout異常(tpn使用jedis,配置的timeout是300ms),經過分析,主要是由下面原因造成的:

緩衝失效造成的逾時:前面我們提到了,tpn的每個redis執行個體的maxmemory設定的是1G,因為bizId越來越多,因此很快每個redis 執行個體的記憶體就超過了maxmemory。而redis在處理用戶端請求時,如果發現當前記憶體的使用量已經大於等於maxmemory,就會去失效部分到期的緩衝,直到記憶體使用量量小於maxmemory。很明顯這個失效緩衝釋放記憶體的操作會影響redis的rt。在訊息高峰期,redis執行個體的記憶體使用量量一直再maxmemory附加徘徊,造成redis在應對大量請求的同時,還要不停地失效緩衝釋放記憶體,造成頻繁逾時。

因為bizId太多,而redis記憶體不夠,所以造成redis請求大量逾時,最簡單地辦法就是加機器,部署更多的redis執行個體來儲存越來越多的訊息bizId。初步估計了一下,要完全把7天內的所有訊息bizId都儲存到記憶體中,需要高達上百G的記憶體:交易訊息和商品訊息是tpn最主要的兩類訊息,因為目前全網大多數活躍賣家都使用了千牛,為了去重,tpn需要把全網7天內所有新增的交易id和商品id都儲存到redis記憶體中,換句話來說,也就是要用記憶體來儲存7天內tc和ic新增的所有id。tpn基本不可能申請到這麼多的redis機器,就算有這麼多的redis機器,部署維護成本也是巨大的。就算不用redis,使用tair的rdb,這個陳本仍然是不能接受的。

在移動千牛用戶端,推送沒有正常到達的情況下(比如長串連斷開的時候),是依賴用戶端在發現長串連斷開以後調用messagecount.get介面來擷取到訊息未讀數,然後促使使用者手動擷取最新的訊息。當redis的記憶體使用量量接近極限時,調用redis的sadd、scard命令很容易就timeout了,因此不能正確地計算出訊息未讀數,就會造成使用者不能及時擷取到最新的訊息。

總的來說,redis的記憶體容量不足以容納越來越多的業務訊息bizId,造成大量redis請求逾時,不能正確地計算訊息未讀數。因此需要對上述方案進行最佳化。

二、redis用於訊息去重判斷,tair存放未讀數訊息數的方案

根據上面的分析,當redis記憶體使用量量達到了上限時,很容易發送timeout,同時redis記憶體使用量量會之所以會很快地達到上限,主要是因為不活躍使用者的set結構裡面儲存了大量的bizId。在不能快速增加redis機器的前提下,最簡單地方法就是在夜間重啟redis。重啟redis會帶來一下影響:

所有使用者儲存在set裡面的訊息bizId全部被清空了,就會造成誤判:即對同一個bizId的訊息重複提醒使用者有新訊息。但這個並不會對使用者造成太大的影響:因為活躍使用者會及時地來查看訊息,所以活躍的set結構基本都是空的;而非活躍使用者的redis set結構雖然有很多訊息bizId,但是因為其是不活躍的,就算被清空,很快又會有新的bizId存放進去,但認為是不活躍使用者,對這種情況基本無感知。

因為set結構被清空,所以所有使用者的訊息未讀數也被清空(通過scard命令來計算未讀數)。根據前面的分析,在訊息推送不能正常達到的情況下,正確的未讀數會促使使用者主動地來擷取最新訊息,所以基本不能接受重啟redis的時候,清空使用者的訊息未讀數

因為不能接受隨意清空使用者的訊息未讀數,所以我們不能定期重啟redis來釋放記憶體。但是如果我們把訊息去重和計算未讀數分開,即redis的set結構只用於判斷一條訊息是否是新訊息,是否需要增加未讀數,而把未讀數儲存在其他的地方,如果tair之類的,那我們是不是就可以定期重啟redis了呢?因此我們得到了下面的方案:

繼續是用redis的set結構來判斷一條訊息是不是新訊息,是不是需要增加訊息未讀數

不再使用redis的scard命令計算訊息未讀數,而是採用基於tair的計數器來計算訊息未讀數,即如果通過redis的set結構判斷出是新訊息,則對儲存在tair裡面的未讀數計數器執行incr unReadCountKey 1。


這樣一來,redis就只用於對訊息bizId去重,而不再用於計算訊息未讀數,訊息未讀數單獨儲存在基於tair的計數器當中。因此我們就大膽地定期在夜間重啟redis了。這個方案成功work了一段時間,但過了一段時間後,應用在請求redis的時候又開始是不是拋出大量的timeout exception。分析了一下,問題還是處在redis記憶體上:

雖然可以通過定期重啟redis來釋放記憶體,但是redis記憶體的增加的速度是不可預期的,我們並不能每次都能在記憶體使用量達到極限前重啟redis

有時候雖然redis的整體記憶體使用量量還沒有達到極限,但是如果一個使用者的set結構裡面的bizId太多了,scard命令仍然會timeout

所以這個方案還不是一個最佳的方案,仍然需要通過更好的辦法來降低redis的記憶體使用量量

三、基於redis的bloomfilter的訊息去重方案

從方案一到方案二,我們一直想解決的就是如何用最小的記憶體來判斷一個訊息bizId是不是新的bizId,即一個訊息bizId是不是已經存在了。以最小的記憶體來實現判斷操作,很容易就聯想到bloomfilter。但是在這個情境,我們不能簡單地使用bloomfilter,先來計算一下“最直接”地使用bloomfilter需要多大的記憶體:bloomfilter的所佔用的記憶體由bitSize決定,而根據公式:

bitSize = (int) Math.ceil(maxKey * (Math.log(errorRate) / Math.log(0.6185)));

我們為每個使用者的每個訊息類型建立一個bloomfilter,以500萬使用者,每個使用者訂閱了10個訊息類型,那麼這個用於去重的bloomfilter所佔用的記憶體總量是:

totalMemory(G) = 5000000*10*Math.ceil(maxKey * (Math.log(errorRate) / Math.log(0.6185)))

這個totalMemory的大小就取決於maxKey和errorRate,保證errorRate不變的前提下,bloomfilter 的maxKey越大,bloomfilter所需要的記憶體也就越大。那我們估算一下使用bloomfilter,需要多少記憶體。

以商品訊息和交易小為例,不同的賣家,7天內的訊息數從幾個到幾萬個不等。最小的是7天只有幾條訊息,最多的7天內有7萬多條。就算取個1000的評價值,這5000w個bloomfilter的記憶體消耗也在上百G,這明顯行不通。

但是,tpn的訊息未讀數還有一個業務特點就是,當一個使用者的某個訊息類型的未讀數已經超99了,就不再顯示具體的數字,而是顯示成99+,同時一個使用者的訊息未讀數超過了99,那麼其實他自己對訊息未讀數的敏感性也不高了,即就算有一條訊息不是新訊息,但是仍然給未讀數+1了,使用者也察覺不出來。

因此,在上面的公式裡,我們可以把每個bloomfilter的maxKey設為100,那這樣一來,所佔用的記憶體就是一個十分能夠接受的數字了:設 errorRate=0.0001,maxKey=100,那麼上面的5000w個bloomfilter只需要11G的記憶體,很明顯,這不是一個完全可以接受的記憶體消耗。

這樣一來,我們就得出下面這個基於redis bloomfilter去重方案:

通過redis的setbit命令來實現一個遠端的bloomfilter,具體可以參見這個例子:https://github.com/olylakers/RedisBloomFilter/blob/master/src/main/java/org/olylakers/bloomfilter/BloomFilter.java

每次來一條新訊息,通過redis的bloomfilter來判斷這是不是一條新訊息

如果是,則對tair中的未讀數計數器+1

使用者每次讀取訊息後,則清空對應的bloomfilter


這樣一來,終於我們可以通過能接受的記憶體來實現未讀數的計算,不再要每天擔心redis是不是記憶體不夠用了,應用又頻繁拋timeout exception了

四、詭異的connection broken pipe

在方案三上線以後,我認為這些redis應該會消停了,redis運行一段時間後,的確再也沒用timeout exception了,但是在運行一段時間後,tpn在向redis執行請求時,往redis寫入命令時會報這個異常:

java.net.SocketException: Broken pipe。我們知道,如果一個socket串連已經被遠端給close掉了,但是用戶端沒有察覺,仍然通過這個串連讀寫資料,那麼就會產生Broken pipe異常。因為tpn使用jedis,通過common pool來實現jedis的connection pool,我第一反應就是tpn沒用正確使用jedis的connection pool,沒有銷毀掉broken的redis connection,而是已經重新把歸還給了connection pool,或者是jedis的connection pool有bug,造成了connection泄露,導致ton在往一條已經往一條已經被close的串連寫入資料。但是仔細檢查了一遍tpn的代碼和 jedis connection pool的代碼,發現沒用什麼問題,那就說明有些redis是真的被redis服務端給關閉了,但是jedis 的connection pool沒有發現。

因為用戶端的jedis pool沒有問題,那麼基本上可以確定的確是redis server端關閉了一些串連。首先懷疑的就是tpn的redis 配置出錯了,錯誤地配置了redis.conf裡的timeout 配置項:

首先懷疑的是不是tpn的redis配置不多,造成因此就去查看redis的相關代碼。redis的設定檔redis.config裡面有timeou這個配置項:

# Close the connection after a client is idle for N seconds (0 to disable) timeout 0

檢查了下tpn 6台redis上的所有設定檔,發現都沒有配置這個選擇,但是tpn部署了兩個版本的redis,redis-2.6.14和redis-2.4,結果在redis-2.4裡面,如果沒有配置這個值,redis就會使用預設的值,5*60(s),而redis-2.6.14的預設值是0,即disable timeout,同時又去查看了下jedis common pool的設定,發現minEvictableIdleTimeMillis=1000L * 60L * 60L * 5L(ms),即一個redis串連的空閑時間超過5個小時才會被connection pool給回收。很明顯,就是因為用戶端和服務端的connection idle time設定不一樣,造成了connection被一端關閉了,但是另一端沒有感知,所有造成了broken pipe。解決辦法就是把redid-2.4升級到redid-2.6.14。

五、總結

從方案一到方案三,我最大的感觸就是,在解決問題,最佳化方案的時候,不能僅僅執拗於技術本身,而是要聯絡業務思考。這個redis的bloomfilter的想法我很早就有了,但是我之前一直沒有想到tpn未讀數訊息數只顯示99+這個商務邏輯,而是一直想如何通過降低訊息bizId的長度來儘可能地去節省記憶體,結果越想越複雜,然後就沒有然後了。。。。


相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.