前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Redis字典实现揭秘:从redisDb到hash冲突

Redis字典实现揭秘:从redisDb到hash冲突

原创
作者头像
Lion Long
修改于 2024-11-17 06:08:00
修改于 2024-11-17 06:08:00
14200
代码可运行
举报
运行总次数:0
代码可运行

“好事”发生

这里推荐一篇实用的文章:《MFC/C++学习系列之简单记录4——错误解决与错误提示》,作者:【升级打怪的菜鸟】。

https://cloud.tencent.com/developer/article/2466091

主要介绍了在修改程序过程出现的问题,并通过查询资料与自身经验解决错误问题,同时学习到有关代码中设置错误提示的ASSert,可以用于程序中判断逻辑是否有问题!非常适合初学者阅读。

一、redis 命令处理是单线程

对于单线程来说不能有耗时操作(所谓耗时是指阻塞IO或者CPU运算数据的时间比较长),对于redis而言耗时会影响响应性能。

1.1、redis 的耗时操作有哪些

那么redis有哪些耗时操作以及怎么解决的呢? (1)IO密集型,磁盘IO。redis是一个数据库,支持持久化,这就有磁盘IO的操作,这是需要耗时的。redis会fork一个子进程,在子进程中做持久化,不占用主进程,不会干扰主进程的命令处理。此外,redis还有aof的持久化策略,在bio_aof_fsync线程中进行异步刷盘,也不会占用redis-server主线程。

(2)IO密集型,网络IO。当redis服务多个客户,数据请求或返回数据量比较大时(比如读取前100名排行榜,或者日志写入),这是耗时的。redis通过开启IO多线程(io_thd_* 线程)来处理网络IO,解决网络IO导致的耗时问题。

(3)CPU密集型,redis支持丰富的数据结构,如果数据结构的时间复杂度比较高可能导致CPU被占用大量的时间去运算,从而耗时。

1.2、redis 命令处理不采用多线程的原因

  1. redis支持丰富的数据结构(string、list、hash、set、zset等),而且对象类型由多个数据结构实现,加锁复杂、锁粒度不好控制;它不像memcached只支持一种数据结构,加锁简单。
  2. 频繁的上下文切换,抵消多线程的优势。redis是一个数据库,有些时间段会密集访问,有些时间段比较少的访问;如果使用多线程,就需要将一些线程休眠和将一些线程唤醒,这就存在线程调度问题,线程调度就会引发上下文切换。

二、redis 存储结构

redis支持的编码定义如下(server.h):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/* Objects encoding. Some kind of objects like Strings and Hashes can be
 * internally represented in multiple ways. The 'encoding' field of the object
 * is set to one of this fields for this object. */
#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */

ZIPLIST初始的时候会分配一个连续的空间,ZIPLIST开始位置会存储第一个元素的地址,第一个元素放在最后面,第一个元素的后面也有偏移量,偏移量记录倒数第二个元素的位置,倒数第二个元素又会记录倒数第三的位置,以此类推;比如一个int数据,0~256,只需要一个字节就可以描述这个整数,如果超过256会进行紧凑压缩。 ZIPLIST按照插入顺序进行压缩。

图片
图片

redis 存储转换:

图片
图片

三、字典(dict)实现

redis 中 KV 组织是通过字典来实现的;hash 结构当节点超过512 个或者单个字符串长度大于 64 时,hash 结构采用字典实现。

3.1、redisDb

redis默认支持16个Db,但只会使用一种Db。可以通过select来选择使用哪种Db,默认是 select 0。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
127.0.0.1:6379> select 0
OK
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> 

源码中redisDb的定义(server.h):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/* Redis database representation. There are multiple databases identified
 * by integers from 0 (the default database) up to the max configured
 * database. The database number is the 'id' field in the structure. */
typedef struct redisDb {
    dict *dict;                 /* The keyspace for this DB */
    dict *expires;              /* Timeout of keys with a timeout set */
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP)*/
    dict *ready_keys;           /* Blocked keys that received a PUSH */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    int id;                     /* Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
    unsigned long expires_cursor; /* Cursor of the active expire cycle. */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;

redisDb里面有散列表,散列表有很多的key-value,不同的value可能会是不同的类型。

3.2、dict

dict定义如下(dict.h):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

dict相当于C++的类的封装。 dictType *type相当于所有的成员函数。 void *privdata相当于上下文。 dictht ht[2]存储所有的数据,是一个散列表。 rehashidx指示rehash到哪个位置了,它是从0开始的,如果rehashidx == -1,则重哈希未进行。

dictType的定义如下(dict.h):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
typedef struct dictType {
    uint64_t (*hashFunction)(const void *key);
    void *(*keyDup)(void *privdata, const void *key);
    void *(*valDup)(void *privdata, const void *obj);
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    void (*keyDestructor)(void *privdata, void *key);
    void (*valDestructor)(void *privdata, void *obj);
} dictType;

dictht的定义如下(dict.h):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

dictEntry **table是指针数组,其实是二维数组。 size 是数组的长度,必须是2的n次幂。 sizemask是数组长度-1,属于对字典的优化。因为散列表的存储是通过hash(key)%size=index 确定索引,sizemask是对取余长度的优化,将hash(key)%size变成hash(key) &sizemask,把除法优化为二进制的运算,从而提高执行速度,这种优化的前提是 数组的长度必须是2的n次幂。 used是实际存储元素的个数。

3.3、hash冲突

  1. 散列表中有一个指针数组,因为数组的槽位需要存储一个链表。
  2. key-value的存储位置通过hash(key) % size求得。
  3. hash冲突:对key进行hash,那么经过hash(key) % size求得的索引很可能出现有多个key-value都映射到同一个index,需要通过链表方式进行连接。hash冲突会造成索引效率的降低。
  4. 负载因子,描述hash冲突的激烈程度。负载因子 = used / size;used 是数组存储元素的个数,size 是数组的长度;负载因子越小,冲突越小;负载因子越大,冲突越大;redis 的负载因子是 1。
  5. 字符串经过 hash 函数运算得到 64 位整数。
  6. 相同字符串多次通过 hash 函数得到相同的64位整数。
  7. 整数对 取余可以转化为位运算。
  8. 抽屉原理 n+1个苹果放在 n 个抽屉中,苹果最多的那个抽屉至少有 2 个苹果;64位整数远大于数组的长度,比如数组长度为 4,那么 1、5、9、1+4n 都是映射到1号位数组;所以大概率会发生冲突。

3.4、扩容

redis是翻倍扩容的方式。 如果负载因子 > 1,则会发生扩容;扩容的规则是翻倍;如果正在 fork (在 rdb、aof 复写以及 rdb-aof 混用情况下)时,会阻止扩容;但是此时若负载因子 > 5,索引效率大大降低, 则马上扩容;这里涉及到写时复制原理。

扩容时,rehash的key存储的index要么在oldIndex,要么在oldIndex+oldSize,不会存放到其他的index中,因为同一个key经过hash得到的值是不会变的,只是取余的size变成了2*size。

图片
图片

写时复制核心思想:只有在不得不复制数据内容时才去复制数据内容。

3.5、缩容

如果负载因子 < 0.1,则会发生缩容;缩容的规则是恰好包含used 的2的n次方。 比如:假如此时数组存储元素个数为 9,恰好包含该元素的就是2的4次方(2的3次方 < 9 < 2的4次方),也就是 16。

图片
图片

思考:为什么缩容的负载因子不是小于1? 因为缩容的负载因子是小于1的话会造成频繁的扩缩容,扩缩容都有分配内存的操作,内存操作变得频繁就会造成IO密集。

3.6、渐进式rehash

扩容和缩容都会导致rehash,因为映射算法发生了改变。 当 hashtable 中的元素过多的时候,因为redis是一个数据库,里面存储的数据非常多,不能一次性 rehash 到ht[1];这样会长期占用 redis,其他命令得不到响应;所以需要使用渐进式 rehash。

rehash步骤: 将 ht[0] 中的元素重新经过 hash 函数生成 64 位整数,再对ht[1] 长度进行取余,从而映射到 ht[1]。

渐进式规则: (1) 分治的思想,将 rehash 分到之后的每步增删改查的操作当中。 (2)在定时器中,最大执行一毫秒 rehash ;每次步长 100 个数组槽位。

思考: 处于渐进式 rehash 阶段时,是否会发生扩容缩容?不会!

3.7、scan命令

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
scan cursor [MATCH pattern] [COUNT count] [TYPE type]

scan命令采用高位进位加法的遍历顺序,rehash 后的槽位在遍历顺序上是相邻的。 会出现一种重复的情况:在scan过程当中,发生两次缩容的时候,会发生数据重复。

图片
图片

scan要达到的目的是从scan开始那刻起redis已经存在的数据进行遍历,不会重复和遗漏(例外是scan过程中两次缩容可能造成数据重复), 因为比如scan 已经快结束了,现在插入大量数据,这些数据肯定遍历不到。 扩容和缩容造成映射算法发生改变,但是使用高位进位累加的算法,可以对scan那刻起已经存在数据的遍历不会出错。

示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
127.0.0.1:6379> keys *
1) "k1"
2) "fly"
3) "k3"
4) "rank"
5) "k2"
6) "k4"
7) "teacher"
8) "k5"
127.0.0.1:6379> scan 0 match * count 1
1) "4"
2) 1) "k1"
127.0.0.1:6379> scan 4 match * count 1
1) "2"
2) 1) "teacher"
127.0.0.1:6379> scan 2 match * count 1
1) "6"
2) 1) "k2"
   2) "k4"
127.0.0.1:6379> scan 6 match * count 1
1) "1"
2) 1) "k5"
127.0.0.1:6379> scan 1 match * count 1
1) "5"
2) 1) "fly"
   2) "k3"
   3) "rank"
127.0.0.1:6379> scan 5 match * count 1
1) "0"
2) (empty array)

3.8、redis 的expire机制

只支持对最外层key过期。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
expire key seconds
pexpire key milliseconds
ttl key
pttl key

(1) 惰性删除。 分布在每一个命令操作时检查 key 是否过期;若过期删除 key,再进行命令操作。 (2)定时删除。 在定时器中检查库中指定个数(25)个 key。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 

/*Keys for each DB loop. */
/*The default effort is 1, and the maximum
configurable effort is 10. */

config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP + ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort;
int activeExpireCycleTryExpire(redisDb *db,dictEntry *de, long long now);

3.9、大KEY

在 redis 实例中形成了很大的对象,比如一个很大的 hash 或很大的 zset,这样的对象在扩容的时候,会一次性申请更大的一块内存,这会导致卡顿;如果这个大 key 被删除,内存会一次性回收,卡顿现象会再次产生。 如果观察到 redis 的内存大起大落,极有可能因为大 key 导致的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# 每隔0.1秒 执行100条scan命令
redis-cli -h 127.0.0.1 --bigkeys -i 0.1

四、redis 使用单线程能高效的原因

在机制上:

  1. redis是内存数据库,数据存储在内存中,可以高效访问。
  2. redis使用hash table的数据组织方式,查询数据的时间复杂度为O(1),能快速查找数据。
  3. redis支持丰富的数据结构,根据性能进行数据结构切换,执行效率与空间占用保持平衡。
  4. redis使用高效的reactor网络模型。

在优化方式上:

  1. redis采用分治思想,把rehash分摊到每一个操作步骤当中,同时在定时器中以100为步长最多rehash 1ms,实现高效。
  2. redis将耗时阻塞的操作放在其他线程处理。
  3. redis的对象类型采用不同的数据结构实现。比如string对象有int、raw、embstr三种编码方式。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
127.0.0.1:6379> get fly
"100"
127.0.0.1:6379> OBJECT encoding fly
"int"
127.0.0.1:6379> set fly 1024a
OK
127.0.0.1:6379> get fly
"1024a"
127.0.0.1:6379> OBJECT encoding fly
"embstr"
127.0.0.1:6379> set fly 123456789012345678901234567890123456789012345678901234567890
OK
127.0.0.1:6379> get fly
"123456789012345678901234567890123456789012345678901234567890"
127.0.0.1:6379> OBJECT encoding fly
"raw"

五、总结

  1. 项目中使用到scan命令时,要在server端自己去重。
  2. redis处于渐进式rehash时,不会发生扩容和缩容。
  3. scan采用高位进位加法的遍历顺序,遍历目标是:不重复,不遗漏。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Redis 数据结构与对象编码 (Object Encoding)
为了将性能优化到极致,redis 作者为每种数据结构提供了不同的实现方式,以适应特定应用场景。
烂猪皮
2020/11/10
6910
Redis Hash哈希(2)
外层的哈希(RedisKV的实现)只用到了hashtable。当存储hash数据类型时,我们把它叫做内层的哈希。内层的哈希底层可以使用两种数据结构实现:
兜兜毛毛
2020/03/19
9260
深入理解Redis的scan命令
熟悉Redis的人都知道,它是单线程的。因此在使用一些时间复杂度为O(N)的命令时要非常谨慎。可能一不小心就会阻塞进程,导致Redis出现卡顿。
Jackeyzhe
2020/03/11
4.2K0
深入理解Redis的scan命令
美团针对Redis Rehash机制的探索和实践
Squirrel(松鼠)是美团技术团队基于Redis Cluster打造的缓存系统。经过不断的迭代研发,目前已形成一整套自动化运维体系,涵盖一键运维集群、细粒度的监控、支持自动扩缩容以及热点Key监控等完整的解决方案。同时服务端通过Docker进行部署,最大程度的提高运维的灵活性。分布式缓存Squirrel产品自2015年上线至今,已在美团内部广泛使用,存储容量超过60T,日均调用量也超过万亿次,逐步成为美团目前最主要的缓存系统之一。
美团技术团队
2019/03/22
1.2K0
美团针对Redis Rehash机制的探索和实践
redis源码之dict
大家都知道redis默认是16个db,但是这些db底层的设计结构是什么样的呢?我们来简单的看一下源码,重要的字段都有所注释
程序员小饭
2020/09/26
5270
redis源码之dict
老板们都应该学一学 Redis,它能管理上亿对象,你们呢?
我们知道一个大型的公司往往都具有复杂的组织结构,成百上千号员工,要做到大而不乱,就必须依靠合理的组织结构来优化内部的交流成本。Redis 内部也有组织结构,不同的是这个组织结构要维系上亿的对象,而不是几百几千。今天我来向大家呈现 Redis 如何来管理这上亿的对象而不会混乱的。
老钱
2018/09/29
5470
老板们都应该学一学 Redis,它能管理上亿对象,你们呢?
Redis 设计与实现读书笔记
(4) 如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现
_春华秋实
2023/08/31
2470
细品Redis高性能数据结构之hash对象
上一节讲Redis的高性能字符串结构SDS,今天我们来看一下redis的hash对象。
袁新栋-jeff.yuan
2020/08/26
8610
细品Redis高性能数据结构之hash对象
Redis源码分析(三)——Redis数据结构-字典
1. 数据结构 1.1 哈希表 typedef struct dictht{ dictEntry **table; unsigned long size; unsigned long si
大闲人柴毛毛
2018/03/09
6750
Redis源码分析(三)——Redis数据结构-字典
【Redis 系列】redis 学习十六,redis 字典(map) 及其核心编码结构
redis 是使用 C 语言编写的,但是 C 语言是没有字典这个数据结构的,因此 C 语言自己使用结构体来自定义一个字典结构
阿兵云原生
2023/02/16
3090
Memcached 与 Redis 实现的对比
_Super_
2016/10/06
7.8K13
Memcached 与 Redis 实现的对比
redis 字典的实现
serena
2017/05/18
1.4K0
redis 字典的实现
从源码看redis的&#39;map&#39;结构
默认的map结构使用的是ziplist的编码方式,当超过hash_max_ziplist_value(默认64)时则会将编码方式替换成 OBJ_ENCODING_HT。",is_english:d,is_original:h,user_index:5.0206691216177,original_type:d,original_author:e,content:"
爬蜥
2022/09/16
1530
从源码看redis的'map'结构
user:100是整个map结构的key,name是map中的一项字段值,通过hget就可以获取存入的结果
爬蜥
2019/07/30
7630
《Redis设计与实现》读书笔记(二) ——Redis中的字典(Hash)
《Redis设计与实现》读书笔记(二) ——Redis中的字典(Hash) (原创内容,转载请注明来源,谢谢) 一、概述 字典,又称符号表、关联数组、映射,是一种保存键值对的抽象数据结构。每个键(key)和唯一的值(value)关联,键是独一无二的,通过对键的操作可以对值进行增删改查。 redis中字典应用广泛,对redis数据库的增删改查就是通过字典实现的。即redis数据库的存储,和大部分关系型数据库不同,不采用B+tree进行处理,而是采用hash的方式进行处理。 另外,毫无疑问,redis的hash
用户1327360
2018/03/07
1K0
《Redis设计与实现》读书笔记(二)  ——Redis中的字典(Hash)
redis中hash扩容过程
Redis一共支持5种数据结构,hash是其中的一种,在hash扩容的时候采用的是渐进式rehash的方式。要想深入理解渐进式rehash,首先要了解以下Redis中hash的数据结构。
chenchenchen
2021/09/06
3.1K0
Redis源码解析——字典基本操作
        有了《Redis源码解析——字典结构》的基础,我们便可以对dict的实现进行展开分析。(转载请指明出于breaksoftware的csdn博客)
方亮
2019/01/16
6100
Redis数据结构——dict(字典)
字典在Redis中的作用是非常巨大的,对Redis数据库的增删改查等操作都构建在对字典的操作之上,因此,了解字典的底层实现能让我们对Redis有更深的理解。下面分4个模块讲解Redis的字典实现(基本所有实现细节和重点都会谈到):
向着百万年薪努力的小赵
2022/12/02
4530
Redis数据结构——dict(字典)
《闲扯Redis七》Redis字典结构的底层实现
字典, 又称符号表(symbol table)、关联数组(associative array)或者映射(map), 是一种用于保存键值对(key-value pair)的抽象数据结构。在字典中, 一个键(key)可以和一个值(value)进行关联(或者说将键映射为值), 这些关联的键和值就被称为键值对。
大道七哥
2020/07/27
1.3K0
《闲扯Redis七》Redis字典结构的底层实现
从Redis源码上来聊聊KV模型-Hash数据类型
建议先阅读:神奇,Redis存储原理竟然是这样! – Karos (wzl.fyi),或者本页面的第一章
Karos
2023/07/21
5600
从Redis源码上来聊聊KV模型-Hash数据类型
相关推荐
Redis 数据结构与对象编码 (Object Encoding)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档