这是Redis官网提供的性能基准测试,Y轴是QPS,X轴是连接数。我们可以看到连接数小于10000时,QPS可每秒达到10-12万左右。
Redis 是基于内存操作的数据库,不论读写操作都是在内存上完成的,直接访问内存的速度远比访问磁盘的速度要快多个数量级。
由下图可知:内存访问一次大概120ns(微秒),SSD 硬盘访问一次50-150us(纳秒),如果按照访问一次150us来算,性能差距在1000倍。这是因为1us等于1000ns。所以,150微秒是150,000纳秒,而120纳秒只是150微秒的一千分之一。
Redis使用单线程的主要原因是为了避免多线程带来的复杂性和线程安全性的问题。
以下是一些原因:
虽然Redis使用单线程,但它通过非阻塞IO、事件驱动和多路复用等技术来实现高并发和高性能。因此,即使是单线程,Redis仍然能够处理大量的并发请求。当然,在某些特定场景下,如需要大量计算的情况下,单线程可能会成为性能瓶颈,但在大多数情况下,Redis的单线程模型能够提供高性能和稳定性。
Redis 内部使用文件事件处理器 file event handler ,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,将产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:
多个socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将产生事件的 socket 放入队列中排队,事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理。
首先,Redis 服务端进程初始化的时候,会将 server socket 的 AE_READABLE 事件与连接应答处理器关联。
客户端 socket01 向 Redis 进程的 server socket 请求建立连接,此时 server socket 会产生一个 AE_READABLE 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该 socket 压入队列中。文件事件分派器从队列中获取 socket,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的 AE_READABLE 事件与命令请求处理器关联。
假设此时客户端发送了一个 set key value 请求,此时 Redis 中的 socket01 会产生 AE_READABLE 事件,IO 多路复用程序将 socket01 压入队列,此时事件分派器从队列中获取到 socket01 产生的 AE_READABLE 事件,由于前面 socket01 的 AE_READABLE 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 key value 并在自己内存中完成 key value 的设置。操作完成后,它会将 socket01 的 AE_WRITABLE 事件与命令回复处理器关联。
如果此时客户端准备好接收返回结果了,那么 Redis 中的 socket01 会产生一个 AE_WRITABLE 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 ok ,之后解除 socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。
redis 6.0开始引入多线程
注意! Redis 6.0 之后的版本抛弃了单线程模型这一设计,原本使用单线程运行的 Redis 也开始选择性地使用多线程模型。
前面还在强调Redis 单线程模型的高效性,现在为什么又要引入多线程?这其实说明 Redis 在有些方面,单线程已经不具有优势了。因为读写网络的 Read/Write 系统调用在 Redis 执行期间占用了大部分 CPU 时间,如果把网络读写做成多线程的方式对性能会有很大提升。
Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程。 之所以这么设计是不想 Redis 因为多线程而变得复杂,需要去控制 key、lua、事务、LPUSH/LPOP 等等的并发问题。
非阻塞IO这一块知识点是直接引用互联网 Java 工程师进阶知识完全扫盲 - Doocs 技术社区,讲的实在是太棒了
字符串是 Redis 中最常用的数据结构,不过作者并没有使用 C 标准款的实现,而是自己实现了一套简单动态字符串(SDS)作为替代。
Redis 中定义动态字符串的结构:
struct sdshdr{
int len;//记录buf数组中已使用字节的数量,等于SDS所保存字符串的长度
int free;//记录buf数组中未使用字节的数量
char buf[];//字符数组,用于保存字符串
}
SDS相对C字符串的优点:
ZipList是List 、Hash、Sorted Set三种数据类型底层实现之一。
ZipList的结构:
struct ziplist<T> {
int32 zlbytes; // 整个压缩列表占用字节数
int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
int16 zllength; // 元素个数
T[] entries; // 元素内容列表,挨个挨个紧凑存储
int8 zlend; // 标志压缩列表的结束,值恒为0xFF
}
ZipList的特性:
ZipList的缺点:
插入和删除操作需要频繁的申请和释放内存,同时会发生内存拷贝,数据量大时内存拷贝开销较大。
在 3.2 以后的版本版本,Redis 又逐渐引入了QuickList数据结构来替代ZipList压缩列表。
redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用,这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
HashTable 是Set 、Hash、Sorted Set三种数据类型底层实现之一。
HashTable的特性:
Redis 的哈希表与 Java 中相似,也是基于 key 得到的哈希值计算桶下标,再采用拉链法解决冲突,并在装载因子超过预定值时自动扩容。
它的特殊之处在于,当扩容的时候,它会基于扩容后的大小创建一张新的哈希表,然后在访问旧表的时候,每次将访问到的桶中的链表转移到新表中。
在这个过程中,每次操作的时候都会先访问旧表,然后再访问新表,直到旧表的数据组件的全部转移到新表以后,旧表会被回收,只留下新表。
这个过程被称为渐进式哈希,它巧妙地避免的在一次操作中大批量的进行数据迁移,而是将其分摊到多次请求中。
SkipList是Sorted Set的底层实现结构之一。
SkipList的特性:
从该有序表中搜索元素 < 23, 43, 59 > ,需要比较的次数分别为 < 2, 4, 6 >,总共比较的次数为 2 + 4 + 6 = 12 次。有没有优化的算法吗? 链表是有序的,但不能使用二分查找。类似二叉搜索树,我们把一些节点提取出来,作为索引。得到如下结构:
这里我们把 < 14, 34, 50, 72 > 提取出来作为一级索引,这样搜索的时候就可以减少比较次数了。我们还可以再从一级索引提取一些元素出来,作为二级索引,变成如下结构:
这里元素不多,体现不出优势,如果元素足够多,这种索引结构就能体现出优势来了。