Redis的实现五:二叉堆的数据结构和TTL、c,c++的实现

简介: 这篇文章详细探讨了二叉堆的数据结构及其在C和C++中的实现,特别强调了二叉堆在Redis中实现TTL(生存时间)功能的重要性,并通过代码示例展示了如何在Redis中使用二叉堆来管理键的过期时间。
    **概述:**Redis的主要用途是作为缓存服务器,管理缓存大小的一种方法是通过显式设置ttl(生存 时间)。ttl可以使用计时器来实现。不幸的是,上一章的计时器是固定值(使用链表);因 此,需要一种排序数据结构来实现任意可变的超时;而堆数据结构是一种流行的选择。 与我们之前使用的AVL树相比,堆数据结构的优势在于占用的空间更少。

   快速回顾一下堆数据结构:

   1. 堆是一个二叉树,被打包成一个数组;而树的布局是固定的。父子关系是隐式的, 指针不包含在堆元素中。

  2. 这棵树上唯一的约束是父母不能比他们的孩子大。

  3. 元素的值可以更新。如果值改变了: •它的值比之前大:它可能比它的子元素大,如果是这样,就把它和最小的子 元素交换,这样亲子约束就再次得到满足。现在,其中一个孩子比以前大, 继续这个过程,直到达到一个休假。 •它的值变小了:同样地,与它的父节点交换,直到到达根节点。   

 4. 新元素以叶子的形式添加到数组的末尾。如上所述保持约束。

 5. 当从堆中删除一个元素时,将其替换为数组中的最后一个元素,然后维护约束, 就好像它的值被更新了一样。

二叉堆的完整例子:

#include <stdio.h>

#define MAX_HEAP_SIZE 100

// 定义最小堆结构
struct MinHeap {
    int arr[MAX_HEAP_SIZE];
    int size;
};

// 函数声明
void initHeap(struct MinHeap* heap);
void insertElement(struct MinHeap* heap, int value);
void deleteMin(struct MinHeap* heap);
void printHeap(struct MinHeap* heap);

int main() {
    struct MinHeap heap;
    initHeap(&heap);

    // 添加元素
    insertElement(&heap, 5);
    insertElement(&heap, 3);
    insertElement(&heap, 8);
    insertElement(&heap, 1);

    // 打印堆
    printf("堆的元素:");
    printHeap(&heap);

    // 删除最小元素
    deleteMin(&heap);

    // 打印堆
    printf("删除最小元素后的堆:");
    printHeap(&heap);

    return 0;
}

// 初始化堆
void initHeap(struct MinHeap* heap) {
    heap->size = 0;
}

// 向堆中插入元素
void insertElement(struct MinHeap* heap, int value) {
    if (heap->size == MAX_HEAP_SIZE) {
        printf("堆已满\n");
        return;
    }

    // 将新元素添加到末尾
    heap->arr[heap->size] = value;
    heap->size++;

    // 上移操作,维护堆的性质
    int currentIndex = heap->size - 1;
    while (currentIndex > 0) {
        int parentIndex = (currentIndex - 1) / 2;
        if (heap->arr[currentIndex] < heap->arr[parentIndex]) {
            // 交换元素,保持堆的性质
            int temp = heap->arr[currentIndex];
            heap->arr[currentIndex] = heap->arr[parentIndex];
            heap->arr[parentIndex] = temp;
        } else {
            break; // 堆的性质已满足
        }
        currentIndex = parentIndex;
    }
}

// 删除堆中的最小元素
void deleteMin(struct MinHeap* heap) {
    if (heap->size == 0) {
        printf("堆为空\n");
        return;
    }

    // 将最后一个元素替换到根节点
    heap->size--;
    heap->arr[0] = heap->arr[heap->size];

    // 下移操作,维护堆的性质
    int currentIndex = 0;
    while (1) {
        int leftChild = 2 * currentIndex + 1;
        int rightChild = 2 * currentIndex + 2;
        int minIndex = currentIndex;

        if (leftChild < heap->size && heap->arr[leftChild] < heap->arr[minIndex]) {
            minIndex = leftChild;
        }
        if (rightChild < heap->size && heap->arr[rightChild] < heap->arr[minIndex]) {
            minIndex = rightChild;
        }

        if (minIndex != currentIndex) {
            // 交换元素,保持堆的性质
            int temp = heap->arr[currentIndex];
            heap->arr[currentIndex] = heap->arr[minIndex];
            heap->arr[minIndex] = temp;
            currentIndex = minIndex;
        } else {
            break; // 堆的性质已满足
        }
    }
}

// 打印堆的元素
void printHeap(struct MinHeap* heap) {
    for (int i = 0; i < heap->size; i++) {
        printf(" %d", heap->arr[i]);
    }
    printf("\n");
}
struct HeapItem {
   uint64_t val = 0;
   size_t *ref = NULL;
};
// the structure for the key
struct Entry {
   struct HNode node;
   std::string key;
   std::string val;
   uint32_t type = 0;
   ZSet *zset = NULL;
   // for TTLs
   size_t heap_idx = -1;
};
 堆用于对时间戳排序,Entry与时间戳相互链接。heap\_idx是对应HeapItem的索引, ref指向Entry。我们再次使用了侵入式数据结构;ref指针指向heap\_idx字段。

父子关系是固定的:

static size_t heap_parent(size_t i) {
    return (i + 1) / 2 - 1;
}
static size_t heap_left(size_t i) {
   return i * 2 + 1;
}
static size_t heap_right(size_t i) {
   return i * 2 + 2;
}

当孩子比父母小时,与父母交换。注意heap_idx是在交换时通过ref指针更新的。

static void heap_up(HeapItem *a, size_t pos) {
    HeapItem t = a[pos];
  while (pos > 0 && a[heap_parent(pos)].val > t.val) {
    // swap with the parent
    a[pos] = a[heap_parent(pos)];
    *a[pos].ref = pos;
    pos = heap_parent(pos);
}
    a[pos] = t;
    *a[pos].ref = pos;
}

和最小的孩子交换也是类似的。

static void heap_down(HeapItem *a, size_t pos, size_t len) {
    HeapItem t = a[pos];
while (true) {
   // find the smallest one among the parent and their kids
   size_t l = heap_left(pos);
   size_t r = heap_right(pos);
   size_t min_pos = -1;
   size_t min_val = t.val;
 if (l < len && a[l].val < min_val) {
   min_pos = l;
   min_val = a[l].val;
}
if (r < len && a[r].val < min_val) {
  min_pos = r;
}
if (min_pos == (size_t)-1) {
  break;
}
   // swap with the kid
   a[pos] = a[min_pos];
   *a[pos].ref = pos;
   pos = min_pos;
}
   a[pos] = t;
  *a[pos].ref = pos;
}

heap_update是用于更新位置的堆函数。它用于更新、插入和删除。

void heap_update(HeapItem *a, size_t pos, size_t len) {
  if (pos > 0 && a[heap_parent(pos)].val > a[pos].val) {
    heap_up(a, pos);
} else {
   heap_down(a, pos, len);
  }
}

将堆添加到我们的服务器:

// global variables
static struct {
   HMap db;
   // a map of all client connections, keyed by fd
   std::vector<Conn *> fd2conn;
   // timers for idle connections
   DList idle_list;
   // timers for TTLs
   std::vector<HeapItem> heap;
} g_data;

更新、添加和删除一个定时器到堆。只需在更新数组元素后调用heap_update即可。

// set or remove the TTL
static void entry_set_ttl(Entry *ent, int64_t ttl_ms) {
  if (ttl_ms < 0 && ent->heap_idx != (size_t)-1) {
   // erase an item from the heap
   // by replacing it with the last item in the array.
   size_t pos = ent->heap_idx;
   g_data.heap[pos] = g_data.heap.back();
   g_data.heap.pop_back();
  if (pos < g_data.heap.size()) {
   heap_update(g_data.heap.data(), pos, g_data.heap.size());
}
  ent->heap_idx = -1;
}  else if (ttl_ms >= 0) {
  size_t pos = ent->heap_idx;
  if (pos == (size_t)-1) {
  // add an new item to the heap
  HeapItem item;
  item.ref = &ent->heap_idx;
  g_data.heap.push_back(item);
   pos = g_data.heap.size() - 1;
}
  g_data.heap[pos].val = get_monotonic_usec() + (uint64_t)ttl_ms * 1000;
  heap_update(g_data.heap.data(), pos, g_data.heap.size());
 }
}

后面懒得写了。

目录
相关文章
|
20天前
|
存储 消息中间件 NoSQL
Redis数据结构:List类型全面解析
Redis数据结构——List类型全面解析:存储多个有序的字符串,列表中每个字符串成为元素 Eelement,最多可以存储 2^32-1 个元素。可对列表两端插入(push)和弹出(pop)、获取指定范围的元素列表等,常见命令。 底层数据结构:3.2版本之前,底层采用**压缩链表ZipList**和**双向链表LinkedList**;3.2版本之后,底层数据结构为**快速链表QuickList** 列表是一种比较灵活的数据结构,可以充当栈、队列、阻塞队列,在实际开发中有很多应用场景。
|
25天前
|
存储 消息中间件 NoSQL
Redis 数据结构与对象
【10月更文挑战第15天】在实际应用中,需要根据具体的业务需求和数据特点来选择合适的数据结构,并合理地设计数据模型,以充分发挥 Redis 的优势。
54 8
|
24天前
|
存储 NoSQL Java
介绍下Redis 的基础数据结构
本文介绍了Redis的基础数据结构,包括动态字符串(SDS)、链表和字典。SDS是Redis自实现的动态字符串,避免了C语言字符串的不足;链表实现了双向链表,提供了高效的操作;字典则类似于Java的HashMap,采用数组加链表的方式存储数据,并支持渐进式rehash,确保高并发下的性能。
介绍下Redis 的基础数据结构
|
1月前
|
存储 算法 搜索推荐
对二叉堆的简单分析,c和c++的简单实现
这篇文章提供了对二叉堆数据结构的简单分析,并展示了如何在C和C++中实现最小堆,包括初始化、插入元素、删除最小元素和打印堆的函数,以及一个示例程序来演示这些操作。
34 19
|
1月前
|
消息中间件 存储 缓存
redis支持的数据结构
redis支持的数据结构
30 2
|
20天前
|
存储 NoSQL 关系型数据库
Redis的ZSet底层数据结构,ZSet类型全面解析
Redis的ZSet底层数据结构,ZSet类型全面解析;应用场景、底层结构、常用命令;压缩列表ZipList、跳表SkipList;B+树与跳表对比,MySQL为什么使用B+树;ZSet为什么用跳表,而不是B+树、红黑树、二叉树
|
20天前
|
存储 NoSQL Redis
Redis常见面试题:ZSet底层数据结构,SDS、压缩列表ZipList、跳表SkipList
String类型底层数据结构,List类型全面解析,ZSet底层数据结构;简单动态字符串SDS、压缩列表ZipList、哈希表、跳表SkipList、整数数组IntSet
|
1月前
|
NoSQL API Redis
如何使用 C++ 开发 Redis 模块
如何使用 C++ 开发 Redis 模块
|
8天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
36 4
|
9天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
33 4