redis核心技术与实战系列1:基础篇A

本章主要讲redis的初级篇,主要讲了三个课题.
一是:从构建一个简单的simple kv类型的数据结构入手,怎样去推导出redis各个方面的应用性.有如下几个问题需要解决:1、需要存放什么类型的数据、2、对数据可提供什么操作、3、数据保存在哪里、4、如何查找数据、5、如何保障服务的高可用性?当你想通了简单simplekv的实操过程后,你就可以开始类比着探寻redis了.
二是:redis为什么快?有什么慢操作?主要从redis的底层数据结构的角度出发,包括了保存键值的全局哈希表结构,和支持集合类型的双向链表、压缩列表、整数数组、哈希表、跳表这五大底层结构.同时还详细讲了不同数据结构的工作原理、对比查询的时间复杂度,了解数据结构的实现原理,可以帮助你从根源上根据业务需求选择合适的数据结构.
三是继续承接上篇的问题:redis单线程指的是什么?为什么用单线程?单线程redis为什么这么快?从网络io的操作角度出发,多路复用的io模型避免了accept()和send()/recv()等潜在的网络io操作阻塞点.类似医院的分诊台一样帮助医生做一些会前的预先准备工作,避免大量事件阻塞在医生导致看病效率不高.

基础架构:一个键值数据库包含什么

学习最好的方式是先建立起“系统观”.若想深入理解和优化redis,必须对它的总体架构和关键模块有一个全局的认知,再深入到具体的知识点.

假设我们构建一个简单的simplekv组建,我们会如何下手?

1、可以存哪些数据?
  • memcache :string
  • redis:string、哈希表、列表、集合等.
    redis能在实际业务场景中得到广泛的应用,得益于支持多样化类型的value.
2、可以对数据做什么操作?

PUT/GET/DELETE/SCAN

3、键值对保存在外存还是内存?
  • 内存:
    (1)读写快
    (2)掉电数据会丢失
  • 外存:
    (1)受限于磁盘的慢速读写,键值数据库整理性能降低
    (2)避免数据丢失

键值数据库基本包含访问框架、索引模块、操作模块、存储模块四个部分.

4、采用什么模式访问?

(1)通过函数库调用的方式供外部应用使用(如上图libsimplev.so以动态链接库的形式链接到自己程序中,提供键值存储)
(2)通过网络框架以socket通信对外提供键值对操作

客户端发送命令后,该命令会被封装到网络包中发送给键值数据库:

PUT HELLO WORLD

数据库网络框架接到网络包,安装协议进行解析,直到客户端想写入一个键值对,开始实际写入流程.

这里会产生一个系统设计上的问题:
网络连接的处理、网络请求的解析、数据存取的处理、是一个线程、多个线程、还是多个进程?如何设计和取舍?
此乃I/O模型设计.

假设一个线程既处理网络连接、解析请求、又要完成数据存取,一旦某一步操作发发生阻塞,整个线程都会阻塞,降低系统响应速度
如果采用不同线程处理,线程间访问共享资源又回产生线程竞争,也会影响系统效率.两难选择,所以需要设计.

5、如何定位键值对的位置?

simplekv解析了客户端的请求,知道要查询操作的键值对是否存在.依赖于键值数据库的索引模块.索引的作用是根据key找到对应value存储的位置,进行操作.

索引类型常见哈希表、b+树、字典树.不同索引结构构在性能、空间消耗、并发控制不一.
memcache & redis 使用哈希表做索引,RocksDB使用跳表.

6、不同操作具体逻辑是怎样的?
  • GET/SCAN 根据value的存储位置返回value即可
  • PUT 为该新的键值对分配内存空间
  • DELETE 删除对应键值对并释放内存空间
7、如何实现重启后快速提供服务?

simplekv采用了常见的内存分配器glibc的malloc和free.
simplekv依赖内存保管数据,提供快速访问,但是也希望simplekv重启后能重新提供服务,故在simplekv存储模块加了持久话功能.
将键值数据通过本地文件系统的操作接口保存在磁盘上

  • 方法一:对每个键值对都做落盘保存,数据更可靠但是性能会受影响
  • 方法二:周期性保存键值对,避免频繁写盘操作的影响,存在丢失的风险.
小节:

simplekv和redis的对比,缺少的功能模块
[数据结构]缺乏广泛的数据支持,范围查询skiplist stream等等
[高可用]缺乏哨兵or master-slave模式的高可用
[横行扩展]缺乏集群和分片功能
[内存安全性]缺乏内存过载时的key淘汰算法
[内存利用率]未对数据结构优化提供内存利用率 如压缩性的数据结构
[不具备事务性]无法保证多个操作的原子性
[内存分配器]simplekv是glibc,redis更多

画外:
redis比memcache流行,表面上是数据结构更丰富,本质上是“计算向数据迁移”,要高性能、高可用、就需要快.memcache只支持string类型,需要获取数据必须得全量返回,返回体大,redis支持指定的数据,返回体小,标志着通过网卡的流量少,更符合redis的epoll的网络模型,尽量不阻塞.

二、快速的redis有哪些慢操作

Redis 的快是指它接收到一个键值对操作后,能以微秒级别的速度找到数据,并快速完成操作。

Redis 快的原因?
  • 内存数据库,所有操作都在内存上完成,内存的访问速度本身就很快。

  • 高效的数据结构

数据类型

String(字符串)、List(列表)、Hash(哈希)、Set(集合)、 Sorted Set(有序集合).

数据结构的底层实现

动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组。
数据类型的对应关系如下图所示:
在这里插入图片描述

String 类型的底层实现只有一种数据结构,即简单动态字符串。 List、Hash、Set 和 Sorted Set 这四种数据类型有两种底层实现结构。我们把这四种类型称为集合类型,特点是一个键对应了一个集合的数据

键和值用什么结构组织?

为了实现从键到值的快速访问,Redis 使用了一个哈希表来保存所有键值对。
一个哈希表就是一个数组,数组的每个元素称为一个哈希桶。一个哈希表是由多个哈希桶组成的,每个哈希桶中保存了键值对数据。
哈希桶中的元素保存的并不是值本身,而是指向具体值的指针。

哈希桶中的 entry 元素中保存了key和value指针,分别指向了实际的键和值,这样一来,即使值是一个集合,也可以通过*value指针被查找到。

因为这个哈希表保存了所有的键值对,所以,我也把它称为全局哈希表。
哈希表好处:
用 O(1) 的时间复杂度来快速查找到键值对只需要计算键的哈希值,就可找到所对应的哈希桶位置,访问相应的 entry 元素。

为什么哈希表操作变慢了?—哈希冲突

哈希冲突:
两个 key 的哈希值和哈希桶计算对应关系时,正好落在了同一个哈希桶中。

解决哈希冲突的方式—链式哈希
同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。我们可以通过 entry 元素中的指针,把它们连起来。这就形成了一个链表,也叫作哈希冲突链。

哈希链过长影响查询效率–rehash
哈希冲突链上的元素只能通过指针逐一查找操作。若哈希冲突链过长会导致链上的元素查找耗时长,效率降低。因此redis 引入rehash 操作.通过rehash增加现有的哈希桶数量.让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突**

Redis使用了两个全局哈希表:哈希表 1 和哈希表 2。一开始插入数据默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:

  1. 给哈希表 2 分配更大的空间,eg:当前哈希表 1 大小的两倍;
  2. 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
  3. 释放哈希表 1 的空间。

从哈希表 1 切换到哈希表 2,用增大的哈希表 2 保存更多数据,原来的哈希表 1 留作下一次 rehash 扩容备用。

步骤二中涉及大量的数据拷贝,一次性迁移所有数据会造成 Redis 线程阻塞,无法服务其他请求。因此Redis 采用了渐进式 rehash。即在拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。

巧妙地把一次性大量拷贝的开销分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。

集合数据操作效率

集合类型的值,第一步是通过全局哈希表找到对应的哈希桶位置,第二步是在集合中再增删改查。那么,集合的操作效率和哪些因素相关呢?

  • 与集合的底层数据结构有关。eg:使用哈希表实现的集合,要比使用链表实现的集合访问效率更高。
  • 操作本身的执行特点有关,eg:读写一个元素的操作要比读写所有元素的效率高。
有哪些底层数据结构?
  • 整数数组/双向链表:
    操作特征都是顺序读写,通过数组下标或者链表的指针逐个元素访问,操作复杂度基本是 O(N),操作效率比较低

  • 压缩列表:
    类似数组,但是在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。
    若是查找定位第一个/最后一个元素,可通过表头三个字段的长度直接定位,复杂度是 O(1)。查找其他元素时只能逐个查找,复杂度就是 O(N) .

  • 跳表:
    跳表在链表的基础上增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位.这个查找过程就是在多级索引上跳来跳去,最后定位到元素。这也正好符合“跳”表的叫法。当数据量很大时,跳表的查找复杂度就是 O(logN)。

不同操作的复杂度

口诀:

单元素操作是基础; 范围操作非常耗时; 统计操作通常高效; 例外情况只有几个。

单元素操作:每一种集合类型对单个数据实现的增删改查操作。eg:HGET、HSET 和 HDEL 是对哈希表做操作,所以复杂度都是 O(1);Set 类型用哈希表作为底层数据结构时,它的 SADD、SREM、SRANDMEMBER 复杂度也是 O(1)。

范围操作:集合类型中的遍历操作,eg:List 类型的 LRANGE 和 ZSet 类型的 ZRANGE。复杂度一般是 O(N),比较耗时,我们应该尽量避免。

统计操作:集合类型对集合中所有元素个数的记录,eg: LLEN 和 SCARD。操作复杂度只有 O(1),这是因为当集合类型采用压缩列表、双向链表、整数数组这些数据结构时,这些结构中专门记录了元素的个数统计,因此可以高效地完成相关操作。

例外情况:指某些数据结构的特殊记录,eg:压缩列表和双向链表都会记录表头和表尾的偏移量。 List 类型的 LPOP、RPOP、LPUSH、RPUSH 是在列表的头尾增删元素,这就可以通过偏移量直接定位,所以它们的复杂度也只有 O(1),可以实现快速操作。

小结
1、Redis 的底层数据结构=保存每个键和值的全局哈希表结构+支持集合类型实现的双向链表、压缩列表、整数数组、哈希表和跳表这五大底层结构。

三、高性能io模型:为什么单线程redis可以那么快

Redis的单线程是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。

Redis 为什么用单线程?
多线程的开销

多线程的好处:
多线程可以增加系统吞吐率,或是可以增加系统扩展性。对于一个多线程的系统来说,在有合理的资源分配的情况下,可以增加系统中处理请求操作的资源实体,进而提升系统能够同时处理的请求数,即吞吐率。

多线程的坏处:

  • 多线程编程模式面临的共享资源的并发访问控制问题。
    多线程同时访问的共享资源,为了保证共享资源的正确性,就需要有额外的机制进行保证,带来额外的开销。
  • 引入同步原语来保护共享资源的并发访问,这也会降低系统代码的易调试性和可维护性。
单线程 Redis 为什么那么快?

高性能的原因:

  1. Redis 大部分操作在内存上完成
  2. 采用了高效的数据结构
  3. Redis 采用了多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率。
基本 IO 模型与阻塞点

以 Get 请求为例,SimpleKV 为了处理一个 Get 请求,需要以下几个步骤:

  • 监听客户端请求(bind/listen)
  • 客户端建立连接(accept)
  • 从 socket 中读取请求(recv)
  • 解析客户端发送请求(parse
  • 根据请求类型读取键值数据(get)
  • 客户端返回结果,向 socket 中写回数据(send)
非阻塞模式

在 socket 模型中,不同操作调用后会返回不同的套接字类型。socket() 方法会返回主动套接字,然后调用 listen() 方法,将主动套接字转化为监听套接字,此时,可以监听来自客户端的连接请求。最后,调用 accept() 方法接收到达的客户端连接,并返回已连接套接字。

针对监听套接字,我们可以设置非阻塞模式:当 Redis 调用 accept() 但一直未有连接请求到达时,Redis 线程可以返回处理其他操作,而不用一直等待。

基于多路复用的高性能 I/O 模型

Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,即select/epoll 机制。在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
下图中的 FD 就是刚才所说的多个套接字。Redis 网络框架调用 epoll 机制,让内核监听这些套接字。此时,Redis 线程不会阻塞在某一个特定的监听或已连接套接字上,也就是说不会阻塞在某一个特定的客户端请求处理上。因此,Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。

为了在请求到达时能通知到 Redis 线程,select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。

这就像病人去医院瞧病。在医生实际诊断前,每个病人(等同于请求)都需要先分诊、测体温、登记等。如果这些工作都由医生来完成,医生的工作效率就会很低。所以医院都设置了分诊台,分诊台会一直处理这些诊断前的工作(类似于 Linux 内核监听请求),然后再转交给医生做实际诊断。这样即使一个医生(相当于 Redis 单线程),效率也能提升。

小结
1、Redis 真的只有单线程吗?
Redis 单线程是指它对网络 IO 和数据读写的操作采用了一个线程
2、为什么用单线程?
避免多线程开发带来的并发控制问题
3、单线程为什么这么快?
单线程的 Redis 也能获得高性能离不内存数据库、数据结构本身的高效性和多路复用的 IO 模型,避免了 accept() 和 send()/recv() 潜在的网络 IO 操作阻塞点。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值