Redis数据结构——字典-hashtable

本文介绍Redis中字典的实现方式,包括哈希表的结构、解决键冲突的方法、rehash操作以及渐进式rehash过程。字典是Redis数据库和哈希键的底层实现,使用哈希表来存储键值对。

字典简介:

字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对的抽象数据结构。

字典是一种用于保存键值对的抽象数据结构。由于C没有内置这种数据结构,Redis构建自己的字典实现。

Redis的数据库就是使用字典来作为底层实现的。字典还是哈希键的底层实现之一。

除了用来实现数据库之外,字典还是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现。

1、字典实现

Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。

1.1、哈希表定义

typedef struct dictht {
    //哈希表数组
    dictEntry **table;
    //哈希表大小
    unsigned long size;
    //哈希表大小掩码,总是等于size-1
    unsigned long sizemask;
    //该哈希表已有节点数量
    unsigned long used;
}

table属性是一个数组,数组中的每个元素都是一个指向dict.h/ dictEntry结构的指针,每个dictEntry结果保存着一个键值对。size属性记录了哈希表的大小,也即是table数组的大小,而used属性则记录了哈希表目前已有节点(键值对)的数量。sizemask属性的值总是等于size-1,这个属性和哈希值一起决定了一个键应该被放到table数组的哪个索引上面。下图是一个空哈希表。

1.2、哈希表节点

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

key属性保存着键值对中的键,而v属性保存着键值对中的值,其中键值对的值可以是一个指针,或者是一个uint64整数或者是一个int64_t整数。

next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,以此来解决键冲突(collision)的问题。

下图展示了将两个索引值相同的键k1和k0连接在一起。

 

1.3、字典

typedef struct dict {
    //类型特定函数
    dictType *type;
    //私有数据
    void *privdata;
    //哈希表
    dictht ht[2];
    //rehash索引,当rehash不进行时,为-1
    int rehashidx;
}

type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的

type:一个指向ditcType结构的指针,每个ditcType结构保存了一簇用于操作特定类型键值对的函数,redis回味用途不同的字典设置不同的类型特定函数。

privdata:保存了需要传给那些类型特定函数的的可选参数。

ht属性包含两个项的数组,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]进行rehash时使用。

除了ht[1]之外,另一个和rehash有关的属性就是rehashidx,它记录了rehash目前的进度,如果目前没有在进行rehash,那么它的值-1。

下图是一个普通状态下的字典

 

2、解决键冲突

 当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时,我们称这些键发生了冲突。

Redis的哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题。

 因为dictEntey节点组成的链表没有指向链表表尾的指针,所以为了速度考虑,程序总是将新节点添加到链表的表头位置(复杂度为0(1)),排在其他已有节点的前面。下图为k2加入到哈希表中出现冲突,加入到链表头之后的情况。

3、rehash

随着操作不断进行,哈希表保存的键值会逐渐增多或者减少,为了让哈希表负载因子维持在一个合理范围之内,当哈希表保存的键值数量太多或者太少时,就会对哈希表进行相应的扩展或者收缩。

扩展和收缩通过执行rehash(重新散列)操作来执行。步骤如下:

1)为字典ht[1]哈希表分配空间,空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量:

  • 如果扩展,ht[1]大小等于ht[0].used*2*2n(2的n次方幂)。
  • 如果收缩,ht[1]大小等于ht[0].used的2n(2的n次方幂)。

2)将ht[0]中的所有键值对rehash到ht[1]上面;rehash指的是重新计算键的哈希值和索引值,然后放到指定位置上。

3)当ht[0]所有的键值对都迁移过去后,将ht[1]设置为ht[0],并未ht[1]创建新的空白哈希表,为下一次rehash做准备。

 举个例子,如下图

 

1、ht[0].used当前的值为4,4*2=8,而8(2^3)恰好是第一个大于等于4的2的n次方,所以程序会将ht[1]

哈希表的大小设置为8。下图展示了ht[1]分配空间之后,字典的样子。

2、 将ht[0]包含的四个键值对都rehash到ht[1],如下图

3、 释放ht[0],并将ht[1]设置为ht[0],然后为ht[1]分配一个空白哈希表,如下图,至此,哈希表的扩展操作执行完毕。

自动执行扩展操作条件:

  1. 服务器目前没有执行BGSAVE命令或者BGREWRITEAOF命令,并且负载因子大于等于1
  2. 服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且负载因子大于等于5

当哈希表负载因子小于0.1时,程序自动开始对哈希表执行收缩操作。

负载因子=哈希表已保存节点数量/哈希表大小   load_factor = ht[0].used / ht[0].size

 

4、渐进式rehash

rehash操作并不是一次性、集中式完成的,而是分多次、渐进式完成的。如果在数据量很大的情况下,一次性操作可能会导致服务器一段时间内停止服务。

因此,为了避免rehash对服务器性能造成影响,服务器不是一次性将ht[0]里面的所有键值对全部rehash到ht[1],而是分多次、渐进式的进行。

以下是哈希表渐进式rehash的详细步骤:

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

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

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

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

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

 

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

进行渐进式rehash的过程中,字典同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash进行期间,字典的删除、查找、更新等操作会在两个哈希表上进行。另外,新添加到字典的键值对一律保存到ht[1]里面,而ht[0]则不再进行任何添加操作。这一措施保证了ht[0]包含的键值对数量会只减不增,并随着rehash操作的执行而最终变成空表。。

<think>嗯,用户想了解Redis 6.0支持的数据结构及其特性。这个问题很专业,需要系统整理Redis核心数据结构在6.0版本中的实现和优化。 首先回忆Redis 6.0的重要更新,虽然数据结构本身没有新增类型,但内存管理和线程模型有重大改进。用户引用的资料[1][4]提到6.0版本提升了性能和扩展性,特别是客户端缓存特性[^4],这对数据结构的使用效率有间接影响。 从基础数据结构开始梳理: 字符串(String)作为最基础的类型,在6.0中embstr编码优化了短字符串存储,引用[2]提到的list结构体其实也适用于所有数据结构——Redis都采用类似的设计模式:用专属结构体管理元数据(如len字段),再指向具体存储节点。哈希表(Hash)的ziplist转hashtable的阈值值得说明,这个直接影响内存效率。 列表(List)要重点讲quicklist结构,这是3.2版本引入的,但6.0的线程IO优化使其并发性能提升。用户引用[2]的list结构体代码很关键,其中dup和free函数指针说明Redis支持深拷贝[^2]。集合(Set)的整数集合升级机制需要举例说明,比如$SADD$命令触发intset转hashtable的过程。 有序集合(ZSet)的ziplist+skiplist双结构是难点,要解释跳表如何实现$O(logN)$查询。引用[4]的客户端缓存特性[^4]对频繁访问的有序集合特别有利,因为可以避免重复查询。位图(Bitmap)和HyperLogLog虽然基于字符串,但它们的位操作指令如$SETBIT$需要单独说明。 地理空间索引(Geospatial)本质是有序集合,但GEOADD命令的坐标编码机制值得展开。流(Stream)作为5.0新增结构,在6.0中稳定性增强,要强调消息ID的时序性和消费者组特性。 最后补充6.0的线程安全改进:虽然数据结构操作本身还是单线程,但网络IO多线程化提升了大value操作的吞吐量。引用[3]的bind配置[^3]看似无关,实则影响数据结构服务稳定性——错误的网络配置可能导致数据丢失。 用户可能混淆了数据结构版本差异,需要明确6.0是优化而非新增结构。相关问题的设计要覆盖使用场景(如为什么选Hash而不是String存储对象)和性能对比(如ZSet的跳表查询复杂度证明)。</think>Redis 6.0 延续了其丰富的数据结构支持,以下是核心数据结构及其特性的详细解析: ### 一、核心数据结构 1. **字符串(String)** - 存储文本、二进制数据(如图片)或数值 - 最大容量 512MB - 特殊操作: ```bash INCR counter # 原子计数器 APPEND log "msg" # 追加操作 BITCOUNT user:1 # 位运算 ``` 2. **哈希(Hash)** - 存储字段-值映射表,适合对象存储 - 底层实现- `ziplist`(元素少时,内存紧凑) - `hashtable`(元素多时,$O(1)$ 复杂度) - 示例: ```bash HSET user:1000 name "Alice" age 30 HGETALL user:1000 ``` 3. **列表(List)** - 双向链表结构,支持左右插入/弹出 - 关键特性: -实现队列(LPUSH/RPOP)或栈(LPUSH/LPOP) - 阻塞操作:`BLPOP` 实现消息队列 - 底层结构:`quicklist`(ziplist 链表,平衡内存和速度) 4. **集合(Set)** - 无序唯一元素集合 - 支持交并差集运算($SINTER/SUNION/SDIFF$) - 底层实现- `intset`(纯整数时) - `hashtable`(其他类型) 5. **有序集合(Sorted Set)** - 元素关联分值,按分值排序 - 底层结构: - `ziplist`(小规模) - **跳表(skiplist)+ 字典**(大规模,$O(logN)$ 复杂度) - 范围操作: ```bash ZRANGEBYSCORE players 1000 2000 # 按分值检索 ``` ### 二、高级数据结构 6. **位图(Bitmap)** - 基于字符串的位操作 - 适用场景:实时用户签到统计 ```bash SETBIT user:2024:sign 15 1 # 记录第15天签到 BITCOUNT user:2024:sign # 统计总签到 ``` 7. **HyperLogLog** - 基数估算(误差约 0.81%) - 内存仅需 12KB ```bash PFADD ip_log 192.168.1.1 PFCOUNT ip_log # 估算独立IP数 ``` 8. **地理空间索引(Geospatial)** - 存储经纬度坐标 - 支持距离计算(GEODIST)和半径查询(GEORADIUS) ```bash GEOADD cities 116.40 39.90 Beijing GEOSEARCH cities FROMLONLAT 116.4 39.9 BYRADIUS 100 km ``` 9. **流(Stream)** - 消息队列实现Redis 5.0+) - 特性: - 消息持久化 - 消费者组(Consumer Groups) - 消息回溯 ```bash XADD orders * product "Book" price 29.9 XREAD GROUP group1 consumer1 COUNT 1 STREAMS orders > ``` ### 三、Redis 6.0 的增强特性 1. **多线程网络 I/O** - 处理网络请求使用多线程(执行命令仍单线程) - 提升高并发场景吞吐量 2. **客户端缓存(Client-side Caching)** - 服务端跟踪客户端缓存(`RESP3` 协议支持) - 主动通知缓存失效(避免脏读)[^4] 3. **ACL 权限控制** - 细粒度命令权限管理 ```bash ACL SETUSER alice ON >pass123 +@read ~cache:* ``` 4. **TLS 加密传输** - 增强客户端-服务端通信安全[^3] ### 四、数据结构选择建议 | 场景 | 推荐结构 | 原因 | |------|----------|------| | 对象属性存储 | Hash | 字段独立存取 | | 排行榜 | Sorted Set | 天然排序能力 | | 消息队列 | Stream | 消息持久化和消费组 | | UV统计 | HyperLogLog | 低内存消耗 | > Redis 6.0 通过优化内存分配(jemalloc 改进)和网络层多线程,显著提升了大型数据结构(如数十万元素的 Sorted Set)的操作效率[^1][^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值