深刻理解跳錶在Redis中的應用

本文首發於:深刻理解跳錶在Redis中的應用
微信公衆號:後端技術指南針
持續輸出乾貨 歡迎關注html

前面寫了一篇關於跳錶基本原理和特性的文章,本次繼續介紹跳錶的機率平衡和工程實現,node

跳錶在Redis、LevelDB、ES中都有應用,git

本文以Redis爲工程藍本,分析跳錶在Redis中的工程實現。github

經過本文你將瞭解到如下內容:redis

  • Redis基本的數據類型和底層數據結構
  • Redis的有序集合的實現方法
  • Redis的跳錶實現細節

1.Redis的數據結構後端

Redis對外共有約五種類型的對象:緩存

  • 字符串(String)
  • 列表(List)
  • 哈希(Hash)
  • 集合(Set)
  • 有序集合(SortedSet)

redis源碼文件src/server.h中對於5種結構的定義:微信

1 /* The actual Redis Object */
2 #define OBJ_STRING 0    /* String object. */
3 #define OBJ_LIST 1      /* List object. */
4 #define OBJ_SET 2       /* Set object. */
5 #define OBJ_ZSET 3      /* Sorted set object. */
6 #define OBJ_HASH 4      /* Hash object. */

Redis對象由redisObject結構體表示,從src/server.h能夠看到該結構的定義以下:網絡

1 typedef struct redisObject {
2     unsigned type:4;
3     unsigned encoding:4;    
4     unsigned lru:LRU_BITS; 
5     int refcount;
6     void *ptr;
7 } robj;

redisObject明確了對象類型、對象編碼方式、過時設置、引用計數、內存指針等,從而完整表示一個key-value鍵值對。數據結構

因爲Redis是基於內存的,Antirez在實現這5種數據類型時在底層建立了多種數據結構,在對象底層選擇採用哪一種結構來實現,

須要根據對象大小以及單個元素大小來進行肯定,從而提升空間使用率和效率。

如圖展現了Redis對外使用的數據類型和底層的數據結構:

有序集合對象的編碼能夠是ziplist或者skiplist,在元素小於128而且元素長度小於64Byte時纔會選擇壓縮列表實現,通常使用skiplist跳錶實現。

2.Redis的ZSet

ZSet結構同時包含一個字典和一個跳躍表,跳躍表按score從小到大保存全部集合元素。

字典保存着從member到score的映射。這兩種結構經過指針共享相同元素的member和score,不會浪費額外內存。

1 typedef struct zset {
2     dict *dict;
3     zskiplist *zsl;
4 } zset;

ZSet中的字典和跳錶佈局:

注:圖片源自網絡

3.ZSet中跳錶的實現細節

  • 隨機層數的實現原理

跳錶是一個機率型的數據結構,元素的插入層數是隨機指定的。Willam Pugh在論文中描述了它的計算過程以下:

  1. 指定節點最大層數 MaxLevel,指定機率 p, 默認層數 lvl 爲1
  2. 生成一個0~1的隨機數r,若r<p,且lvl<MaxLevel ,則lvl ++
  3. 重複第 2 步,直至生成的r >p 爲止,此時的 lvl 就是要插入的層數。

論文中生成隨機層數的僞碼:

論文中關於隨機層數的僞碼

在Redis中對跳錶的實現基本上也是遵循這個思想的,只不過有微小差別,

看下Redis關於跳錶層數的隨機源碼src/z_set.c:

 1 /* Returns a random level for the new skiplist node we are going to create.
 2  * The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL
 3  * (both inclusive), with a powerlaw-alike distribution where higher
 4  * levels are less likely to be returned. */
 5 int zslRandomLevel(void) {
 6     int level = 1;
 7     while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
 8         level += 1;
 9     return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
10 }

其中兩個宏的定義在redis.h中:

1 #define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^32 elements */
2 #define ZSKIPLIST_P 0.25      /* Skiplist P = 1/4 */

能夠看到while中的:

1 (random()&0xFFFF) < (ZSKIPLIST_P*0xFFFF)

第一眼看到這個公式,由於涉及位運算有些詫異,須要研究一下Antirez爲何使用位運算來這麼寫?

最開始的猜想是random()返回的是浮點數[0-1],因而乎在線找了個浮點數轉二進制的工具,輸入0.25看了下結果:

能夠看到0.25的32bit轉換16進制結果爲0x3e800000,若是與0xFFFF作與運算結果是0,好像也符合預期,再試一個0.5:

能夠看到0.5的32bit轉換16進制結果爲0x3f000000,若是與0xFFFF作與運算結果仍是0,不符合預期。

我印象中C語言的math庫好像並無直接random函數,因此就去Redis源碼中找找看,因而下載了3.2版本代碼,也並無找到random()的實現,不過找到了其餘幾個地方的應用:

  • random()在dict.c中的使用:

  • random()在cluster.c中的使用:

看到這裏的取模運算,後知後覺地發現原覺得random()是個[0-1]的浮點數,可是如今看來是uint32纔對,這樣Antirez的式子就好理解了。

因爲ZSKIPLIST_P=0.25,因此至關於0xFFFF右移2位變爲0x3FFF,假設random()比較均勻,

在進行0xFFFF與運算以後高16位清零以後,低16位取值就落在0x0000-0xFFFF之間,這樣while爲真的機率只有1/4,更通常地說爲真的機率爲1/ZSKIPLIST_P。

對於隨機層數的實現並不統一,重要的是隨機數的生成,在LevelDB中對跳錶層數的生成代碼是這樣的:

 1 template <typename Key, typename Value>
 2 int SkipList<Key, Value>::randomLevel() {
 3 
 4   static const unsigned int kBranching = 4;
 5   int height = 1;
 6   while (height < kMaxLevel && ((::Next(rnd_) % kBranching) == 0)) {
 7     height++;
 8   }
 9   assert(height > 0);
10   assert(height <= kMaxLevel);
11   return height;
12 }
13 
14 uint32_t Next( uint32_t& seed) {
15   seed = seed & 0x7fffffffu;
16 
17   if (seed == 0 || seed == 2147483647L) { 
18     seed = 1;
19   }
20   static const uint32_t M = 2147483647L;
21   static const uint64_t A = 16807;
22   uint64_t product = seed * A;
23   seed = static_cast<uint32_t>((product >> 31) + (product & M));
24   if (seed > M) {
25     seed -= M;
26   }
27   return seed;
28 }

能夠看到leveldb使用隨機數與kBranching取模,若是值爲0就增長一層,這樣雖然沒有使用浮點數,可是也實現了機率平衡。

  • 跳錶結點的平均層數

咱們很容易看出,產生越高的節點層數出現機率越低,不管如何層數老是知足冪次定律越大的數出現的機率越小。

若是某件事的發生頻率和它的某個屬性成冪關係,那麼這個頻率就能夠稱之爲符合冪次定律。冪次定律的表現是少數幾個事件的發生頻率佔了整個發生頻率的大部分, 而其他的大多數事件只佔整個發生頻率的一個小部分。冪次定律

 

 

冪次定律應用到跳錶的隨機層數來講就是大部分的節點層數都是黃色部分,只有少數是綠色部分,而且機率很低。

定量的分析以下:

  • 節點層數至少爲1,大於1的節點層數知足一個機率分佈。
  • 節點層數剛好等於1的機率爲p^0(1-p)。
  • 節點層數剛好等於2的機率爲p^1(1-p)。
  • 節點層數剛好等於3的機率爲p^2(1-p)。
  • 節點層數剛好等於4的機率爲p^3(1-p)。
  • 依次遞推節點層數剛好等於K的機率爲p^(k-1)(1-p)

所以若是咱們要求節點的平均層數,那麼也就轉換成了求機率分佈的指望問題了,靈魂畫手大白再次上線:

表中P爲機率,V爲對應取值,給出了全部取值和機率的可能,所以就能夠求這個機率分佈的指望了。

方括號裏面的式子其實就是高一年級學的等比數列,經常使用技巧錯位相減求和,從中能夠看到結點層數的指望值與1-p成反比。

對於Redis而言,當p=0.25時結點層數的指望是1.33。

小結:在Redis源碼中有詳盡的關於插入和刪除調整跳錶的過程,本文就再也不展開了,代碼並不算難懂,都是純C寫的沒有那麼多炫技的特效,放心大膽讀起來。

4.參考資料

5.推薦閱讀

白話布隆過濾器BloomFilter
理解緩存系統的三個問題
幾種高性能網絡模型
二叉樹及其四大遍歷
理解Redis單線程運行模式
Linux中各類鎖及其基本原理
理解Redis持久化
深刻理解IO複用之epoll
深刻理解跳躍鏈表[一]
理解堆和堆排序
理解堆和優先隊列

6.關於本公衆號

開號不久做者力爭持續輸出原創乾貨,若是文章有幫助到你,

但願朋友們多多轉發和分享,做者會更加有動力,推出更好的文章,共同進步。

微信公衆號:後端技術指南針

相關文章
相關標籤/搜索