2、Redis 执行

1、Redis 内存结构

1.1、数据库结构

/* 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 */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;

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;

image

1.2、添加数据

将键值对,添加到 dict 结构字典中去,Key 必须为 String 对象,Value 为任何类型的对象都可以
我们使用命令:SET hellomsg "hello mart",键空间会变成如下结构
image

1.3、查询数据

数据查询很清晰,在 dict 找到对应的 key 即完成了查询

1.4、更新数据

对已存在 Key 对象的任何变更操作,都是更新,比如针对 String 对象赋予新值、给 List 对象增减元素
举个例子,使用命令 RPUSH animals bird 成功后得到的结构如下
image

1.5、删除数据

删除即把 Key 和 Value 都从 dict 结构里删除,使用命令 DEL scoredic 成功之后的结构会如下
image

1.6、过期键

过期键是存在 expires 字典上,假设上面例子的 Key 都设置了过期时间,那么其结构如下
这里的 dict 中和 expires 中 Key 对象,实际都是存储的 String 对象指针,所以并不是会重复占用内容,Redis 对内存的使用都是很珍惜的
image

2、Redis 单线程

Redis 多线程网络模型全面揭秘

核心处理逻辑,Redis 一直是单线程的
某些异步流程从 4.0 开始用多线程,如 UNLINK、FLUSHALL ASYNC 等非阻塞操作
网络 I / O 解包(接收客户端请求并解析命令)回包(将命令执行结果发送回客户端)从 6.0 开始用的都是多线程
瓶颈在 I / O 不是 CPU,这种情况下,选择多线程成本和复杂性高,综合投入产出比,所以选择了单线程

Redis 单线程性能:2 核 8G 机器,读 10w/s 左右、写 7 ~ 8w/s

  • 内存数据库:内存操作本身就很快
  • 高效的数据结构:很多对象,底层有多种实现,以应对不同的场景,追求性能的极致
  • 多路复用:使其在网络 I / O 操作中,能并发处理大量的客户端请求,实现高吞吐量

image

  • 在 Redis 6.0 之前的版本中,Redis 是单线程的,所有的命令处理、事件循环、网络通信都是由一个主线程顺序执行
    这种设计简化了并发控制,因为 Redis 的所有操作都是原子性的,不需要考虑并发问题
    然而随着 Redis 在企业级应用中的广泛使用,单线程模型在处理大量并发请求时可能会成为瓶颈
  • 为了提高 Redis 在处理大量并发连接时的性能,尤其是在高并发的读写操作场景下,Redis 6.0 引入多线程,主要用于处理 IO 操作
    包括解包(接收客户端请求并解析命令)和回包(将命令执行结果发送回客户端),这允许 Redis 在多个核心上并行处理网络 IO,从而提高吞吐量

3、内存淘汰

maxmemory <bytes>

使用 maxmemory 配置,默认是被注释掉的,也就是默认值是 0

  • 在 32 位操作系统中,maxmemory 的默认值是 3G,因为 32 位的机器最大只支持 4GB 的内存,而系统本身就需要一定的内存资源来支持运行,默认 3G 相对合理
  • 现在的机器基本都是 64 位机器,面试重点提到的也是 64 位机器下的情况,64 位机器不会限制内存的使用

3.1、内存淘汰策略

每次进行读写的时候,都会去检查是否需要释放内存,如果需要则会触发

  • noeviction:默认就是这种策略,此时如果内存达到 maxmemory,则写入操作会失败,但不会淘汰已有数据
  • 多种淘汰策略,主要支持 LRU、LFU、RANDOM、TTL 这几个方式
    • lru:根据 LRU(Least Recently Used 最近最久未使用)算法尝试回收最长时间未使用的
    • Ifu:根据 LFU(Least Frequently Used)驱逐最不常用的键,Ifu 是在 4.0 引入的
    • random:回收随机的键使得新添加的数据有空间存放
    • TTL:回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放
    • 这四种策略,可以选择是 volatile,也就是设置了过期时间的 Key,或者是 allkeys,即全部的 Key,所以一共有 8 种淘汰方式

image

3.2、LRU

最近最久未使用,即记录每个 Key 的最近访问时间,维护一个访问时间的数据(为所有数据维护一个顺序列表,实际就是做一个双向链表)
但是如果 Redis 数据稍微多些,这个链表就是巨大的成本,对于 Redis 而言内存是最宝贵的,所以 Redis 选择了近似 LRU 算法

维护一个全局链表,对 Redis 来说是巨大的成本,所以 Redis 选择采样的方式来做,也就是近似 LRU 算法

  • 在 LRU 模式,redisObject 对象中 lru 字段存储的是 key 被访问时 Redis 的时钟 server.lruclock
    当 key 被访问的时候 Redis 会更新这个 key 的 redisObject 的 lru 字段
    注意:Redis 为了保证核心单线程服务性能,缓存了 Unix 操作系统时钟,默认每 100 毫秒更新一次,缓存的值是 Unix 时间戳取模 2 ^ 24
    近似 LRU 算法在现有数据结构的基础上,采用随机采样的方式来淘汰元素,当内存不足时,就执行一次近似 LRU 算法
    具体步骤是随机采样 n 个 key,这个采样个数默认为 5,然后根据时间戳淘汰掉最旧的那个 key,如果淘汰后内存还是不足,就继续随机采样来淘汰
  • 采样范围:Redis 可以选择范围策略,allkeys 所有 key 中随机采样,volatile 从有过期时间的 key 随机采样,分别对应 allkeys-lru、volatitle-lru
  • 近似 LRU 优点在于节约了内存,缺点就是随机采样得到的结果,其实不是全局真正的最久未访问
  • Redis 3.0 对近似 LRU 算法进行了一些优化
    新算法会维护一个大小为 16 的淘汰池,池中的数据根据访问时间进行排序
    第一次随机选取的 key 都会放入池中,然后淘汰掉最久未访问的,比如第一次选了 5 个,淘汰了 1 个,剩下 4 个继续留在池子里
    随后每次随机选取的 key 只有活性比池子里活性最小的 key 还小时才会放入池中,当池子装满了,如果有新的 key 需要放入,则将池中活性最大的 key 移除
    通过池子存储,在池子里的数据会越来越接近真实的活性最低,所以其表现也会非常接近真正的 LRU

3.3、LFU

LFU 淘汰算法,即 Least Frequently Used 最不频繁淘汰算法,优先淘汰活跃最低、使用频率最低的

LRU 的不足

LRU 本身已经能解决大部分问题,但是脱离频率,只谈最近访问,在部分场景是得不到我们希望的结果
如下所示,key niuniu 频率很高,key mart 虽然是最近访问的,但是实际频率低(我们假设没有其他 key 的干扰)
如果内存满了,会淘汰 key niuniu,但如果用 LFU 的话,会淘汰 key mart,可以保留率较高的 key niuniu
希望按访问频率来进行淘汰,这其实是一个很正常的需求,LFU 就是专门做这个事的
image

LFU 结构

image

typedef struct redisobject {
    unsigned type: 4;
    unsigned encoding: 4;
    unsigned lru: LRU_BITS; /* LRU time (relative to global lru clock) or
                             * LFU data (least significant 8 bits frequency
                             * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;
  • 如果用 LRU,那么 redisObject 中 lru 字段,就是用来存储最近访问时间的,这个字段长度是 LRU_BITS,这个值一直都是 24 位
  • 如果用 LFU,因为 LRU、LFU 是不会同时开启的,所以两者可以说是互斥,基于这个情况,加上节约内存的考虑
    Redis 在 LFU 策略下复用 lru 字段,还是用它来表示 LFU 的信息
    不过将 24 拆解:高 16 bit 存储 ldt(Last Decrement Time)、低 8 bit 存储 logc(Logistic Counter)

LFU 如何淘汰

访问计数代表活性大小,淘汰访问计数最小的

  • 更新:每次访问时,更新当前时间 + 访问计数
  • 访问计数衰减:访问计数大小 0 ~ 255,相对于上次访问,一定有时间间隔,根据间隔来计算应该减少的次数
  • 一定概率增加访问计数:次数不足 5 次那一定会增加,如果 5 < 次数 < 255,会一定概率增加 1,且次数越大增加的难度越大
    可以通过 lfu-log-factor 参数来调节难度,它的值越大难度就越大,如果为 0,那么每次必然 + 1,很快就能 255,默认是 10,需要 1M 流量才能达到最大值

image

posted @ 2023-10-04 19:17  lidongdongdong~  阅读(5)  评论(0编辑  收藏  举报