diva-notes
  • README
  • Ads
    • 定价策略
    • 广告层级
    • 归因模型
    • 买量
    • Chat GPT
    • Google
  • AI
    • 参考资料
    • Chat GPT
    • stable-diffusion-webui安装
  • Algorithm
    • 倍增
    • 并查集
    • 参考
    • 环的判断
    • 凸包
    • 蓄水池抽样
    • 最短路径
    • 最小生成树
    • KMP算法
    • Rabin-Karp算法
    • Tarjan桥算法
  • Architecture
    • Serverless
  • Career
  • CICD
    • 代码质量
    • CICD实践
  • Data Structure
    • 布谷鸟过滤器
    • 布隆过滤器
    • 浮点
    • 红黑树
    • 锁
    • LSM树
  • DB
    • My SQL
      • 隔离级别
      • 架构
      • 索引
      • 锁
      • 页结构
      • 主从同步
      • ACID
      • Log
      • MVCC
      • Questions
    • Postgres
      • 持久化
      • 对比MySQL
      • 隔离级别
      • 索引
      • Greenpulm
      • MVCC
    • 倒排索引
    • 列式存储
    • H Base
    • HDFS
    • MPP数据库选型
    • Questions
  • Distributed System
    • 分布式事务
    • 服务网格
    • BASE理论
    • CAP
    • Etcd
    • Raft协议
    • ZAB协议
  • Go
    • 1.语言基础
      • 1.CPU寄存器
      • 2-1.函数调用
      • 2-2.函数调用栈
      • 2.接口
      • 3.汇编
      • 4.调试
    • 2.编译
      • 1.编译
      • 2.词法与语法分析
      • 3.类型检查
      • 4.中间代码生成
      • 5.机器码生成
    • 3.数据结构
      • 1.数组array
      • 2.切片slice
      • 3.哈希表map
      • 4.字符串
    • 4.常用关键字
      • 1.循环
      • 2.defer
      • 3.panic和recover
      • 4.make和new
    • 5.并发编程
      • 1.上下文Context的实现
      • 2-1.runtime.sema信号量
      • 2-2.sync.Mutex的实现
      • 2-3.sync.WaitGroup
      • 2-4.sync.Once的实现
      • 2-5.sync.Map的实现
      • 2-6.sync.Cond
      • 2-7.sync.Pool的实现
      • 2-8.sync.Semaphore的实现
      • 2-9.sync.ErrGroup
      • 3.定时器Timer的实现
      • 4.Channel的实现
      • 5-1.调度-线程
      • 5-2.调度-MPG
      • 5-3.调度-程序及调度启动
      • 5-4.调度-调度策略
      • 5-5.调度-抢占
      • 6.netpoll实现
      • 7.atomic
    • 6.内存管理
      • 1-1.内存分配基础-TCmalloc
      • 1-2.内存分配
      • 2.垃圾回收
      • 3.栈内存管理
    • 参考
    • 各版本特性
    • 坑
    • Go程序性能优化
    • http.Client
    • net.http路由
    • profile采样的实现
    • Questions
    • time的设计
  • Kafka
    • 高可用
    • 架构
    • 消息队列选型
    • ISR
    • Questions
  • Network
    • ARP
    • DNS
    • DPVS
    • GET和POST
    • HTTP 2
    • HTTP 3
    • HTTPS
    • LVS的转发模式
    • NAT
    • Nginx
    • OSI七层模型
    • Protobuf
    • Questions
    • REST Ful
    • RPC
    • socket缓冲区
    • socket详解
    • TCP滑动窗口
    • TCP连接建立源码
    • TCP连接四元组
    • TCP三次握手
    • TCP数据结构
    • TCP四次挥手
    • TCP拥塞控制
    • TCP重传机制
    • UDP
  • OS
    • 磁盘IO
    • 调度
    • 进程VS线程
    • 零拷贝
    • 内存-虚拟内存
    • 内存分配
    • 用户态VS内核态
    • 中断
    • COW写时复制
    • IO多路复用
    • Questions
  • Redis
    • 安装
    • 参考
    • 高可用-持久化
    • 高可用-主从同步
    • 高可用-Cluster
    • 高可用-Sentinel
    • 缓存一致性
    • 事务
    • 数据结构-SDS
    • 数据结构-Skiplist
    • 数据结构-Ziplist
    • 数据结构
    • 数据类型-Hashtable
    • 数据类型-List
    • 数据类型-Set
    • 数据类型-Zset
    • 数据淘汰机制
    • 通信协议-RESP
    • Questions
    • Redis6.0多线程
    • Redis分布式锁
    • Redis分片
  • System Design
    • 本地缓存
    • 错误处理
    • 大文件处理
    • 点赞收藏关注
    • 短链接生成系统
    • 负载均衡
    • 高并发高可用
    • 规则引擎
    • 集卡活动
    • 秒杀系统
    • 评论系统
    • 熔断
    • 限流
    • 延迟队列
    • Docker
    • ES
    • K 8 S
    • Node.js
    • Questions
  • Work
    • Bash
    • Charles
    • Code Review
    • Ffmpeg
    • Git
    • intellij插件
    • I Term 2
    • Mac
    • mysql命令
    • Nginx
    • postgresql命令
    • Protoc
    • Ssh
    • Systemd
    • Tcp相关命令
    • Vim
Powered by GitBook
On this page
  • dictEntry
  • dict 哈希表
  • 字段初始化
  • 哈希表渐进式 rehash 的详细步骤
  • 触发条件
  • 触发时机
  • 渐进式 rehash 执行期间的哈希表操作
  • 好处
  • 问题
  1. Redis

数据类型-Hashtable

dictEntry

哈希表里的 key 和 value,是封装为 dictEntry 对象存储的。

//24字节
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    struct dictEntry *next;     // 指向下个哈希表节点,形成链表
} dictEntry;

dict 哈希表

redis 里的 hashtable 使用了链表法解决哈希冲突。

// 哈希表
// 每个字典都使用两个哈希表子结构,以实现渐进式 rehash 。
typedef struct dict {
    dictType *type; // 类型特定函数
    void *privdata; // 私有数据
    dictht ht[2];   // ht=hash table,每个dict有俩,增量扩容时使用
    
    // 搬迁进度
    // 因为是渐进式的哈希,数据的迁移并不是一步完成的,所以需要有一个索引来标记当前的搬迁进度
    // rehash 是 以bucket(桶)为基本单位进行渐进式的数据迁移的,每步完成一个 bucket 的迁移,直至所有数据迁移完毕
    long rehashidx;      // 扩容时的迁移进度。不迁移时为-1
    int16_t pauserehash; // 标记是否暂停扩容。持久化时需要暂停扩容
} dict;

// 哈希表子结构
typedef struct dictht {
    // 哈希表数组
    // 可以看作是:一个哈希表数组,数组的每个项是entry链表的头结点(链地址法解决哈希冲突)
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值。其值即为 size - 1
    unsigned long sizemask;
    // 该哈希表已有节点的数量
    unsigned long used;
} dictht;

字段初始化

在 redis 中字典中的 hash 表采用的是延迟初始化策略。创建字典的时候并没有为哈希表分配内存,只有当第一次插入数据时,才真正分配内存

哈希表渐进式 rehash 的详细步骤

在 redis 中,扩展或收缩哈希表需要将 ht[0] 里面的所有键值对 rehash 到 ht[1] 里面, 但是, 为了避免 rehash 对服务器性能造成影响,这个 rehash 动作并不是一次性、集中式地完成的, 而是分多次、渐进式地完成的。 这个跟 Golang 里 map 的扩容几乎是一样的。

  1. 为 ht[1] 分配两倍原哈希表的空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。

  2. 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。

  3. 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 的值加 1。期间 hash[0] 停写。

  4. 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时将 rehashidx 属性设为 -1 , 表示 rehash 已完成。

触发条件

  1. 没有在执行 BGSAVE / BGREWRITEAOF 命令 2. keys / size >= 1 触发扩容 2. keys / size <= 0.1 触发缩容

  2. 在执行 BGSAVE / BGREWRITEAOF 命令

    1. 扩容因子为 5

// src/dict.c
static int _dictExpandIfNeeded(dict *d) {
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio) && // dict_force_resize_ratio 持久化时扩容条件更严苛
        dictTypeExpandAllowed(d))
    {
        return dictExpand(d, d->ht[0].used + 1);
    }
    return DICT_OK;
}

触发时机

  1. 在 redis 中每一个增删改查命令中都会判断数据库字典中的哈希表是否正在进行搬迁,如果是则帮助执行一次

  2. 如果服务器比较空闲,redis 数据库将很长时间内都一直使用两个哈希表。所以也会有定时任务周期性检查,如果发现有字典正在进行搬迁,则会花费1毫秒的时间协助搬迁

// src/dict.c
// 向dict中写入时
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) {
    if (dictIsRehashing(d)) _dictRehashStep(d); // 辅助迁移
}

渐进式 rehash 执行期间的哈希表操作

因为在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删改查等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找。

另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作:这一措施保证了 ht[0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。

// src/dict.c
// 向dict中写入时
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) {
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; // 判断当前命中的 dict 是否正在扩容,是的话使用 ht[0]
}

好处

渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。

问题

渐进式 rehash 避免了redis 阻塞,可以说非常完美,但由于在 rehash 时,需要分配一个新的 hash 表,在 rehash 期间,同时有两个 hash 表在使用,会使得 redis 内存使用量瞬间突增,在 redis 满容状态下由于 rehash 会导致大量 key 被驱逐。

Previous数据结构Next数据类型-List

Last updated 3 years ago

image-20210804164931040