• redis 源码分析 跳表实现


    redis中为什么要使用跳表的数据结构?

    reids 的zset有序集合的实现,需要兼顾方便的查找,插入和删除,如果使用链表,查找效率位O(N),这对redis不可接受,使用红黑树,则实现比较麻烦,所以redis使用了一种新的结构,跳表。

    上图是一个理想跳表

    如果你现在要查找21,你只需要从最高的第2层开始,找到18,跳到第1层,找到24,跳到第0层,找到21,如果数据量大,这将达到了二分查找的效果。

    从上面理想跳表,我们可以思考下如果定义一个跳表的数据结构,应该具备什么?

    如果是一个跳表的节点:比如1这个节点

    跳表的粗略的数据结构

    1. struct MySkipListLevel
    2. {
    3. TMySkipListNode* m_pNext; // 下一个节点
    4. int m_nSpan; // 当前层跳离下一节点的间隔
    5. };
    6. struct TMySkipListNode
    7. {
    8. void* m_pData; // 数据
    9. MySkipListLevel m_L[]; // 每一层
    10. TMySkipListNode* m_pPrev; // 查找到24,要往回走
    11. };
    12. struct TMySkipList
    13. {
    14. TMySkipListNode* m_pHeadNode;
    15. int m_nLeves;
    16. };

    reids跳表

    1. typedef struct zskiplistNode {
    2. sds ele;
    3. double score;
    4. struct zskiplistNode* backward; // 后退指针, 只能指向当前节点最底层的前一个节点,
    5. struct zskiplistLevel {
    6. struct zskiplistNode *forward; // 指向后一个节点
    7. unsigned long span; // forward指向的节点与本节点之间的元素个数
    8. } level[];
    9. } zskiplistNode;
    10. typedef struct zskiplist {
    11. struct zskiplistNode *header, *tail;
    12. unsigned long length;
    13. int level;
    14. } zskiplist;

    其中,多了尾部指针tail,length,一会看看有什么用,数据部分的差异忽略不计。

    redis实现的跳跃表

    1、创建跳跃表节点

    1. zskiplistNode *zslCreateNode(int level, double score, sds ele) {
    2. zskiplistNode *zn =
    3. zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
    4. zn->score = score;
    5. zn->ele = ele;
    6. return zn;
    7. }

    2、创建跳跃表并创建头节点

     新跳跃表的默认层高是多少?

    1. int zslRandomLevel(void) {
    2. int level = 1;
    3. while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
    4. level += 1;
    5. return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
    6. }

    从代码可以看出,假设小于(ZSKIPLIST_P * 0xFFFF) 的概率为p

    大于(ZSKIPLIST_P * 0xFFFF)的概率为1-p

    则层数为n,对应的(p^(n-1)) * (1-p)

    其数学期望为:E=1/(1-p),当p为0.25时,则对应的E = 1.33

    创建跳跃表,头节点不存储信息,层数64,无数据。

    1. zskiplist *zslCreate(void) {
    2. int j;
    3. zskiplist *zsl;
    4. zsl = zmalloc(sizeof(*zsl));
    5. zsl->level = 1;
    6. zsl->length = 0;
    7. zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
    8. for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
    9. zsl->header->level[j].forward = NULL;
    10. zsl->header->level[j].span = 0;
    11. }
    12. zsl->header->backward = NULL;
    13. zsl->tail = NULL;
    14. return zsl;
    15. }

    3、插入节点

    1. zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    2. zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    3. unsigned int rank[ZSKIPLIST_MAXLEVEL];
    4. int i, level;
    5. serverAssert(!isnan(score));
    6. x = zsl->header;
    7. // 从最高层往底层访问
    8. // rank[i]是第i层对应的可插入新节点的前一个节点的经过的span(也就是头节点到插入节点的前一个节点的间隔),最高层为0
    9. // update[i] 是第i层可以插入节点的前一个节点
    10. for (i = zsl->level-1; i >= 0; i--) {
    11. /* store rank that is crossed to reach the insert position */
    12. rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
    13. while (x->level[i].forward &&
    14. (x->level[i].forward->score < score ||
    15. (x->level[i].forward->score == score &&
    16. sdscmp(x->level[i].forward->ele,ele) < 0)))
    17. {
    18. rank[i] += x->level[i].span;
    19. x = x->level[i].forward;
    20. }
    21. update[i] = x;
    22. }
    23. /* we assume the element is not already inside, since we allow duplicated
    24. * scores, reinserting the same element should never happen since the
    25. * caller of zslInsert() should test in the hash table if the element is
    26. * already inside or not. */
    27. // 假定不会重复插入,新建一个高度为level的节点,如果比原来的调表的高度高,需要更新对应的rank和update
    28. level = zslRandomLevel();
    29. if (level > zsl->level) {
    30. for (i = zsl->level; i < level; i++) {
    31. rank[i] = 0;
    32. update[i] = zsl->header;
    33. update[i]->level[i].span = zsl->length;
    34. }
    35. zsl->level = level;
    36. }
    37. // 创建节点,更新节点的前向节点和后向节点信息
    38. x = zslCreateNode(level,score,ele);
    39. for (i = 0; i < level; i++) {
    40. x->level[i].forward = update[i]->level[i].forward;
    41. update[i]->level[i].forward = x;
    42. /* update span covered by update[i] as x is inserted here */
    43. x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
    44. update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    45. }
    46. /* increment span for untouched levels */
    47. // 插入新的节点后,对应的高出来的那几层需要更新span
    48. for (i = level; i < zsl->level; i++) {
    49. update[i]->level[i].span++;
    50. }
    51. // 设置后向节点上数值
    52. x->backward = (update[0] == zsl->header) ? NULL : update[0];
    53. if (x->level[0].forward)
    54. x->level[0].forward->backward = x;
    55. else
    56. zsl->tail = x;
    57. // 更新跳表长度
    58. zsl->length++;
    59. return x;
    60. }

    插入跳表的节点,1、关键在于如果低于层数,则高于当前跳表层数的要赋值对应的层的数据。2、维持好插入节点的前后节点。3、维持好插入节点后整个跳表的其他节点,长度,层高的影响。

    4、删除节点:

    1. void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
    2. int i;
    3. // 下一个节点就是被删除节点,则加上被删除节点的间距-1,不是被删除节点,直接-1
    4. // 这里假定,被删除的节点一定存在跳表中
    5. for (i = 0; i < zsl->level; i++) {
    6. if (update[i]->level[i].forward == x) {
    7. update[i]->level[i].span += x->level[i].span - 1;
    8. update[i]->level[i].forward = x->level[i].forward;
    9. } else {
    10. update[i]->level[i].span -= 1;
    11. }
    12. }
    13. // 如果被删除节点有下一个节点,则要将下一个节点前向指针赋值为被删除节点的上一个节点
    14. if (x->level[0].forward) {
    15. x->level[0].forward->backward = x->backward;
    16. } else {
    17. zsl->tail = x->backward; // 更新尾节点
    18. }
    19. // 跳表高度大于1,最高层没有下一个节点,层数减1,调整高度
    20. while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
    21. zsl->level--;
    22. // 更新长度
    23. zsl->length--;
    24. }

    5、删除跳跃表:

    1. void zslFree(zskiplist *zsl) {
    2. zskiplistNode *node = zsl->header->level[0].forward, *next;
    3. zfree(zsl->header);
    4. while(node) {
    5. next = node->level[0].forward;
    6. zslFreeNode(node);
    7. node = next;
    8. }
    9. zfree(zsl);
    10. }

    从最底层遍历,然后往后遍历,释放他的所有的节点。

    跳表redis的应用:

    redis中,zset有两种实现,跳表和压缩列表

    当元素个数小于128时,使用压缩列表

    元素个数大于128时,使用跳表

    所以zset当满足条件时,会转换为跳表实现,而且不会转回来。

  • 相关阅读:
    Kubernetes:云原生时代的核心引擎
    springboot点餐微信小程序毕业设计源码221144
    编译原理网课笔记——第一章
    激光雷达物体检测(二):点视图检测算法
    【C++】类和对象(上)
    四旋翼无人机的飞行原理--【其利天下分享】
    使用FRP进行内网穿透的最佳实践
    学点设计模式,盘点Spring等源码中与设计模式的那些事之结构型模型
    第一个 Angular 项目 - 添加路由
    Spring Boot JPA EntityManager实体管理器示例
  • 原文地址:https://blog.csdn.net/hahackeris/article/details/125592356