数据如何存储

偷大佬的全景图

看代码

// redis 5.0.5
// 定义Redis数据库的结构体
typedef struct redisDb {
    // 存储数据库中的键值对
    dict *dict;                 
    // 存储设置了超时时间的键
    dict *expires;              
    // 存储有客户端等待数据的键(例如BLPOP操作)
    dict *blocking_keys;        
    // 存储接收到PUSH操作的阻塞键
    dict *ready_keys;           
    // 存储在MULTI/EXEC事务中被WATCH的键
    dict *watched_keys;         
    // 数据库ID,标识不同的数据库,从0开始
    int id;                     
    // 平均TTL值,仅用于统计
    long long avg_ttl;          
    // 需要延迟碎片整理的键名列表
    list *defrag_later;         
} redisDb;
  • dict *dict: 字典(`dict`),用于存储数据库中的所有键值对。

  • dict *expires: 字典,用于存储那些设置了过期时间的键。

  • dict *blocking_keys: 字典,包含了有客户端正在等待数据的键,比如在执行 BLPOP 操作时。

  • dict *ready_keys: 字典,包含了那些已经接收到 PUSH 操作的阻塞键。

  • dict *watched_keys: 字典,包含了在 MULTI/EXEC 事务中被 WATCH 的键。

  • int id: 数据库ID,用于区分不同的数据库。

  • long long avg_ttl: 数据库中键的平均生存时间(TTL),仅用于统计信息。

  • list *defrag_later: 指向一个列表,包含了需要延迟进行碎片整理的键名。

typedef struct dict { // 定义一个名为dict的结构体类型

    dictType *type; // 指向dictType类型的指针,用于存储字典中值的类型信息

    void *privdata; // 指向void类型的指针,用于存储与字典相关的私有数据

    dictht ht[2]; // 包含两个dictht类型的元素的数组,dictht可能是一个结构体,用于哈希表的内部结构

    long rehashidx; // 长整型变量,用于跟踪rehash进度,如果值为-1,则表示没有rehash操作正在进行

    unsigned long iterators; // 无符号长整型变量,用于记录当前正在运行的迭代器数量

} dict; // 结构体定义结束,dict是这个结构体的别名

大致工作原理图

设置有过期时间的对象如何存储

  • redisDb 结构体成员中有专门的字典成员存(dict *expires)储设置有过期时间的kv对

  • 过期字典中的key 和 数据字典中的key 共享一个对象,索引存的是这个key的指针

操作数据库中的key(对象)

  • 初始化状态

  • 添加数据

set hellomsg “hello mart”

  • 查询

get hellomsg

解析数据包 -> 查redisDb中的字典 -> 匹配到 key -> 顺着索引取出数据 -> 发送给client

  • 更新数据

RPUSH animals bird

  • 删除数据

Redis 是单线程还是多线程

  • 主流程,也就是数据库事务操作仍是单线程

  • 4.0 之后某些异步流程改为多线程,比如 UNLINK、FLUSH 落盘指令。6.0后网络I/O 部分改为多线程

核心仍是多线程的原因是什么?Redis 使用单线程的原因是什么?

  • redis 设计之初面向的数据事务就是短快类型的数据事务,单线程能很好的保证事务的原子性,数据的顺序性。若在核心业务中引入多线程,就会带来的数据竞争,且编程复杂度也会直线上升,综上,Redis 仍然使用单线程。

  • redis 性能瓶颈不在数据事务操作本身,而在网络I/O、磁盘I/O 等其他方面,贸然引入多线程到核心线程,不见得有什么好处.且Redis 已经为单线程做了诸多优化,比如精心为单线程设计的SDS,各种精确到位操作的编码、这些设计只执行对应的业务函数本身并不长(指令量不多),如果引入多线程,光保护临界资源所耗费的开销都有可能在其中占用较大部分资源了。

最后偷张图

最后关于Redis 单线程下的优化

Redis的数据结构和单线程模式下的优化主要体现在以下几个方面:

1. 基于内存的操作

Redis的数据都存储在内存中,内存的读写速度远远快于磁盘。因此,相比于传统的基于磁盘的数据库,Redis的内存存储方式极大地提高了数据的读写速度。

2. 高效的数据结构

Redis使用多种高效的数据结构,如哈希表(用于实现HashMap)、跳跃表(用于实现SortedSet)等。这些数据结构的设计使得数据的查找、插入和删除操作能够在尽可能短的时间内完成。例如,哈希表的查找操作平均时间复杂度为O(1),跳跃表的查找操作时间复杂度为O(logn)。

3. 非阻塞I/O模型

Redis采用了非阻塞I/O多路复用机制,如epoll(在Linux系统下)。这种机制允许Redis在一个线程中同时处理多个连接的I/O请求,而不会被阻塞。当有多个客户端连接到Redis时,Redis可以高效地监听这些连接上的可读、可写等事件,只有当事件真正发生时才进行相应的处理,从而充分利用CPU资源。

4. 单线程避免了锁竞争

由于Redis是单线程的,所以不存在多个线程同时访问共享资源而需要加锁的情况。这样就避免了锁的开销,包括获取锁、释放锁的时间以及因为锁竞争导致的线程上下文切换等开销。

5. 代码优化和算法优化

Redis的源码中,对各种命令的处理都进行了优化,以减少不必要的计算和内存分配。例如,简单的SET命令(`setCommand`)直接调用`setGenericCommand`函数,减少了代码的复杂度和执行路径的长度。

6. 内存分配器优化

Redis使用自己定制的内存分配器(如jemalloc),以减少内存分配和释放的开销,提高内存管理的效率。

7. 数据类型内部编码优化

Redis的不同数据类型可以根据存储的数据特性采用不同的内部编码方式,以进一步优化内存使用和访问速度。例如,字符串(String)可以采用int编码、raw编码或embstr编码,根据数据的特点自动选择最合适的编码方式。

8. 避免大数据结构操作导致的阻塞

Redis通过优化数据结构和算法,避免了大数据结构操作导致的阻塞,例如通过使用ziplist(压缩列表)来存储小型的List和Set,以减少内存占用和提高访问速度。

这些优化措施共同作用,使得Redis即使在单线程模式下也能保持极高的性能和吞吐量。Redis的单线程模型简化了并发控制,减少了锁的使用,使得操作更加原子性和快速。同时,Redis的事件驱动模型和非阻塞I/O处理能力,使得它能够有效地处理大量的并发连接和请求。

Redis 单线程为什么这么快?

  • 看上面.... “核心仍是多线程的原因是什么?Redis 使用单线程的原因是什么?”

Redis 性能如何?

  • 保底8W,一般情况都有10W QPS

具体可以看各家厂商Redis 主机专栏 的branchmark

Redis 处理过程 <这部分有空细说...>

  • 数据接收 - 解析

看图

  • 从客户端对象buffer 读取 - 执行 -返回执行结果

网络I/O 部分多线程配置

  • 配置 /etc/redis/redis.conf 配置文件开启多线程数 Redis 6.0 配置文件

# 启用 I/O 多线程
io-threads 4  # 设置线程数,根据 CPU 核心数调整 // 1 时表示只有主线程,也就是单线程模式
io-threads-do-reads yes  # 允许读取操作在多个线程中处理
  • 负载方式 round-robin; evenloop 会将I/O 任务轮流分配个每个线程种的 io_thred_list 作业队列

  • 投送完成后主线程会轮询等待累加各线程计数器,直到累加计数器 = 0 表示个线程的作业队列为空,表示作业完毕;才会去执行各作业线程汇总来的解析命令 buffer,执行完后将执行结果写到传进来的客户端对象的写buffer中,唤醒对应的send 通知函数,让主事件循环继续分配socket send 作业。

Redis 配置文件中配置了多线程,多线程真的就开启了吗?(配置得线程池什么时候工作)

  • 配置文件 io-threads-do-reads 表示是否在读阶段也使用 io 线程,默认是只在写阶段使用 io 线程的。

  • 开启后不是线程池有任务来就工作得,而是到大得任务量达到一定量得时候才开启:

    • 读写线程工作条件:concurrent > io_thread_num * 2 是才会将 io_threads_active 置为true 表示启动线程

    • 并将对应时间操作位置位 对应类型枚举 io_threads_op 置为IO_THREADS_OP_READ 或者 IO_THREADS_OP_WRITE

-- 简单来说就是当主线程发现读写任务有积压且积压大于一定量时线程池才会处于活跃状态,具体说是积压量 大于 线程池得两倍是才会开启线程池作业...

6.0 多线程后性能提升了吗?

有人压测过:

  • 读提升100%QPS

  • 写提升 75% - 100% QPS


Redis 内存管理

Redis 最大支持配置(可以占用)多大内存?

  • 32 CPU寻址位宽限制,系统安装容量最大只能到4G,Redis 在32 位系统中默认最大容量是3G

  • 在64 CPU 寻址位宽是32的两倍,支持的最大内存是32位32的2幂次倍,即支持最大内存是2147483648GB 对于redis 来说是一个十分大的数据了

  • 要想配置redis 最大使用内存可 maxmemory 项, 这是redis 在 /etc/redis/redis.conf 中的 一个配置项

内存满了怎么办?

  • 如果配置文件中配置了内存淘汰策略,就会触发淘汰策略,通过对应策略淘汰一些符合淘汰条件的kv对,直到占用内存低于阈值maxmemory 配置的值,停止淘汰.

看图

Redis淘汰策略都有那几种?

看图

  • noeviction 不淘汰,说明数据很重要,内存满了报错,写入失败。

  • 基于时间的淘汰策略

    • volatile 局部类型

      • volatile-lru (last recently use)

      • volatile-lfu (last frequency use)

      • volatile-random

      • volatile-ttl (expires 设置有EX/PEX参数从小到大)

    • 全局类型

      • allkeys-lru

      • allkeys-lfu

      • allkeys-random

如何选用淘汰策略?

  • 看业务.... 不能丢的,用noeviction 写不下告警,人工处理,做好灾备配置 (一般开启RDB + AOF)

Redis 中的LRU (last recently use)

Redis 中使用的LRU 不是严格LRU 而是一种近似的LRU,因为使用严格LRU 需要维持海量key 的时间戳这内存代价对于redis 这种内存型数据库来说是非常昂贵的.

typedef struct redisObject {
    // 类型
    unsigned type:4个bit;
    // 编码
    unsigned encoding:4个bit;
    // 对象最后一次被访问的时间
    unsigned lru:REDIS_LRU_BITS; /* LRU_BITS为24bit*/
//引用计数
    int refcount;4个字节
    // 指向实际值的指针
    void *ptr;8个字节
} robj;

redis 内部有个变量记录毫秒级当前unix 时间戳,每100ms 更新一次,取值是 nowtime % 2 ^ 24,这个更新频率可以用info 查看,hz输出项就是每秒刷新次数,这个工作频率可通过配置文件修改。

Redis 的redisObject 对象中定义有一个无符号的lru属性成员占24位,这个成员就是用来保存LRU 的最近访问时间戳(LRU:毫秒级 % 2^24)或者分钟级时间戳(LFU)+ 访问计数,淘汰策略触发时默认随机采样5个kv对,逐个放淘汰池比较时间戳淘汰。如果还触发淘汰,继续捞5个继续重复上面淘汰策略...

效果怎么样?

  • 效果还不错,3.0 后新增16个大小淘汰池后很近似严格LRU效果

Redis 中的LFU (last frequency use)

LFU 只需要注意 对象初始化后默认访问计数是5,每分钟减1,达到一定访问次数后计数会增加,redis 默认计数因子是10,等级越高计数增加越难

100 hit -> 10

1000 hit -> 18

....