后端面试知识点总结 redis

数据库之Redis

Redis概述

Redis(Remote Dictionary Server),即远程字典服务。是一个开源的、使用C语言编写、支持网络、可基于内存亦可持久化的日志型、key-value数据库,并提供多种语言的API。

Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

Redis是单线程(因为全部数据在内存中,使用单线程可以取消线程切换时的上下文切换资源消耗)的,基于内存操作,CPU不是redis的性能瓶颈,他的瓶颈在于机器的内存和网络带宽。

Redis键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。

Redis中键值和值都是用SDS(Simple Dynamic String)存储。string同C语言一样,末尾保存一个’\0’,但是不计算在len中。

struct sdshdr{
	int len;		// 记录buf数组中已使用字节的数量,也就是SDS中字符串的长度
	int free;		// 记录未使用字节的数量
	char buf[];		// 字节数组
};

Redis特性

  • 多样的数据类型
  • 持久化
  • 集群
  • 事务

基本指令

select num	# 默认有16各数据库,num为0-15
keys *		# 查看数据库所有的key
flushdb		# 清空当前库
flushall	# 清空全部的数据库

重要指令

KEYS 	*				# 查看所有的key
SET 	_key _value		# 设置key
GET 	_key			# 获取key的value
EXISTS 	_key			# 判断key是否存在
MOVE 	_key _num		# 将指定key移动到num号数据库
EXPIRE 	_key _sec		# 设置key过期时间,单位是秒
TTL 	_key			# 查看key的剩余时间
TYPE	_key			# 查看key的类型

Redis连接方式

unix scoket(本机通信)

  • 基于网络协议栈的,是网络中不同主机之间的通讯,需要明确IP和端口。

tcp(网络通信)

  • 同一台主机内不同应用不同进程间的通讯,不需要基于网络协议,不需要打包拆包、计算校验和、维护序号和应
    答等,只是将应用层数据从一个进程拷贝到另一个进程,主要是基于文件系统的,它可以用于同一台主机上两个
    没有亲缘关系的进程,并且是全双工的,提供可靠消息传递(消息不丢失、不重复、不错乱)的IPC机制,效率
    会远高于tcp短连接。与Internet domain socket类似,需要知道是基于哪一个文件(相同的文件路径)来通
    信的。
    unix domain socket有2种工作模式一种是SOCK_STREAM,类似于TCP,可靠的字节流。另一种是SOCK_DGRAM,
    类似于UDP,不可靠的字节流。

如果您想尽快回答并且您的负载低于redis-server峰值性能,那么避免流水线操作可能是最佳选择.但是,如果您希望能够处理更高的吞吐量,那么您可以处理请求的管道.响应可能需要更长时间,但您可以在某些硬件上处理更多请求.

Redis和Memcached区别

Redis优点

  • 支持多种数据结构,如 string(字符串)、 list(双向链表)、dict(hash表)、set(集合)、zset(排序set)
  • 支持持久化操作,可以进行aof及rdb数据持久化到磁盘,从而进行数据备份或数据恢复等操作,较好的防止数据丢失的手段。
  • 支持通过Replication进行数据复制,通过master-slave机制,可以实时进行数据的同步复制,支持多级复制和增量复制,master-slave机制是Redis进行HA的重要手段。
  • 单线程请求,所有命令串行执行,并发情况下不需要考虑数据一致性问题。
  • 支持pub/sub消息订阅机制,可以用来进行消息订阅与通知。

Redis缺点

  • Redis只能使用单线程,性能受限于CPU性能,故单实例CPU最高才可能达到5-6wQPS每秒(取决于数据结构,数据大小以及服务器硬件性能,日常环境中QPS高峰大约在1-2w左右)。
  • 支持简单的事务需求,但业界使用场景很少,并不成熟,既是优点也是缺点。
  • Redis在string类型上会消耗较多内存,可以使用dict(hash表)压缩存储以降低内存耗用。

Memcached优点

  • Memcached可以利用多核优势,单实例吞吐量极高,可以达到几十万QPS(取决于key、value的字节大小以及服务器硬件性能,日常环境中QPS高峰大约在4-6w左右)。适用于最大程度扛量。
  • 支持直接配置为session handle。

Memcache缺点

  • 只支持简单的key/value数据结构,不像Redis可以支持丰富的数据类型。
  • 无法进行持久化,数据不能备份,只能用于缓存使用,且重启后数据全部丢失。
  • 无法进行数据同步,不能将MC中的数据迁移到其他MC实例中。
  • Memcached内存分配采用Slab Allocation机制管理内存,value大小分布差异较大时会造成内存利用率降低,并引发低利用率时依然出现踢出等问题。需要用户注重value设计。

区别

两者都是非关系型内存键值数据库,主要有以下不同:

  • 网络IO模型:Redis使用单线程的IO复用模型,自己封装了一个简单的AeEvent事件处理框架,主要实现了epoll,kqueue和select,对于单存只有IO操作来说,单线程可以将速度优势发挥到最大;Memcached是多线程,非阻塞IO复用的网络模型,分为监听主线程和worker子线程,主线程监听网络连接,接受请求后,将连接描述字pipe传递给worker线程,进行读写IO,网络层使用libevent封装的事件库,多线程模型可以发挥多核作用,但是引入了cache coherency和锁的问题。

  • **数据类型:**Redis支持五种不同的类型,可以更灵活的解决问题;Memcached仅支持字符串类型

  • **数据持久化:**Redis支持两种持久化策略(RDB快照和AOF日志);Memcached不支持持久化

  • **分布式:**Redis Cluster实现了对分布式的支持;Memcached不支持分布式,只能通过在客户端使用一致性哈希来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。

  • 内存管理机制:

    • Redis中,并不是所有的数据都一直在内存中,可以将一些很久没用的value交换到磁盘;Memcached的数据一直在内存中。
    • Memcached将内存分割为特定长度的块来存储数据,以解决内存碎片的问题,但是这种方式会导致内存使用率不高。

redis事件驱动详解

事件驱动数据结构
  • 事件循环结构体aeEventLoop:事件循环结构体维护 I/O 事件表,定时事件表和触发事件表。

    • 文件事件结构体:aefileevent
    • 时间事件结构体:aetimeevent
    • 触发事件结构体:aefiredevent
  • redis 的主函数中调用 initServer() 函数从而初始化事件循环中心(EventLoop),它的主要工作是在 aeCreateEventLoop() 中完成的。

事件驱动原理
  • 事件注册:文件 I/O 事件注册主要操作在 aeCreateFileEvent() 中完成。aeCreateFileEvent() 会根据文件描述符的数值大小在事件循环结构体的 I/O 事件表中取一个数据空间,利用系统提供的 I/O 多路复用技术监听感兴趣的 I/O 事件,并设置回调函数。

    • 对于不同版本的 I/O 多路复用,比如 epoll,select,kqueue 等,redis 有各自的版本,但接口统一,譬如 aeApiAddEvent(),会有多个版本的实现。
    • img
  • 准备监听的工作:redis 提供了 TCP 和 UNIX 域套接字两种工作方式。以 TCP 工作方式为例,listenPort() 创建绑定了套接字并启动了监听

  • 为监听的套接字注册事件:initServer() 为所有的监听套接字注册了读事件(读事件表示有新的连接到来),响应函数为 acceptTcpHandler() 或者 acceptUnixHandler()。

    • acceptCommonHandler() 主要工作就是:
      • 建立并保存服务端与客户端的连接信息,这些信息保存在一个 struct redisClient 结构体中;
      • 为与客户端连接的套接字注册读事件,相应的回调函数为 readQueryFromClient(),readQueryFromClient() 作用是从套接字读取数据,执行相应操作并回复客户端。
  • 进入事件循环:发生在 aeProcessEvents() 中:

    • 根据定时事件表计算需要等待的最短时间;
    • 调用 redis api aeApiPoll() 进入监听轮询,如果没有事件发生就会进入睡眠状态,其实就是 I/O 多路复用 select() epoll() 等的调用;
    • 有事件发生会被唤醒,处理已触发的 I/O 事件和定时事件。
  • aeApiPoll() 调用了 select() 进入了监听轮询。aeApiPoll() 的 tvp 参数是最小等待时间,它会被预先计算出来,它主要完成:

    • 拷贝读写的 fdset。select() 的调用会破坏传入的 fdset,实际上有两份 fdset,一份作为备份,另一份用作调用。每次调用 select() 之前都从备份中直接拷贝一份;
    • 调用 select();
    • 被唤醒后,检查 fdset 中的每一个文件描述符,并将可读或者可写的描述符记录到触发表当中。

    接下来的操作便是执行相应的回调函数,先处理 I/O 事件,再处理定时事件。

redis如何提供服务

详细过程
  • 初始化:redis 在启动做了一些初始化逻辑,比如配置文件读取(initserverconfig),数据中心初始化,网络通信模块初始化等(initserver),待所有初始化任务完毕后,便开始进入事件循环等待请求(aemain)。

  • redis 注册了回调函数 acceptTcpHandler(),当有新的连接到来时,这个函数会被回调

  • 获取客户端的数据:readQueryFromClient() 则是获取来自客户端的数据,接下来它会调用 processInputBuffer() 解析命令和执行命令,对于命令的执行,调用的是函数 processCommand()。

    • redis 首先根据客户端给出的命令字在命令表中查找对应的 c->cmd, 找到命令结构提之后直接调用相应的回调函数指针

Redis数据结构

SDS

简单动态字符串(simple dynamic string),redis中使用sds作为默认字符串。

struct sdshdr{
   
    // 1 bytes
	uint8_t len;		// 字符串长度
    // 1 bytes
	uint8_t free;		// buf数组中未使用的字节数量
	// 1 bytes
    unsigned char flags;
	char buf[];		// 字符串数组,最后有一个隐式的'\0'
};
与C字符串区别
  • 常数时间复杂度获取字符串长度
  • 不会发生缓冲区溢出,由于sds知道自己的字符串长度和剩余空间,所以当不足时可以自动扩展空间
  • 减少修改字符串时带来的内存重分配次数。
    • sds通过未使用空间解除了字符串长度和底层数组长度之间的关联;SDS通过未使用空间实现了空间预分配和惰性空间释放两种优化策略
    • 空间预分配:用于优化字符串增长操作,当sds需要扩展空间的时候,程序不仅会对sds分配修改所需要的空间,同时还会为sds分配额外的未使用空间。
      • 如果扩展之后空间小于1M,程序会分配和len属性大小相同的未使用空间,即len=free
      • 如果大于1M,程序会额外分配1M的未使用空间,即free=1M
    • 惰性空间释放:用于优化sds的缩短操作,当sds需要缩短空间的时候,程序不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将其存起来。
  • 二进制安全,C字符串只能保存文本数据,sds可以保存文本或者二进制数据,可以识别出’\0’
  • 兼容部分C字符串函数

链表list

双端,无环,带有表头指针和表尾指针,带链表长度计数器。

多态:链表节点使用void*指针来保存节点值,并且通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。

typedef struct listNode{
	struct listNode *pre;
	struct listNode *next;
	void* value;
};

typedef sturct list{
	listNode *head;
	listNode *tail;
	unsigned long len;
	void *(*dup) (void *ptr);		// 节点值复制函数
	void (*free) (void *ptr);		// 节点值释放函数
	int (*match) (void *ptr, void *key);	// 节点值对比函数
}list;

字典

dictht是一个散列表结构,使用拉链法解决哈希冲突。

// 一个哈希表结构,每个字典有两个这样的结构
typedef struct dictht{
	dictEntry **table;			// 哈希表数组
	unsigned long size;			// 哈希表大小
	unsigned long sizemask;		// 哈希表大小掩码,用于计算索引值,总是等于size-1
	unsigned long used;			// 该哈希表已有节点的数量
}dictht;

// 哈希表节点结构
typedef struct dictEntry{
	void *key;			// 键
	union{				// 值
		void *val;
		uint64_t u64;
		int64_t s64;
		double d;
	}v;
	struct dictEntry *next;	// 指向下一个哈希表节点,形成链表,用来解决键冲突
}dictEntry;

Redis的字典dict包含两个哈希表dictht,这是为了方便进行rehash操作,在扩容时,将其中一个dictht上的键值对rehash到另一个dictht上面,完成之后释放空间并交换两个dictht的角色。

typedef struct dict{
	dictType *type;			// 类型特定函数
	void *privdata;			// 私有数据
	dictht ht[2];			// 哈希表,一般只使用ht[0],ht[1]是用来rehash的
	long rehashidx;			// 记录rehash目前的进度,如果没有进行rehash他的值为-1
	unsigned long iterators;// 当前运行的迭代器的数量
}dict;
img
字典中的哈希算法
hash = dict->type->hashFunction(key);
index = hash & index->ht[0].sizemask;
字典解决键冲突(哈希冲突)

Redis的哈希表使用链地址法解决哈希冲突,每个哈希表节点上都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单项链表连接起来。

程序总是将新节点添加到链表的表头为止(复杂度为O(1)),排在其他已有节点的前面。

字典的rehash

当哈希表中键值对主键增多或者减少,为了让哈希表的负载因子维持在一个合理的范围内,当哈希表键值对数量太多或者太少,程序需要对哈希表的大小进行相应的扩展或者收缩,也就是rehash操作。

rehash步骤:

  • 为字典的ht[1]哈希表分配空间,空间大小取决于要执行的操作以及ht[0]当前包含的键值对数量。
    • 扩展操作:ht[1]的大小为第一个大于等于ht[0].used*2的2^n(会考虑redis是否正在bgsave,但是如果过多也会强制扩容)
    • 收缩操作:ht[1]的大小为第一个大于等于ht[0].used的2^n(缩容条件:小于10%,不会考虑是否正在besave)
  • 将保存在ht[0]中的所有键值对rehash到ht[1]上:rehash指重新计算键的哈希值和索引值,然后放置在ht[1]的指定位置上
  • 当ht[0]中的键值对迁移完成之后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]上创建一个空白哈希表。

rehash操作不是一次性完成的,而是采用渐进方式,这是为了避免一次性执行过多的rehash操作给服务器带来过大的负担。

渐进式rehash步骤:

  • 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
  • 在字典中维持一个索引计数器变量rehashidx,并将其设置为0,表示渐进式rehash开始
  • 在rehash期间,每次对字典进行添加、删除、查找或者更新操作时,会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1]上,之后将该值加1
  • 完成全部键值对rehash之后,程序将rehashidx属性值设置为-1,表示操作完成。

采用渐进式rehash会导致字典中的数据分散在两个dictht上,因此对字典的查找操作也需要到对应的dictht去执行。

添加操作之后保存到ht[1]中,可以保证ht[0]中的键值对只减不增。

跳跃列表

是有序集合的底层实现之一。是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。

跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。

Redis中只有一个地方用到了跳跃表:有序集合

跳跃表是基于多指针有序链表实现的,可以看成多个有序链表。

img

Redis跳跃表由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义,其中zskiplistNode结构用于表述跳跃表节点,zskiplist结构用于保存跳跃表节点的相关信息,比如节点的数量,以及指向表头结点和表尾节点的指针等等。

跳跃表节点
typedef struct zskiplistNode{
	// 层
	struct zskiplistLevel{
		// 前进指针
		struct zskipListNode *forward;
		// 跨度,用于计算一个元素的排名
		unsigned int span;
	}level[];
	// 后退指针
	struct zskiplistNode *backward;
	// 分值
	double score;
	// 成员对象
	robj *obj;
}zskiplistNode;
  • **层:**跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点的速度就越快。
    • 层数为每次创建一个新跳跃表节点时按照幂次定律(越大的数出现的概率越小)随机生成一个介于1和32之间的值。
  • 前进指针:每个层都有一个指向表尾方向的前进指针(level[i].forward),用于从表头向表尾方向访问节点。
  • 跨度:层的跨度用于记录两个节点之间的距离,两个节点之间的跨度越大,相距就越远;指向NULL的所有前进指针的跨度都为0,因为他们没有连向任何节点。
  • 后退指针:用于从表尾向表头访问节点,一次只能后退至前一个节点。
  • 分值和成员:分值是一个double浮点数,跳跃表中的所有节点按照分值从小到大排序;对象是一个指针,指向一个字符串对象,保存着一个SDS值。
跳跃表结构
typedef struct zskiplist{
	// 表头节点和表尾节点
	struct zskiplistNode *header, *tail;
	// 表中节点的数量
	unsigned long length;
	// 表中层数最大的节点的层数
	int level;
}zskiplist;
  • 头尾指针:通过这两个指针程序定位表头节点和表尾节点的复杂度为O(1)
  • length:节点个数
  • level:层数最大的节点的层数量,头节点层高不算在内。

在查找中,从上层指针开始查找,找到对应的区间之后再到下一层取查找。

与红黑树等平衡树相比,跳跃表具有以下优点:

  • 插入速度非常快速,因为不需要进行旋转等操作维护树的平衡性
  • 更容易实现
  • 支持无锁操作
查找过程

从当前的最高层开始,后继比待查关键字大,下移;比待查关键字小,右移。

整数集合(IntSet)

是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。

数据结构:intset.h/intset

typedef struct intset{
	uint32_t encoding;		// 元素的编码方式
	uint32_t length;
	int8_t contents[];		// 保存元素的数据
}intset;

压缩列表(ziplist)

是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么是长度比较短的字符串,那么redis就会使用压缩列表来做列表键的底层实现。

struct ziplist<T>{
	int32 zlbytes;		// 整个压缩列表字节数
	int32 zitail_offset;// 最后一个元素的偏移量
	int16 zllength;		// 元素个数
	T[] entries;		// 元素内容
	int8 zlend;			// 结束标志,恒为0xFF
};

struct entry{
    int<var> prevlen;	// 上一个entry的字节长度,如果长度小于254就用一个字节表示;如果超出就用5个字节表示
    int<var> encoding;	// 元素类型编码,通过前缀位识别具体存储的数据形式
    optional byte[] content;	// 元素内容
};

为了支持双向遍历,加入了最后一个元素的偏移量,同时在entry结构体中加入了上一个entry的字节长度。

增加元素时,因为ziplist都是紧凑存储,没有冗余空间意味着插入一个新元素都要调用realloc扩展内存。

紧凑链表(listpack)

对ziplist结构的改进,在存储空间上更加节省,结构上也比ziplist更精简。

struct listpack<T>{
	int32 total_bytes;	// 占用的总字节
	int16 size;			// 元素个数
	T[] entries;		// 紧凑排列的元素列表
	int8 end;			// 0xFF
};
struct lpentry{
    int<var> encoding;
    optional byte[] content;
    int<var> length;
};

因为在lpentry结构体的内部,length放置在了尾部,并且存储的是当前元素的长度,所以可以省去

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值