上海做oocl船的公司网站,网站app建设图片,设计公司网站欣赏,梅州市住房与城乡建设局网站一、Redis到底是多线程还是单线程 1、redis6.0版本之前的单线程#xff0c;是指网络请求I/O与数据的读写是由一个线程完成的#xff1b; 2、redis6.0版本升级成了多线程#xff0c;指的是在网络请求I/O阶段应用的多线程技术#xff1b;而键值对的读写还是由单线程完成的。所…一、Redis到底是多线程还是单线程 1、redis6.0版本之前的单线程是指网络请求I/O与数据的读写是由一个线程完成的 2、redis6.0版本升级成了多线程指的是在网络请求I/O阶段应用的多线程技术而键值对的读写还是由单线程完成的。所以redis多线程的模型依然是线程安全的。
二、Redis的数据存储结构 redis 存储结构
redis的存储结构从外层往内层依次是redisDb、dict、dictht、dictEntry。redis的Db默认情况下有16个每个redisDb内部包含一个dict的数据结构。redis的dict内部包含dictht的数组数组个数为2主要用于hash扩容使用。dictht内部包含dictEntry的数组可以理解就是hash的桶然后如果冲突通过挂链法解决 下面我们依次来看下每个存储对象的数据结构 redisDbRedis中存在“数据库”的概念该结构由redis.h中的redisDb定义。当Redis 服务器初始化时会预先分配 16 个数据库所有数据库保存到结构 redisServer 的一个成员 redisServer.db 数组中redisClient中存在一个名叫db的指针指向当前使用的数据库。 RedisDB结构体源码如下 DB
typedef struct redisDb {int id; //id是数据库序号为0-15默认Redis有16个数据库long avg_ttl; //存储的数据库对象的平均ttltime to live用于统计dict *dict; //存储数据库所有的key-valuedict *expires; //存储key的过期时间dict *blocking_keys;//blpop 存储阻塞key和客户端对象dict *ready_keys;//阻塞后push 响应阻塞客户端 存储阻塞后push的key和客户端对象dict *watched_keys;//存储watch监控的的key和客户端对象
} redisDb;每个DB维护了多个字典每个字段存储的内容不同主要的是存储了value和过期时间。我们看下字典的数据结构 字典
typedef struct dict {dictType *type; // 该字典对应的特定操作函数void *privdata; // 上述类型函数对应的可选参数dictht ht[2]; /* 两张哈希表存储键值对数据ht[0]为原生哈希表ht[1]为 rehash 哈希表 */long rehashidx; /*rehash标识 当等于-1时表示没有在rehash否则表示正在进行rehash操作存储的值表示hash表 ht[0]的rehash进行到哪个索引值(数组下标)*/int iterators; // 当前运行的迭代器数量
} dict;字典当中主要我们要注意两个字段一个是dictht这是一个hash数组但是容量只有2也就是只有两张hash表数组下标为0的是数据默认存储的位置如果没有扩容就不会触发rehash如果出发了rehash就会在数组下标1的位置记录rehash阶段的数据。第二个就是rehashidx这里是记录了当前rehash的标识因为redis是渐进式rehash的所以要有一个类似游标的字段来记录当前rehash到了哪里。 我们进入dictht来看下hash表的数据结构 hash表
typedef struct dictht {dictEntry **table; // 哈希表数组unsigned long size; // 哈希表数组的大小unsigned long sizemask; // 用于映射位置的掩码值永远等于(size-1)unsigned long used; // 哈希表已有节点的数量,包含next单链表数据
} dictht;这个结构比较的简单了主要是维护了一个hash数组真正存储数据地方我们要注意这里还维护了一个字段叫size这个很重要当我们要统计整个redis中的键的数量时就不需要全部遍历一遍了。 Hash表节点
typedef struct dictEntry {void *key; // 键union { // 值v的类型可以是以下4种类型void *val;uint64_t u64;int64_t s64;double d;} v;struct dictEntry *next; // 指向下一个哈希表节点形成单向链表 解决hash冲突
} dictEntry;表节点中的可以就是键值的指针值v的类型可以是四种类型 数据存储过程 以set为例作为说明过程如下 1、从redisDb当中找到dict每个db就一个dict而已。 2、从dict当中选择具体的dictht对象。 3、首先根据key计算hash桶的位置也就是index。 4、新建一个DictEntry对象用于保存key/value将新增的entry挂到dictht的table对应的hash桶当中每次保存到挂链的头部。 5、dictSetKey的宏保存key 6、dictSetVal的宏保存value 三、Redis的底层数据结构 我们都知道Redis提供了多种数据结构例如string、hash、list、set、zset等那么这些数据结构是如果在redis中存储的呢从第二个问题中我们可以得到如下这张图。 从全局来看Redis整个数据库是用字典来存储的就是dict那么这个字典其实就是一张hash表那么hash表主要由两个数据结构来组成一个是桶也可以理解成是一个数组列表然后每个桶下面是对应的entry数据entry数据中有key和valuekey默认都是字符串value就是各种各样的数据类型。
数据类型和底层数据结构 不同的数据类型在redis底层是怎么实现的呢我们看下这张图。
SDS C语言中的char类型的问题如下 1、获取字符串的长度要遍历所有的字符为O(n) 2、追加字符串的时候比如把hello 变成 hello world会重新分配内存地址会造成缓冲区溢出 3、字符串中不能含有\0字符串不能保存像图像、视频、音频这样的二进制数据 为此redis设计了新的数据类型SDS简单动态字符串string类型的实现。
// 3.0版本
struct sdshdr {int len; // 记录buf数组中已使用字节的数量int free; // 记录buf数组中未使用字节的数量char buf[]; // 字节数组用于保存字符串}
// 5.0版本
struct sdshdr {int len; // 记录buf数组中已使用字节的数量int alloc; // 分配给字节数组的大小char *flags; // 类型char buf[]; // 字节数组用于保存字符串} 1、len记录了字符串长度。这样获取字符串长度的时候只需要返回这个成员变量值就行时间复杂度只需要 O1。 2、alloc分配给字符数组的空间长度。这样在修改字符串的时候可以通过 alloc - len 计算出剩余的空间大小可以用来判断空间是否满足修改需求如果不满足的话就会自动将 SDS 的空间扩展至执行修改所需的大小然后才执行实际的修改操作所以使用 SDS 既不需要手动修改 SDS 的空间大小也不会出现前面所说的缓冲区溢出的问题。 3、flags用来表示不同类型的 SDS。一共设计了 5 种类型分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64后面在说明区别之处。 4、buf[]字符数组用来保存实际数据。不仅可以保存字符串也可以保存二进制数据。
List 同样的redis也自己实现了一套链表数据结构首先定义了一个listNode
typedef struct listNode {//前置节点struct listNode *prev;//后置节点struct listNode *next;//节点的值void *value;
} listNode;好像也没有什么特别就是前驱节点后置节点以及节点的值。为了更好的管理和统计Redis又抽象出来了一个List数据结构
typedef struct list {//链表头节点listNode *head;//链表尾节点listNode *tail;//节点值复制函数void *(*dup)(void *ptr);//节点值释放函数void (*free)(void *ptr);//节点值比较函数int (*match)(void *ptr, void *key);//链表节点数量unsigned long len;
} list;list 结构为链表提供了链表头指针 head、链表尾节点 tail、链表节点数量 len、以及可以自定义实现的 dup、free、match 函数。 压缩列表 list结构有一个最大的问题就是内存是不连续的无法利用CPU的缓存。所以就出现了压缩列表它被设计成一种内存紧凑型的数据结构占用一块连续的内存空间不仅可以利用 CPU 缓存而且会针对不同长度的数据进行相应编码这种方法可以有效地节省内存开销。 压缩列表由于存储尾节点的偏移量那么获取头尾节点都很简单但是还是有几个问题。 1、获取中间节点就需要像数组一样一个个遍历时间复杂度变成了O(N)。 2、插入数据的时候如果内存分配的不够就要重新分配。 3、由于每个entry中存储了上一个节点的长度默认是使用一个字节小于254)如果上一个节点的长度大于254就要使用5个字节此时会出现一个问题如果上一个节点的长度从小于254变成了大于254那么后续的所有节点就会出现联动效果都要重新分配内存地址如果列表很长就会极大的影响性能 此时为此Redis又推出了新的压缩列表新增设计了两种数据结构quicklistRedis 3.2 引入 和 listpackRedis 5.0 引入。这两种数据结构的设计目标就是尽可能地保持压缩列表节省内存的优势同时解决压缩列表的「连锁更新」的问题。 quicklist quicklist 就是「双向链表 压缩列表」组合因为一个 quicklist 就是一个链表而链表中的每个元素又是一个压缩列表。 在向 quicklist 添加一个元素的时候不会像普通的链表那样直接新建一个链表节点。而是会检查插入位置的压缩列表是否能容纳该元素如果能容纳就直接保存到 quicklistNode 结构里的压缩列表如果不能容纳才会新建一个新的 quicklistNode 结构。 listpack Redis 在 5.0 新设计一个数据结构叫 listpack目的是替代压缩列表它最大特点是 listpack 中每个节点不再包含前一个节点的长度了压缩列表每个节点正因为需要保存前一个节点的长度字段就会有连锁更新的隐患。 listpack 还是用一块连续的内存空间来紧凑地保存数据并且为了节省内存的开销listpack 节点会采用不同的编码方式保存不同大小的数据。 每个 listpack 节点结构主要包含三个方面内容 1、encoding定义该元素的编码类型会对不同长度的整数和字符串进行编码 2、data实际存放的数据 3、lenencodingdata的总长度 listpack 没有压缩列表中记录前一个节点长度的字段了listpack 只记录当前节点的长度当我们向 listpack 加入一个新元素的时候不会影响其他节点的长度字段的变化从而避免了压缩列表的连锁更新问题。 实际操作如下
127.0.0.1:6379 lpush list one
(integer) 1
127.0.0.1:6379 lpush list two
(integer) 2
127.0.0.1:6379 type two
none
127.0.0.1:6379 type list
list
127.0.0.1:6379 OBJECT encoding list
listpack
Set Set的底层使用的是hash和整数集合如果Set中存储的都是数字就使用整数集合。hash我们之前已经讲过了这里只看整数集合 整数集合 当一个 Set 对象只包含整数值元素且不相同时就会使用整数集这个数据结构作为底层实现。整数集合是有序的。
struct intset {//编码方式uint32_t encoding;//集合包含的元素数量uint32_t length;//保存元素的数组int8_t contents[];
} 如果 encoding 属性值为 INTSET_ENC_INT16那么 contents 就是一个 int16_t 类型的数组数组中每一个元素的类型都是 int16_t 如果 encoding 属性值为 INTSET_ENC_INT32那么 contents 就是一个 int32_t 类型的数组数组中每一个元素的类型都是 int32_t 如果 encoding 属性值为 INTSET_ENC_INT64那么 contents 就是一个 int64_t 类型的数组数组中每一个元素的类型都是 int64_t 实际操作如下
127.0.0.1:6379 sadd myset 1
(integer) 1
127.0.0.1:6379 sadd myset 2
(integer) 1
127.0.0.1:6379 type myset
set
127.0.0.1:6379 OBJECT encoding myset
intset
ZSet Redis 只有在 Zset 对象的底层实现用到了跳表跳表的优势是能支持平均 O(logN) 复杂度的节点查找。Zset 对象是唯一一个同时使用了两个数据结构来实现的 Redis 对象这两个数据结构一个是跳表一个是哈希表。这样的好处是既能进行高效的范围查询也能进行高效单点查询
typedef struct zset {dict *dict;zskiplist *zsl;
} zset;跳表 跳表就是增加了一张跳跃表这个跳跃表将存储的数据分成了很多个段。或者说分成了几个层这里有点像B树的结构。层级为3的跳表大致为 我们可以看到一共有三个头指针L0-L2。 L0层一共是5个节点也是全部的节点全量链表 L1层一共有3个节点分别是2,3,5 L2层一共有1个节点是3。 如果我们要找节点3从L2层就可以直接获得时间复杂度是O(1) 如果我们要找节点4从L2层只有节点3而4就是3的后置节点这样查找两次就可以找到 这种近似二分查找逻辑的存储模式可以保证跳表的查找复杂度是O(logN) 那么调表是如何实现多层级的呢我们来看下调表的数据结构。
typedef struct zskiplistNode {//Zset 对象的元素值sds ele;//元素权重值double score;//后向指针struct zskiplistNode *backward;//节点的level数组保存每层上的前向指针和跨度struct zskiplistLevel {struct zskiplistNode *forward;unsigned long span;} level[];
} zskiplistNode;可以看到主要就是依靠level数组来保存每层的数据。比如节点3中level数组就有三个元素分别是0级1级2级的层数据。
Hash Hash结构就是我们第二个问题说明的采用是dict、dictht、dictEntry三层这里就不多说了。注意当hash对象的所有键值对字符串长度小于64个字节或者键值对的数量小于512个时hash结构的数据存储类型依然是压缩列表。
127.0.0.1:6379 hset myhash user liubei
(integer) 1
127.0.0.1:6379 hset myhash user guanyu
(integer) 0
127.0.0.1:6379 hget myhash
(error) ERR wrong number of arguments for hget command
127.0.0.1:6379 hget myhash user
guanyu
127.0.0.1:6379 hset myhash pass 123
(integer) 1
127.0.0.1:6379 type myhash
hash
127.0.0.1:6379 OBJECT encoding myhash
listpack 当我们新增一个字段值超过了64个字节后
127.0.0.1:6379 hset myhash address Hash结构就是我们第二个问题说明的采用是dict、dictht、dictEntry三层这里就不多说了。注意当hash对象的所有键值对字符串长度小于64个字节或者键值对的数量小于512个时hash结构的数据存储类型依然是压缩列表C
127.0.0.1:6379 OBJECT encoding myhash
hashtableredis存储对象也一般使用hash
限于篇幅关于底层数据结构更加详细的内容请阅读这两篇文章为了拿捏 Redis 数据结构我画了 40 张图完整版-CSDN博客
源码篇--Redis 底层数据结构_redis 字符串存储结构-CSDN博客
四、渐进式扩容 为了实现从键到值的快速访问前面的章节已经讲到Redis使用了一张Hash表来快速地定位数据。哈希表让我们可以用 O(1) 的时间复杂度来快速查找到键值对——我们只需要计算键的哈希值就可以知道它所对应的哈希桶位置然后就可以访问相应的 entry 元素。
key-value 访问过程
通过 key 计算出对应的哈希值根据哈希值计算出对应的哈希桶索引根据索引找到对应 entry如果是哈希链则挨个查找到对应的 entryString 类型则 entry 中的 value 就是要查找的值集合类型则需要在 value 中进一步查找 那么就必定会出现hash冲突的问题解决问题的方法就是挂链表。同一个hash桶中的元素用一个链表来串起来这就是为什么dictEntry数据结构中有一个next指针。但是如果链表过长那么查询的效率就会降低此时就会触发redis的rehashrehash 也就是增加现有的哈希桶数量让逐渐增多的 entry 元素能在更多的桶之间分散保存减少单个桶中的元素数量从而减少单个桶中的冲突。 Rehash 一开始当你刚插入数据时默认使用哈希表 1此时的哈希表 2 并没有被分配空间。随着数据逐步增多Redis 开始执行 rehash这个过程分为三步
给哈希表 2 分配更大的空间例如是当前哈希表 1 大小的两倍把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中释放哈希表 1 的空间。 到此我们就可以从哈希表 1 切换到哈希表 2用增大的哈希表 2 保存更多数据而原来的哈希表 1 留作下一次 rehash 扩容备用。 渐进式Rehash rehash有一个问题就是如果数据量过大可能rehash的过程很长那么就会造成一段时间的服务不可用这是无法承受的。那么渐进式Rehash就出现了其实也比较容易理解一次时间太长就分散到多次。简单来说就是在第二步拷贝数据时Redis 仍然正常处理客户端请求。 1、每处理一个请求时从哈希表1中的第一个索引位置开始顺带着将这个索引位置上的所有entries拷贝到哈希表2中 2、等处理下一个请求的再顺带拷贝哈希表1中的下一个索引位置的entries。 因为在进行渐进式 rehash 的过程中 字典会同时使用 ht[0] 和 ht[1] 两个哈希表 所以在渐进式 rehash 进行期间 字典的删除delete、查找find、更新update等操作会在两个哈希表上进行 比如说 要在字典里面查找一个键的话 程序会先在 ht[0] 里面进行查找 如果没找到的话 就会继续到 ht[1] 里面进行查找 诸如此类。
另外 在渐进式 rehash 执行期间 新添加到字典的键值对一律会被保存到 ht[1] 里面 而 ht[0] 则不再进行任何添加操作 这一措施保证了 ht[0] 包含的键值对数量会只减不增 并随着 rehash 操作的执行而最终变成空表。 rehash 触发条件 rehash 的触发条件跟负载因子load factor有关系负载因子哈希表已保存节点的数量/哈希表的大小。触发 rehash 操作的条件主要有两个 1、当负载因子大于等于 1 并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令也就是没有执行 RDB 快照或没有进行 AOF 重写的时候就会进行 rehash 操作 2、当负载因子大于等于 5 时此时说明哈希冲突非常严重了不管有没有有在执行 RDB 快照或 AOF 重写都会强制进行 rehash 操作
五、架构演进 单机版的Redis 一个应用服务对应单个数据库(mysql)单机Redis这种架构比较简单应对普通的低并发低数据存储要求的应用足够了。 主从模式 一个master多个slave一主多从的模式主从节点直接通过psync命令来完成数据复制具体过程概括为
建立连接。从节点启动时通过配置文件中的信息连接到主节点并发送SYNC命令请求同步数据。数据同步。主节点在接收到SYNC命令后开始进行数据快照的创建RDB文件并通过psync命令将快照发送给从节点。从节点接收并加载这个快照从而获得与主节点的初始数据一致性。增量同步。初始同步完成后主节点开始将新的写入操作发送给从节点以保持从节点数据的实时性。从节点接收并应用这些写入命令。复制数据。主节点持续将写入命令发送给所有连接的从节点从节点接收到这些命令后将它们应用到本地数据集以维持与主节点的数据一致性。主从切换。如果主节点发生故障管理员可以手动或自动将一个从节点提升为新的主节点。其他从节点将连接到新的主节点以获取数据复制。部分重同步。如果从节点在断线后重新连接主节点主节点会根据复制偏移量和复制积压缓冲区决定是否执行部分重同步或全量重同步。部分重同步用于补偿从节点在断线期间丢失的数据。 详细可以参考- Redis 主从复制原理分析这篇就够了_redis主从复制原理-CSDN博客 哨兵模式 主从模式解决了Redis数据安全的问题一旦主节点挂了数据不会丢失启动一个从节点即可所以如果有两个或两个以上的从节点可以使用主从从的模式像链条一样后一个节点复制前一个节点的数据这样只需要把主节点后面连接的从节点升级为主节点即可。可以这样的切换还需要人工的干预同时还得时刻关注redis的状态。 哨兵模式就是在主从节点之外又新增加了一个角色这个哨兵节点顾名思义就是监控主节点的状态主节点如果挂了就自动的选择一个从节点将其升级为主节点但是哨兵也会有单节点的问题故而哨兵也做了集群哨兵之间通过共识算法进行统一操作。 关于共识算法可以参考笔者的另一篇文章面试题集中营—分布式共识算法-CSDN博客 Redis Cluster 哨兵模式有一个问题就是master节点只有一个也就是说对外提供服务的永远只有一个节点我们部署了一个主节点两个从节点三个哨兵节点却只有一个节点提供服务好像有点浪费资源的意思。那么能不能多部署几个master节点呢这就是Redis官方提供的Redis Cluster方案。 Redis Cluster 内置了哨兵逻辑无需再部署哨兵。对于客户端来说也不需要关心数据存储在哪一个master节点上因为我们使用的sdk内置了proxy逻辑。
六、数据备份与恢复
6.1 备份
RDB与AOF redis提供了两种备份方式。默认是RDBRedis DataBase的备份方式。 RDB只持久化某一时刻的数据快照到磁盘上创建一个子进程来做 AOF每一次写操作都持久到磁盘主线程写内存根据策略可以配置由主线程还是子线程进行数据持久化 1、RDB 采用二进制 数据压缩的方式写磁盘这样文件体积小数据恢复速度也快 2、AOF 记录的是每一次写命令数据最全但文件体积大数据恢复速度慢
AOF rewrite 由于 AOF 文件中记录的都是每一次写操作但对于同一个 key 可能会发生多次修改我们只保留最后一次被修改的值是不是也可以是的这就是我们经常听到的AOF rewrite我们可以对 AOF 文件定时 rewrite避免这个文件体积持续膨胀这样在恢复时就可以缩短恢复时间了。
混合持久化 当 AOF rewrite 时Redis 先以 RDB 格式在 AOF 文件中写入一个数据快照再把在这期间产生的每一个写命令追加到 AOF 文件中。因为 RDB 是二进制压缩写入的这样 AOF 文件体积就变得更小了。
6.2 恢复
RDB数据恢复 Redis启动后会读取RDB快照文件将数据从硬盘载入到内存根据数据量大小与结构和服务器性能不同通常将一个记录一千万个字符串类型键、大小为1GB的快照文件载入到内存中需花费2030秒钟。
AOF数据恢复 重新启动Redis后Redis会使用AOF文件来恢复数据因为AOF方式的持久化可能丢失的数据更少可以在redis.conf中通过appendonly参数开启Redis AOF全持久化模式。 七、Redis为什么这么快 1、所有操作都是在内存中进行没有磁盘的I/O开销键值对的读写都是纳秒级别的速度 2、单线程操作没有线程的切换造成的开销更不需要考虑多线程死锁的问题 3、使用了多路复用的机制提升了I/O的利用率 4、高效的数据存储结构。 5、渐进式rehash 参考
Redis底层数据结构_redisdb结构-CSDN博客
详解Redis内部数据结构——Dict_redis dict entry_ptr_mask的作用-CSDN博客为了拿捏 Redis 数据结构我画了 40 张图完整版-CSDN博客
16张图吃透 Redis 架构演进全过程_redis mysql 架构图 redis架构原理-CSDN博客
redis详解之数据备份与恢复_redis备份和恢复-CSDN博客