Redis底层数据结构
1.基本数据结构
对应关系
String - embstr + int
List - 双向链表 + zipList压缩链表
Hash - 字典 + zipList压缩链表
Set - 字典 + intset整形数组
ZSet - 数据少ZipList 数据多 字典+跳表
单线程模型
单线程模型
Redis客户端对服务端的每次调用都经历了发送命令,执行命令,返回结果三个过程。计算向存储侧移动,其中执行命令阶段,由于Redis是单线程来处理命令的,所有每一条到达服务端的命令不会立刻执行,所有的命令都会进入一个队列中,然后逐个被执行。并且多个客户端发送的命令的执行顺序是不确定的。但是可以确定的是不会有两条命令被同时执行,不会产生并发问题,这就是Redis的单线程基本模型。
为啥是单线程
redis处理的瓶颈在于网络IO和内存,而不是cpu,不需要多个cpu并发处理,减少线程切换带来的损耗
多路复用处理请求
多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉。当有一个或多个流有 I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。
a.非阻塞[忙轮询]:采用死循环方式轮询每一个流,如果有IO事件就处理,这样一个线程可以处理多个流,但效率不高,容易导致CPU空转。
b.Select代理(无差别轮询):可以观察多个流的IO事件,如果所有流都没有IO事件,则将线程进入阻塞状态,如果有一个或多个发生了IO事件,则唤醒线程去处理。但是会遍历所有的流,找出流需要处理的流。如果流个数为N,则时间复杂度为O(N)
c.Epoll代理:Select代理有一个缺点,线程在被唤醒后轮询所有的Stream,会存在无效操作。Epoll哪个流发生了I/O事件会通知处理线程,对流的操作都是有意义的,复杂度降低到了O(1)。
epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用B+树数据结构实现)。把原先的select/poll调用分成了3个部分:
1)调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
2)调用epoll_ctl向epoll对象中添加这100万个连接的套接字
3)调用epoll_wait收集发生的事件的连接实现上面场景只需要在进程启动时建立一个epoll对象,在需要的时候向epoll对象中添加或者删除连接。同时epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。
Redis为什么快
- 纯内存操作。
- 核心是基于非阻塞的IO多路复用机制。
- 数据结构简单,高效,还可以进行少量计算
- 单线程同时也避免了多线程的上下文频繁切换问题,预防了多线程可能产生的竞争问题。
- 底层使用C语言实现,一般来说,C 语言实现的程序"距离"操作系统更近,执行速度相对会更快。
Redis事务
本质上讲,是一批命令的集合,通过MULTI、EXEC、WATCH等一组命令的集合,所以不支持回滚,保证隔离性和一致性,不保证原子性
隔离性:通过 单线程来保证
持久性:持久化到文件中
一致性:可以保证
对于 Redis 的持久化、集群数据同步、异步删除等都是其他线程执行。
主从复制原理
- 建立连接:从库会和主库建立连接,从库执行 replicaof 并发送 psync 命令并告诉主库即将进行同步,主库确认回复后,主从库间就开始同步了。
- 主库同步数据给从库:master 执行 bgsave命令生成 RDB 文件,并将文件发送给从库,同时主库为每一个 slave 开辟一块 replication buffer 缓冲区记录从生成 RDB 文件开始收到的所有写命令。从库保存 RDB 并清空数据库再加载 RDB 数据到内存中。
- 发送 RDB 之后接收到的新写命令到从库:在生成 RDB 文件之后的写操作并没有记录到刚刚的 RDB 文件中,为了保证主从库数据的一致性,所以主库会在内存中使用一个叫 replication buffer 记录 RDB 文件生成后的所有写操作。并将里面的数据发送到 slave。
详解
- 1.ZipList是一种压缩列表,就是普通的双向队列实现,元素紧挨着,经过特殊的编码,占用空间小。
- 2.跳表
跳表为什么比红黑树好:
1.范围查找场景跳表更好
2.插入、删除节点更快,红黑树需要做旋转
3.跳表实现简单,可读性好
- 3.QuickList
是一种zipList结合list的结构
底层结构
1、简单动态字符串SDS
2、链表
3、字典
4、跳跃表
5、整数集合
6、压缩列表
7、快速列表
1.字符串SDS
free + len + buffer 组成,\0结尾
2.String存储形态
根据存储的类型不同,实际存储字符串的底层结构可能有int,raw,embstr,几种类型
如果String中的对象是整数值 - 使用int编码
String中的对象大于32字节 - 使用raw编码
String中的对象小于32字节 - 使用embstr编码
raw:redisObject和SDS分开存储
embstr:空间连续存储
3.List列表对象
在redis的早期版本中,列表对象使用的编码是ziplist或linkedlist。
但是现在使用的是快速列表(quicklist)
4.Hash哈希对象
哈希对象的底层编码是ziplist或者hashtable(字典)
当哈希对象保存的所有键对值的键和值的长度都是小于64字节并且键对值数量小于512个的时候,使用ziplist。
保存键对值的时候,现将键压至栈底,再将值压至栈底。
存储的元素比较多的时候,使用hashtable
5.Set集合对象
集合对象可使用的编码是intset或hashtable
当集合中所有元素都是整数并且元素数量小于512个,intset底层是使用整数集合实现的。
当不满足用intset的条件的时候,使用hashtable
6.Zset有序集合
有序结合对象使用的是ziplist或者是skiplist
当有序集合中元素小于128个并且所有元素的长度都小于64字节,使用ziplist,ziplist保存的方式也是先保存键,再保存值,键和值是挨着的,元素是按照值由小变大排序的。
当不满足ziplist的两个条件的时候,使用的是skiplist,skiplist底层是zset结构,包含一个字典和一个跳跃表。
服务器在执行某个命令的时候,会先根据redisObject里的type属性判断是否可以执行指定的命令。
Redis使用引用计数实现内存回收机制(在JVM垃圾回收的时候说过,此机制不能解决循环引用的问题,所以JVM不用此机制)。