使用Go與redis構建有趣的應用,goredis構建

來源:互聯網
上載者:User

使用Go與redis構建有趣的應用,goredis構建

本文分為4個部分,第一部分是介紹redis的功能、應用以及資料結構是怎樣的。第二部分是開始使用redis構建鎖。第三個是使用redis構建線上使用者統計器。第四個是使用redis構建自動補完程式。

首先介紹一下redis的特點,redis具有多種不同的資料結構可用,包括字串、散列、列表、有序集合、位元影像(bitmap)、Hyperloglog、地理座標(GEO)等。它還有記憶體儲存和基於多路服用的事件響應系統,確保了命令請求的執行速度。第三個是它具有豐富的附加功能,如事務、lua指令碼,鍵到期機制(定期讓鍵自動刪除)、鍵淘汰機制,多種持久化方式(AOF、RDB、RDB+AOF混合)等。另外它還有強大的多機功能支援,前幾年redis還是2點多版本的時候,人們經常會說是它是玩具資料庫,因為缺少多機的支援,最近這幾個版本已經慢慢加入了多機功能,比如說把主從提供高可用。最新的4.0版本它還提供了一個模組的擴充系統,把redis當做一個記憶體的平台,它提供了一些介面,使用者可以通過介面去使用redis的功能,在原來的基礎上提供更多新的功能,讓你對它進行開發。


圖 1

redis有多種不同的資料結構。首先是字串,有點類似C語言的字串,但是在C語言的基礎上增加了一些功能,譬如說長度記錄,就是擷取一個C字串的長度是線性,redis增加了一個字串長度記錄功能,讓客戶可以以常數複雜度去擷取長度,不需要整個遍曆一遍,直接存取就可以。還有一些二進位安全,可以直接在資料庫裡面儲存位元據,不一定是字串,比如說一些壓縮檔都可以。然後它還有記憶體預分配系統,當你要頻繁的修複字串,它可以通過記憶體預分配策略減少記憶體重分配次數,提高效能。

圖 2

第二個散列,散列和GO語言裡面的map很像,一個鍵映射到另外一個值,每個鍵都各不同,擷取單個鍵的複雜度為常數,如果有需要可以一次把索引值對全部擷取出來,但是效能會比較差。


圖 3


還有列表,列表有點像GO的序列,底層是用雙端鏈表來實現的的,所以你可以對它的頭或者尾進行操作,都是常數複雜度,但是如果你要增加或者截斷這個列表或者是對它進行遍曆的話就比較慢。它跟之前的雜湊表不同,它允許有重複的元素。


圖 4


接下來是集合,集合會以無序的方式儲存多個各不相同的元素,針對單個元素的複雜度的操作都為常數,速度很快,可以把它跟其他集合,做一些交集、並集計算。


圖 5


有序集合比較少見,集合裡每個元素都由一個分值和成員組成,每個成員是按照分值的大小有序的排序。使用者可以按照這個分值有序的擷取,譬如說你擷取分值最少的元素或者擷取分值最大的兩個元素,或者你直接按照成員擷取前兩個和後兩個都可以。有序集合的底層是使用跳躍表來實現的,所以擷取單個元素會有複雜度。


圖 6

位元影像是由一連串二進位位組成的數組,數組有索引,你可以根據索引對二進位位進行操作。可單獨設定指定的位,可擷取指定範圍的多個位又或者對它們計數。


圖 7


Hyperloglog比較複雜,它有一個演算法,對一系列二進位位進行計算,可以算出計數值,一個特點是就算你往Hyperloglog裡面添加上億個元素,它對上億個元素進行計數,佔用的記憶體都是固定的12GB,後面我們可以看到如何使用Hyperloglog減少對記憶體的使用。


圖 8

最後一個地理位置,你給一個經緯度和位置名就可以儲存到redis裡面,或者對它進行範圍計算,或者說計算出在這個地方100公裡或者5公裡之內有什麼東西,寫LBS應用的朋友應該對這個會感興趣。


圖 9


這個就是redis資料庫典型的樣子,redis是索引值對資料庫,它的鍵都是字串,它的值可以是我們剛才介紹過的資料結構的任意一種。


圖 10


我們可以通過命令去操作這些索引值對,就好像我們用SQL語言操作關聯式資料庫一樣。首先要有個命令,命令你要執行什麼工作,第二個就有一個鍵,鍵就是要有一個你要操作的對象,然後你要有任意多個參數,因為非常簡單,所以有時候經常都是1、2個參數就可以做到很多東西,有的比較複雜的命令還會有選項,選項還會有值。


圖 11


這裡是一些命令的樣本,比如說ping,如果一切正常伺服器就會給你返回一個ping,就是以一個鍵的形式進行排序,第三個是對一些雜湊散列進行設定,設定多個索引值對,最後一個RPUSH可以把後面的元素都歸到一個列表裡面。

回到我們今天的主題GO,要在GO使用redis,我們需要通過用戶端,用戶端官方推薦兩個radix和redigo,今天我們會使用的是radix和我們的老朋友GOget。這裡是串連用戶端的範例程式碼,用dial方法去串連資料庫,通過cmd方法去執行ping命令並擷取回複,最後我們用str方法過將回複轉換成字串,然後列印出來。


圖 12

來到我們第一個應用執行個體——鎖。鎖是一種同步機制,它可以保證一項資源在任何時候只能被一個進程使用,如果有其它進程想使用這個資源就必須等待,直到正在使用資源的進程放棄使用權為止。一般一個鎖都有擷取和釋放這2個操作,擷取操作就是擷取資源的使用權,在任何時候,擷取這個資源的安全,只能有一個進程去獲得鎖,當一個鎖被擷取的時候其他嘗試擷取鎖的其它進程都會失敗。還有是一個釋放操作,獲得的鎖進程釋放後,其他人就可以再次使用資源。

方法一是使用字串結構去實現鎖,具體的方法是把一個字串用做鎖,如果這個字串鍵有值,那麼就說明鎖被擷取了,如果鍵沒有值的話就是沒有被擷取。

下面是需要用到的一些命令:

  • GET key:擷取字元鍵 key 的值,如果該鍵尚未有值,那麼返回 個空值(nil)                                    

  • SET key value: 將字元 鍵 key 的值設定為 value ,如果鍵已經有值,那麼預設使 新值去覆蓋舊值                               

  • DEL key:刪除給定的鍵 key 

我們首先使用GET方法,擷取鍵的值,並把這個值轉換為字串,然後用if方法去檢查有沒有值,如果沒有值的話就返回一個空的字串,確認沒有值就調用set方法進行設定,就是給它加鎖。這裡展示的代碼就是為了節約時間,我們就把錯誤的回複處理掉了。


圖 13


這個方法雖然能夠成功,但是它有一個競爭條件,在執行get命令之後和執行set方法之前,這裡有個中間時段,其他用戶端就可能搶先對鍵進行了設定,這時候就會產生一個競爭條件,假設現在有兩個用戶端,他們一起去執行剛才的acquire方法,他們同時調用get,都獲得了那個鍵沒有值的共識,用戶端2因為執行的快一點就執行了set方法進行了加鎖,然後成功獲得了鎖,這個時候用戶端1姍姍來遲,因為前面是同時的執行get方法,大家都以為鍵沒有加鎖,所以用戶端1就會繼續執行這個set方法,對鎖進行加鎖,這時候就會出現兩個用戶端同時出現成功擷取鎖的情況,就會出錯。


為瞭解決這個問題我們需要使用redis的事務特性去保證加鎖操作的安全性。這裡是一個redis的非事務命令的示範,一般來說很多資料庫都一樣的,如果設定一般命令的話會一個接一個,事務也是命令的一種特殊的命令,一個事務裡面會包含多種命令,這樣就可以保證安全性。

現在看一下安全的鎖需要用到什麼命令?第一個是WATCH,監視給定的鍵,如果被監視的鍵在事務執行之前已經被其它用戶端搶先修改的話,執行命令的用戶端提交的命令就會被拒絕。第二個是MULTI命令,它會開啟一個事務,在執行這個命令之後,用戶端發送的所有操作命令都會被進入到事務隊列裡等待。最後一個EXEC,是嘗試執行事務,成功時將返回一個由多個命令回複組成的隊列給用戶端,失敗則返回nil。


圖 14


這個是實現代碼,首先用WATCH命令去監測一個鍵,然後再調用get方法,判斷這個鍵沒有值的話就調用MULTI開始一個事務,然後把EXEC放到隊列裡面,這是最關鍵的一步,如果事務成功執行的話,這個回複就不會是空的,如果返回空,就說明lock_key就已經被其它用戶端搶先修改了,然後我們就根據這個事物是否執行成功來判斷加鎖是否成功。

但是使用事務也會帶來代價,它會使代碼複雜化,我們的加鎖程式本身也不到5行代碼,現在加到10行,代碼的量加到了一倍,雖然保證了安全性,但是代碼複雜了。考慮到這種情況,redis提供了一種新的功能——帶NX選項的SET命令。當我們使用帶NX選項的SET命令時,只有在鍵key不存在的情況下才會對它進行設定,如果鍵已經有值,就會放棄對它進行設定代碼,並返回nil表示設定失敗。NX選項的作用就相當於把剛才這一段會引起競爭條件的代碼放到伺服器裡面執行了,這樣就保證了執行操作的安全性。


圖 15


這個就是我們的NX選項,很簡單,比我們最初的那一版加鎖還要簡單,就直接一個SET命令,但是有一個NX選項,在lock_key沒有值的情況下對它進行設定,通過這個檢查回複是否回空的情況下就知道加鎖成功了。


圖 16


看一下第二個應用樣本——線上使用者統計器,比如可以統計網站有多少使用者,有多少人看直播等。實現這種功能的第一個方法就是使用集合,當一個使用者上線的時候我們就把使用者名稱添加在線上使用者集合裡面。


圖 17


需要用到的命令,SADD set element [element ...],可以將給定的元素添加到集合當中。SCARD set 可以擷取集合的基數 ,即集合包含的元素數量,也即當前有多少使用者。 SISMEMBER set element 可以檢查給定的元素是否存在集合裡面,應用起來比如可以用這個命令去檢查某個使用者是否線上等。


圖 18


實現代碼也很簡單,當一個使用者線上的時候我們就接受使用者名稱,然後調入SADD命令,如果要統計多少使用者線上的話,就擷取集合有多少個元素的基數,最後我們要檢查一個使用者是否線上的話,會給定元素存在的時候返回1,我們就看傳回值數給1就可能知道使用者是否線上。

使用集合統計線上使用者有一個很嚴重的問題,集合的體積將隨著元素的增加而增加,假設每個使用者平均名字是10位元組,那麼擁有100萬使用者的網站每天需要使用10MB記憶體去儲存,擁有一千萬使用者的網站每天需要使用100MB  記憶體去儲存。如果把這些資訊儲存1年,擁有100萬使用者的網站每年需要為此使用3.65GB記憶體,擁有1000萬使用者的網站每年需要為此付出36.5G記憶體。統計如此小的一個功能卻要花費如何的的記憶體將會非常不值得,這還不包括一些額外的開銷。

方法二是使用位元影像,為每一個使用者建立一個ID,當一個使用者上線後,就用他的ID作為索引,假設現在有一個使用者peter,我們給他映射一個ID 10086,然後根據這個ID把這個位元影像裡面索引為10086的值設定為1,值為1的使用者就是線上,值為0的就是不線上。

這裡同樣需要用到3個命令:

  • SETBIT bitmap index value :將位元影像指定索引上的二進位位設定為給定的值

  • GETBIT bitmap index  :擷取位元影像指定索引上的二進位位

  • BITCOUNT bitmap :統計位元影像中值為 1 的二進位位的數量

實現代碼如下, setbit接受使用者的ID,把二進位位設為1。統計值為1的二進位位元量,如果要檢查使用者是否線上,就先根據使用者的ID然後檢查位元影像裡面的二進位位的值是否為1,1就是線上,0就是不線上。

圖 19


跟剛才的集合相比,雖然位元影像的體積仍然會隨著使用者數量的增多而變大,但因為記錄每個使用者所需的記憶體數量從原來的平均10位元組變成了1位,所以將節約大量的記憶體,把幾十G的佔用降為了幾百MB。

我們要繼續進行最佳化就得到了方法三——使用Hyperloglog。當一個使用者上線時,我們就使用Hyperloglog對他進行計數。假設現在有一個使用者jack,我們通過Hyperloglog演算法對他進行計數,然後把這個計數反映到Hyperloglog裡面,如果這個元素之前沒有被Hyperloglog計數過的話,你新添加在Hyperloglog裡面就會對自己的計數進行加1。如果jack已經存在,它的計數值就不會加1。

Hyperloglog需要用到兩個方法,第一個是PFADD hll element [element ...] ,對給定的元素進行計數。第二個PFCOUNT hll 是擷取Hyperloglog的近似基數,也即是基數的估算值。使用Hyperloglog有一個缺陷,因為Hyperloglog是一個機率演算法,它只能給出一個估算的值。比如你有1000個使用者進行計數,可能只會返回你970或者是980個,它沒有辦法給出一個準確的計數值,只能給出一個近似,好處就是無論我們對多少使用者進行計數,單個Hyperloglog都只佔12KB,下面是實現代碼。

圖 20

三種實現的內容消耗對比如所示:


圖 21

最後一個樣本應用——自動補全。我們在很多無論是案頭應用,譬如說網頁瀏覽器、搜尋引擎、twitter,當你輸入一些東西的時候,譬如說你在瀏覽器裡面輸入go然後會自動幫你補全go相關的一些命令。我們分析一下自動補全的原理,當我們輸入的時候會返回一個補全的補全的結果,這個結果還帶有一個權重值,排在越前面的值權重值越高。為了實現這個自動補全程式我們需要構建一個權重表,對redis來說儲存這樣的權重最合適的就是有序集合。


圖 22

那麼該如何去構建一個權重表呢?構建權重表有很多種方式,比如你可以用很多複雜的演算法計算出每個候選結構的權重,也有一些簡單的方法,比如最簡單的是你根據使用者的輸入,譬如使用者輸入gmail,然後你就對使用者進行計數,如果輸入google又進行一次計數,然後根據客戶輸入的計數構建一個權重表。假設我們有一個權重表,gopher的權重值為277,如果連續輸入3次gopher,就會對gopher的權重值進行加三操作,提高到280。所以我們構建了一個權重表,但是我們無法對它進行補全。如果我們要補全的話,你輸入G的時候會聯想GOPHER,輸入go的時候也會聯想gopher, 我們要枚舉出這樣的一個排列,對每個排列都建一個權重表,譬如我們要在權重表裡面添加一個gopher項,對於每一個排列都要為gopher添加一個權重項。每當使用者輸入一個的gopher時候,對每個排列對應的權重表我們在權重表裡面找到gopher然後再添加過去。


圖 23


補全的過程,當我們輸入G的時候就根據G尋找對應的權重表,可以看到gopher排到第三位,它的分值比google和gmail要低,這個表裡面全部是G開頭的候選結果。當我們輸入go的時候,就開始聯想,它會繼續尋找一個go開頭的成員。可以看到根據我們聯想的結果,gopher不斷的在上升,就出現在使用者上面,當我們輸入GOP的時候,會根據GOP找到我們的gopher。


圖 24


實現我們的自動補全需要用到兩個命令,第一個ZINCRBY zset increment member是對給定成員的分值執行自增操作;第二個ZREVRANGE zset start end [WITHSCORES]是按照分值從大到小的順序,從有序集合裡面擷取指定索引範圍內的成員。因為我們的權重是從大到小排列的,我們就先擷取權重最高的值就會顯示出來,實現代碼如下所示。


圖 25

我們來總結一下:首先GO和redis都是很簡單強大的工具,組合起來可以輕鬆解決很多過去 常難以實現或者需要很多代碼才能實現的特性,比如自動補全,如果你不是用GO和redis一起做,那今天的PPT數量頁數可能會增加三倍。第二點是在構建程式的時候一定要確保程式的安全性和正確性,雖然保證這兩點常常會使得程式變得複雜,但有時候本身也有魚與熊掌兼得的方案。譬如說前面說的線上統計,就可以有很多方法去實現,但是只有對它足夠熟悉,才能找到最優方法。最後一點是不同方法實現的效率和功能通常也會有所不同,我們需要根據自身的情況進行選擇,不要盲目相信所謂的最優解。

相關文章

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.