这里写目录标题
Redis
1、Redis 为什么这么快
- 因为它是内存数据库,所有操作都在内存上完成,内存的访问速度本身就很快。
- Redis 键值对是按一定的数据结构来组织的,操作键值对最终就是对数据结构进行增删改查操作,所以说 Redis 能快速处理数据
- Redis 使用异步机制来执行一些并不关键的操作,不关键的操作一般就是不许要 Redis 返回数据结果的操作;像我们的删除操作;还有一些阻塞式的操作,比如:bigkey 删除,清空数据库都可以异步来执行
Redis 主线程在启动后,会创建三个子线程来负责执行删除操作、写AOF日志、文件的关闭;他会把接收到的操作封装成任务,然后放到一个任务队列里;拿着这个任务队列和子线程交互;比如说,我发出了一个删除操作,然后Redis会把这个操作封装成一个任务放到任务队列中,然后返回一个完成信息,但其实这个时候删除操作并没有完成,等到后台的子进程从任务队列中把这个任务取出来,才会开始执行删除操作;
补充(别读):
有两个阻塞是没有办法异步的:集合全量查询和聚合操作、从库加载 RDB 文件,有各自的技巧
- 集合全量查询和聚合操作:可以使用 SCAN 命令,分批读取数据,再在客户端进行聚合计算;
- 从库加载 RDB 文件:把主库的数据量大小控制在 2~4GB 左右,以保证 RDB 文件能以较快的速度加载。
2、Redis 是单线程的吗
Redis 的网络 IO 和键值对读写是由一个线程来完成的;
但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。
为什么用单线程?
一方面单线程的程序可以避免线程上下文切换带来的性能损耗;另一方面,单线程程序不存在临界资源,不会出现锁以及并发竞争的情况
为什么单线程能这么快?
- 基于内存操作;
- 定制化的数据结构,大部分命令的时间复杂度是O(1);
- 一方面单线程的程序可以避免线程上下文切换带来的性能损耗;另一方面,单线程程序不存在临界资源,不会出现锁以及并发竞争的情况;
- 采用了IO多路复用技术,可以高效的处理多个客户端的请求。
3、Redis 全局哈希
Redis 使用一个哈希表来保存所有键值对;每一个哈希桶位都保存了指向具体值的指针,不管值是 String,还是集合类型,哈希桶中的元素都是指向它们的指针;这样做的好处就是可以用 O(1) 的时间复杂度来快速查找到键值对
存在两个问题就是哈希表的冲突问题和 rehash 可能带来的操作阻塞
全局哈希表使用拉链法来解决哈希冲突;但是当哈希冲突剧烈的时候,哈希冲突链会过长,会影响我们查找效率,这个时候会有 rehash 操作,来增加数组的桶位,减少单个桶位的哈希冲突
Redis 的 rehash 操作是渐进式的,并且使用了两个全局哈希表来完成;当我们添加数据的时候,是往第一个哈希表添加,第二个哈希表只有当我们 rehash 的时候才会创建;rehash分为三步:
-
给第二个哈希表分配一个更大的空间
-
把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
第二步涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都迁移完,会造成 Redis 线程阻塞,无法服务其他请求;Redis的策略就是把一次性大量拷贝的开销,分摊到了多次处理请求的过程中
第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries;
-
释放哈希表 1 的空间,留作下一次 rehash 扩容备用
4、Redis 数据类型
Redis 有五种基本数据类型;分别是:String、List、Hash、Sort set、Set
4.1、String(sds来实现)
Redis 是基于 C 语言开发的,但是它并没有复用 C 语言中对于字符串的实现,也就是使用 char* 字符数组来实现字符串,而是对 char* 字符数组进行了封装,既最大程度上复用了 C 语言的函数,又保证了 Redis 的速度
redis 中所有 key 的类型就是 sds,包括一些缓冲区也是 sds
char* 数组的缺陷:
- char * 字符数组实现字符串的方式很简单,就是一块连续的内存空间,依次存放了字符串中的每一个字符; char * 指针只是指向字符数组的起始位置,字符数组的结尾位置就用 “\0” 表示,这样就涉及到一个问题,如果我们要存储方式的数据中本身就有 “\0”,就会发生数据的截断
- C 语言中一些对于字符串进行操作的函数,比如说返回字符串长度值和追加这两个函数都需要遍历整个字符串,直到遇到结束字符;特别是追加函数,不仅需要遍历,还可能需要动态分配内存空间,复杂度就很高
sds 封装了以下几个属性:
- 字符数组:buf[]
- 字符串现有长度:len
- 分配给字符数组的空间长度:alloc
- sds类型:flag
SDS 结构中记录了字符数组已占用的空间和被分配的空间,效率肯定就会高很多,而且sds还把空间检查和扩容封成了一个函数,只要是涉及空间变化,像追加,就会直接调用这个函数
sds 对于内存方面的优化:
sds 封装了一个属性,flag,用来表示 sds 的类型;主要有 sds8、sds16、sds32、sds64,这4种数据类型,它们的区别在于,每一种 sds 中,len 和 alloc 这两个属性占用的空间不一样,sds8 的 len 和 alloc 两个属性占1个字节,这样做的目的就是在保存不同大小的字符串的时候,节省结构头占用的内存空间
sds 从编译方面也做了一些优化:
在编译的时候,采用紧凑的方式分配内存,实际占用多少内存空间,编译器就分配多少空间
String 类型的实现,底层对应 3 种数据结构:
- embstr:小于 44 字节,嵌入式存储,redisObject 和 SDS 一起分配内存,只分配 1 次内存
- rawstr:大于 44 字节,redisObject 和 SDS 分开存储,需分配 2 次内存
- long:整数存储(小于 10000,使用共享对象池存储,但有个前提:Redis 没有设置淘汰策略,详见 object.c 的 tryObjectEncoding 函数)
4.2、hash(dict来实现)
一个 hash 永远要面对两个问题,一个是哈希冲突,一个是rehash开销
Redis 使用 链式哈希和渐进式rehash 来解决这两个问题
链式哈希是一个老生常谈的东西了,问题的关键是当哈希冲突剧烈时,链表长度增长会影响性能,redis应该如何解决呢,redis 用 rehash 来解决
redis 如何实现 rehash:
redis 的 hash 实现,其实就是一个长度为2的数组,数组中的两个桶位都指向一个 hashmap,在平时的时候,所有的键值对都写入到下标为 0 的 hashmap 中,当进行 rehash 的时候,键值对迁移到下标为 1 的 hashmap 中;当迁移完成后,会把 h0 的空间释放,并把 h1 的地址赋值给 h0,h1 的表大小设置为 0
rehash 涉及三个关键问题:
什么时候触发 rehash?
rehash 扩容扩多大?
rehash 如何执行?
什么时候触发 rehash?
redis 把什么时候触发 rehash 封成了一个函数,这个函数会会根据 Hash 表的负载因子以及能否进行 rehash 的标识,判断是否进行 rehash,rehash 标识是由另外一个函数来维护的,会根据 RDB 和 AOF 的执行情况,启用或禁用 rehash
redis 把判断是否触发 rehash 封装成了一个函数,在这个函数里面定义了三个扩容条件:
- h0 的 hashmap 的大小为0,这个其实斌不是严格意义上的 rehash 触发条件,而是一个初始化条件
- h0 承载的元素个数已经超过 h0 的大小,并且这个时候 hash 表可以进行扩容
- h0 承载的元素个数,是 h0 大小的 5 倍
条件二、三其实就是判断负载因子是否大于等于 1 和是否大于 5;当大于 5 时,立刻进行扩容,当负载因子大于 1 小于 5 时,根据是否启用 rehash 来判断是否进行 rehash,是否启用 rehash 是用一个变量来维护的,当前没有 RDB 子进程,并且也没有 AOF 子进程,就设置为启用,有则反之
rehash 扩容多大?
扩容到原来的 2 倍
渐进式 rehash 如何实现?
为什么要实现渐进式 hash,当发生 rehash 时,很多键就需要从原来的位置拷贝到新的位置,在键拷贝时,Redis 主线程无法执行其他请求,会产生很大的 rehash 开销
Redis 使用渐进式 rehash 来降低 rehash 开销,把很大块迁移数据的开销,平摊到多次小的操作中,来降低主线程的性能影响;以桶位为粒度来做数据迁移,每迁移完一个桶位,hash 表就会执行正常的增删请求操作
redisObject
redis 采用 key-value 来存储数据,key 一般为 sds,value 类型则为 redisObject 对象
redis 为了加快读写速度和提供一种通用的表示方式,并不会直接用数据结构,而是会封装一层,叫 redisObject
redisObject 更像是连接「上层数据类型」和「底层数据结构」之间的桥梁。
redisObject 的结构为:
- type:redisObject 封装的数据结构的类型
- encoding:封装的数据结构的编码类型
- lru:lru 时间
- refcount:redisObject 的引用计数
- ptr:指向值的指针
优点:
- 为多种数据类型提供统一的表示方式
- 同一种数据类型,底层可以对应不同实现,节省内存
- 支持对象共享和引用计数,共享对象存储一份,可多次使用,节省内存
嵌入式字符串
在创建普通字符串的时候,Redis 需要给 redisObject 和 SDS 都分配一次内存,带来了内存分配开销,同时也会导致内存碎片;
当要存储字符串很小时,这种设计显然是不划算的,所以