Redis 真的是单线程吗?
Redis 的单线程是指在处理客户端的请求时包括获取(socket 读) 、解析、执行、内容返回(socket 写) 是由一个顺序串行的线程处理的,这就是所谓的单线程。
但是 Redis 的其他功能,比如持久化、异步删除、集群数据同步等等,其实是由额外的线程执行的。 Redis 的工作线程是单线程的,但是,整个 Redis 是多线程的。
Redis 单线程依然很快的原因?
- 集于内存操作
- 数据结构简单
- 多路复用和非阻塞 I/O
- 避免上下文切换
Redis 单线程存在的问题?
del 指令 ,正常情况下使用 del 指令可以很快的删除数据,但是当被删除的 Key 是一个非常大的对象的时候,使用 del 指令会造成 Redis 主线程卡顿
整数数组和压缩列表在查找时间复杂度上并没有很大的优化,为什么 Redis 会把它们作为底层数据结构?
我们知道 Redis 是内存数据库,内存空间是很宝贵的资源。数组和压缩列表都是非常紧凑的数据结构,它们占用的内存比列表小的多,有利于在有限的空间内存储更多的数据量
数组对 CPU 高速缓存支持更友好,当数据元素超过阈值时,会转为 hash 和跳表,保证查询效率
缓存雪崩、击穿、穿透
缓存雪崩
同一时间或短时间内,大批量的缓存失效,所有的请求直接打到 Mysql 中,导致数据库宕机。
解决方法:给每个 Key 的失效时间加个随机值 or 热点数据设置永不过期 or 布隆过滤器
缓存穿透
查询数据库中不存在的值,导致每次请求都直接打到 mysql 中
缓存击穿
缓存击穿跟缓存雪崩有点相似,都是 Key 失效,导致请求打到 mysql 中。
但是又有点不太一样,缓存雪崩是大批量,缓存击穿可能是某个非常非常热点的 Key,在不停的扛着大并发,当这个 Key 突然失效时,所有的请求直接打到 Mysql 中。有点像一个完好无损的桶被凿开了一个洞
Redis 持久化
Redis 提供了 RDB 和 AOF 两种持久化方式, RBD 是把内存中的数据集以快照形式写入磁盘,实际操作是通过 fork 子进程执行,采用二进制压缩存储; AOF 是以文本日志的形式记录 Redis 处理的每一个写入或删除操作。
RDB 把整个 Redis 的数据保存在单一文件中,比较适合用来做灾备,缺点是快照保存完成之前宕机,这段时间的数据就会丢失,另外保存快照可能会导致服务短时间不可用。
AOF 对日志文件的写入操作使用追加模式,有灵活的同步策略,支持每秒同步、每次修改同步、不同步,缺点是相同规模的数据集,AOF 要大于 RDB, AOF 在运行效率上往往慢于 RDB。
Redis 底层数据结构
SDS
zipList
ziplist 数据结构
- zlbytes (4 bytes)存储的是整个 ziplist 所占用的内存字节数
- zltail (4 bytes)存储的是最后一个 entry 的偏移量,用来定位最后一个元素
- zllen (2bytes)指的是 entry 的数量,如果 entry 的数量小于 65535,那么就表示 entry 真实的数量,若等于或超过 65535,那么需要遍历一下才能知道 entry 的数量
- zlend (1bytes)是一个终止字符,其值为全 F ,ziplist 保证首字符不会是 255
QuickList
IntSet
Redis rehash
Redis 对字典的哈系表进行 rehash 的步骤如下:
-
为字典的 ht[1] 哈希表分配空间,这个哈希表的空间大小取决与要执行的操作以及 ht[0] 当前包含的键值对数量(ht[0].used 属性的值)
- 执行的是扩展操作:ht[1] 的大小为第一个大于等于 ht[0].used * 2 的 2 n
- 执行的是收缩操作:ht[1]的大小为第一个大于等于 ht[0].used 的 2n
-
将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上,rehash 是指重新计算键的哈希值和索引值,然后将键值对放置到 ht[1] 哈希表的指定位置上
-
当 ht[0] 包含的所有键值对都迁移到 ht[1] 之后,释放 ht[0],将 ht[1] 设置为 ht[0],并在 ht[1] 新创建一个空白哈希表,为下一次 rehash 做准备
Redis 渐进式 rehash
- 为 ht[1] 分配空间,字典同时持有 ht[0] 和 ht[1] 两个哈希表
- 字典中维持一个索引计数器 rehashidx ,将 rehashidx 设置为 0,表示 rehash 工作正式开始
- rehash 进行期间,每次对字典执行增加、删除、更新、查询操作时,会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] ,当前 rehash 工作完成后,程序会将 rehashidx 的值加一
- 随着字典操作的不断进行,最终在某个时间节点上,ht[0] 中的所有键值对会被 rehash 到 ht[1] 哈希表中,此时程序会将 rehashidx 设置为 -1,表示 rehash 操作完成。
在渐进式 rehash 执行期间,
跳表底层实现
跳表中有两个重要的数据结构,分别是 zskiplistNode 和 zskiplist
// 跳表节点的结构
typedef struct zskiplistNode {
// member 对象
robj *obj;
// 分值
double score;
// 后退指针
struct zskiplistNode *backward;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 这个层跨越的节点数量
unsigned int span;
} level[];
} zskiplistNode;
- level
level 是一个数组,一个跳表节点可以有多个层,每一个层都包括一个指向其他节点的 forward 指针。
每次创建一个跳表节点的时候,程序会随机生成一个介于 1 到 32 之间的随机数作为 level 数组的大小
- forward 指针
每个层都有一个指向表尾方向的前进指针(表尾方向,并不是表尾)
- span
层的跨度,用于记录两个节点之间的距离
两个节点之间的跨度越大,它们相距越远
// 跳表结构
typedef struct zskiplist {
// 头节点,尾节点
struct zskiplistNode *header, *tail;
// 节点数量
unsigned long length;
// 目前表内节点的最大层数
int level;
} zskiplist;
- header
指向跳表的表头指针
- tail
指向跳表的表尾指针
- length
跳表的节点个数,不包括表头节点
- level
跳表中最大的层,不包括表头节点中的层高