redis-1
redis的IO模型
4大核心组建
- 多路复用
- 套接字队列
- 事件分派器
- 事件处理器 (请求处理器、连接处理器、回复处理器)
运行过程:
主线程监听套接字事件,把套接字丢给套接字队列,事件分派器从队列中获取套接字,丢给事件处理器处理。
(此处应有图)
6.0引入多线程模型
redis原IO模型的瓶颈在于从套接字中读取数据,因此在6.0版本中引入异步IO去解决这个问题。在新的多线程模型下,主线程在监听套接字事件后,找到一个IO线程去读取套接字数据,主线程根据事件找到对应的事件处理器,执行命令。在将响应写入套接字这个过程,也是交给IO线程去处理。总的来看,相当于有一个异步IO线程池,专门处理IO的读写,而主线程负责监听套接字和执行命令。
(此处应有图)
与memcache的IO模型对比
memcache的IO模型,本质上也是多路复用。与redis的IO模型不同的是,memcache的主线程只负责监听套接字,而读写IO和执行命令的工作,都交给worker线程来完成。
(此处应有图)
类似netty worker group
(此处应有图)
过期处理
- 定期删除
- 惰性删除
定期删除
redis定期遍历数据库,检查过期的key并删除。特点是随机检查,不会一次全部删除所有过期的key,而是在规定的事件内能删多少删多少,目的是为了平衡CPU的开销和内存的消耗。
关联G1的部分region回收,是为了停顿时间可控
惰性删除
在访问key的时候,检查key的过期时间,如果已经过期,则删除该key键值对。
过期处理时,RDB、AOF和主从复制下的表现
当redis开启RDB、AOF和主从复制时,redis的过期处理:
- 加载RDB时会忽略已过期的key-RBD不读
- 重写AOF时会忽略已过期的key-AOF不写
- 主从同步时,从服务器等待主服务器的删除命令-从服务器不处理
不同版本的过期处理策略
针对主从同步场景,在进行过期删除时,从服务器等待主服务器的删除指令;但在读过期数据时:
- 版本3.2之前,从服务器会返回过期key的值,仿佛key没过期
- 版本3.2之后,从服务器会返回NULL,和主服务器行为一致
版本3.2之前,从服务器读取过期key的处理,可以使用TTL命令来判断key有没有过期
redis在执行删除指令时,不会同步删除key,而是将key从keyspace中移除,然后将删除任务放入一个异步队列,最后由其他后台线程删除。但如果key键值对不是很大,删除时不需要很大的开销,此时就没必要使用异步队列去删除了,所以,redis会先计算删除key的开销,达到一定的阈值才会启用异步删除。
redis value的数据结构
SDS
特点:
- 存储了字符串长度,可以常量时耗获取字符串长度
- SDS采用了预分配和懒回收策略来分配内存,减少分配和回收内存的次数
hashtable
特点:
- 使用拉链法解决冲突
- 渐进式rehash
- murmurhash2算法
拉链法
redis的hashtable使用拉链法解决hash冲突,当发生冲突时,将元素插入链表头。
渐进式rehash
当扩容或缩容时需要进行rehash,为了避免集中式rehash会对服务器带来庞大的计算量从而导致响应延迟等,需要将rehash分为多次,渐进式的完成。
渐进式rehash是将rehash的计算工作平摊到每次的增删改查的执行中,是一种分而治之的思想,渐进式rehash的步骤:
- 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
- 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
- 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
- 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
murmurhash2
该算法效率高,随机性较好,减少发生冲突的几率。
ziplist
ziplist是Redis为了节省内存而自定义的一种特殊编码的顺序性数据结构。ziplist内存结构类似数组,是连续的内存空间,但存储的元素类型和大小不一定一致。ziplist通常用于有序并且大小较小数量较少的数据集合。
对ziplist进行增删查操作时,会涉及两个关键点:
- 元素移动
- 连锁更新
元素移动
因为ziplist是类似数组那样使用连续内存空间去存储,所以在增删元素时和数组有相似的表现:增删某一元素时,首先要从头开始遍历直到找到目标位置,在对目标位置元素进行改变后,其前后的元素也要相应的进行位移(增:往后位移,删:往前位移)。所以ziplist增删的时间复杂度是O(n)。
连锁更新
ziplist有个特点,每个元素会存储前一个元素的大小,这样可以实现反向遍历。但如果前一个节点发生长度变化时,当前元素就要改变这个值。所以最坏的情况是,一个元素的改动影响所有元素的改动,时间复杂度是O(n^2)。
ziplist特点
- 整个ziplist存在一个连续内存中
- 通过固定内存偏移量获取头节点,保留与尾节点的偏移量zltail,而不像双端链表存储头尾节点
- 每个节点保留前置节点的长度,从而实现反向遍历,而不像双端链表存储前置节点指针
- 每个节点采用变长的编码方式,存储字符串或整数值
- 在特定条件下,ziplist会发生连锁更新,导致增删操作时间复杂度由O(n)变为O(n^2)
intset
intset是一个数组结构,用于存储整数类型,里面的元素是唯一的。它可以存放16、32、64位的整数。如果元素位数变大,那么就会触发升级过程。例如原本存储的元素都是16位整数,现在插入一个32位的整数,那么 Redis 需要按照32位重新计算内存大小,并且分配内存,迁移原本的数据,而后将新数据插入。有一点需要注意的是,Redis并不支持降级。这是intset升级不降级的特性。
网友评论