1.什么是Redis?
Redis简介--了解Redis开源项目
Redis是一个开源(BSD 许可)的内存数据结构存储)用作数据库、 缓存、消息代理和流引擎。Redis提供数据结构,例如字符串、散列、列表、集合、带范围查询的排序集合、位图、超日志、地理空间索引和流。Redis 内置了复制、Lua 脚本、LRU 驱逐、事务和不同级别的磁盘持久性,并通过以下方式提供高可用性Redis Sentinel和Redis Cluster的自动分区。
您可以对这些类型运行原子操作例如附加到字符串;增加哈希值;将元素推入列表;计算集交、并、差;或获取排序集中排名最高的成员。
为了达到最佳性能,Redis 使用内存中的数据集。根据您的用例,Redis 可以通过定期将数据集转储到磁盘或将每个命令附加到基于磁盘的日志来持久化您的数据。如果您只需要- -个功能丰富的网络内存缓存,您也可以禁用持久性。
Redis支持异步复制,具有快速非阻塞同步和自动重新连接以及网络拆分上的部分重新同步。
Redis还包括:●交易
●发布/订阅
●Lua脚本
●生命周期有限的密钥
●LRU驱逐密钥
●自动故障转移Redis官方的介绍原版是英文的,翻译成中文后截图如上,有些文字读起来会比较拗口,没关系,接下.来会把里面比较重要的特性抽出来讲。
Redis是一-种基于内存的数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景。
Redis提供了多种数据类型来支持不同的业务场景,比如String(字符串)、Hash(哈希)、 List (列表).
Set(集合)、Zset(有序集合)、 Bitmaps (位图)、HyperLogLog (基数统计)、GEO (地理信息)Stream (流), 并且对数据类型的操作都是原子性的,因为执行命令由单线程负责的,不存在并发竞争的问题。
除此之外,Redis 还支持事务、持久化、Lua脚本、多种集群方案(主从复制模式、哨兵模式、切片
机群模式)、发布/订阅模式,内存淘汰机制、过期删除机制等等。
2. Redis和Memcached有什么区别?
很多人都说用Redis作为缓存,但是Memcached也是基于内存的数据库,为什么不选择它作为缓存
呢?要解答这个问题,我们就要弄清楚Redis和Memcached的区别。
Redis与Memcached共同点:
1.都是基于内存的数据库,- -般都用来当做缓存使用;
2.都有过期策略;
3.两者的性能都非常高;
Redis与Memcached区别;
●Redis支持的数据类型更丰富(String、 Hash. List. Set、 ZSet) ,而Memcached只支持最简单的key-value数据类型;
●Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用:
而Memcached没有持久化功能,数据全部存在内存之中,Memcached 重启或挂掉后,数据会丢
失;
●Redis原生支持集群模式,Memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;
●Redis支持发布订阅模型、Lua脚本、事务等功能,而Memcached不支持。
3.为什么用Redis作为MySQL的缓存?
主要是因为Redis具备「高性能」和「高并发」两种特性。
1. Redis具备高性能
假如用户第一次访问MySQL中的某些数据,这个过程会比较慢,因为是从硬盘上读取的;将该用户访问的数据缓存在Redis中,这样下一-次再访问这些数据的时候就可以直接从缓存中获取了,操作Redis缓存就是直接操作内存,所以速度相当快。
如果MySQL中的对应数据改变的之后,同步改变Redis缓存中相应的数据即可,不过这里会有Redis和MySQL双写-致性的问题,后面我们会提到。
2. Redis具备高并发
单台设备的Redis的QPS (Query Per Second,每秒钟处理完请求的次数)是MySQL的10倍,Redis单机的QPS能轻松破10w,而MySQL单机的QPS很难破1w。
所以,直接访问Redis能够承受的请求是远远大于直接访问MySQL的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
4. Redis 数据类型以及使用场景分别是什么?
Redis提供了丰富的数据类型,常见的有五种数据类型: String (字符串),Hash (哈希),List (列
表),Set (集合)、Zset (有序集合)。
随着Redis版本的更新,后面又支持了四种数据类型: BitMap (2.2 版新增)、HyperLogLog (2.8 版
新增) . GEO (3.2 版新增)、Stream (5.0 版新增)。Redis 五种数据类型的应用场景:
●String类型的应用场景:缓存对象、常规计数、分布式锁、共享session信息等。
●List 类型的应用场景:消息队列(但是有两个问题: 1.生产者需要自行实现全局唯一 ID; 2.不能以消
费组形式消费数据)等。
●Hash类型:缓存对象、购物车等。
●Set类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
●Zset类型:排序场景,比如排行榜、电话和姓名排序等。
Redis后续版本又支持四种数据类型,它们的应用场景如下:
●BitMap (2.2版新增) :二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数
等;
●HyperLogLog (2.8 版新增) :海量数据基数统计的场景,比如百万级网页UV计数等;
●GEO (3.2版新增) :存储地理位置信息的场景,比如滴滴叫车;
●Stream (5.0版新增) :消息队列,相比于基于List类型实现的消息队列,有这两个特有的特性:自
动生成全局唯一消息ID, 支持以消费组形式消费数据。
5.五种常见的Redis数据类型是怎么实现?
如下是一张Redis数据类型和底层数据结构的对应关图,左边是Redis 3.0版本的,也就是《Redis设计与实现》这本书讲解的版本,现在看还是有点过时了,右边是现在Redis 7.0版本的。
5.1.1. String类型内部实现
String类型的底层的数据结构实现主要是SDS (简单动态字符串)。SDS 和我们认识的C字符串不太- -样,之所以没有使用C语言的字符串表示,因为SDS相比于C的原生字符串:
●SDS不仅可以保存文本数据,还可以保存二进制数据。因为SDS使用len属性的值而不是空字符来判断字符串是否结束,并且SDS的所有API都会以处理二进制的方式来处理SDS存放在buf[]数组里的数据。所以SDS不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据;
●SDS获取字符串长度的时间复杂度是0(1)。因为C语言的字符串并不记录自身长度,所以获取长度的复杂度为O(n);而SDS结构里用len属性记录了字符串长度,所以复杂度为0(1);
●Redis 的SDS API是安全的,拼接字符串不会造成缓冲区溢出。因为SDS在拼接字符串之前会检查
SDS空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。5.1.2. List 类型内部实现
List类型的底层数据结构是由双向链表或压缩列表实现的:
●如果列表的元素个数小于512个(默认值,可由list-max-ziplist-entries配置),列表每个元素的值
都小于64字节(默认值,可由list-max-ziplist-value 配置),Redis 会使用压缩列表作为List类型的底层数据结构;
●如果列表的元素不满足上面的条件,Redis 会使用双向链表作为List类型的底层数据结构;但是在Redis 3.2版本之后,List 数据类型底层数据结构就只由quicklist实现了,替代了双向链表和压缩列表。5.1.3. Hash类型内部实现
Hash类型的底层数据结构是由压缩列表或哈希表实现的:
●如果哈希类型元素个数小于512个(默认值,可由hash-max-ziplist-entries配置),所有值小于
64字节(默认值,可由hash-max ziplist-value配置)的话,Redis 会使用压缩列表作为Hash类型
的底层数据结构; .
●如果哈希类型元素不满足上面条件, Redis会使用哈希表作为Hash类型的底层数据结构。
在Redis 7.0中,压缩列表数据结构已经废弃了,交由listpack数据结构来实现了。5.1.4. Set类型内部实现
Set类型的底层数据结构是由哈希表或整数集合实现的:
●如果集合中的元素都是整数且元素个数小于512 (默认值,set-maxintset-entries配置) 个,Redis
会使用整数集合作为Set类型的底层数据结构;
●如果集合中的元素不满足上面条件,则Redis使用哈希表作为Set类型的底层数据结构。5.1.5. ZSet类型内部实现
Zset类型的底层数据结构是由压缩列表或跳表实现的:
●如果有序集合的元素个数小于128个,并且每个元素的值小于64字节时,Redis会使用压缩列表作
为Zset类型的底层数据结构;
●如果有序集合的元素不满足上面的条件,Redis会使用跳表作为Zset类型的底层数据结构; 在Redis 7.0中,压缩列表数据结构已经废弃了,交由listpack数据结构来实现了。
6. Redis是单线程吗?
Redis单线程指的是「接收客户端请求->解析请求->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说Redis是单线程的原因。
但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程(BIO) 的:
●Redis在2.6版本,会启动2个后台线程,分别处理关闭文件、AOF刷盘这两个任务;
●Redis 在4.0版本之后,新增了一个新的后台线程,用来异步释放Redis内存,也就是lazyfree 线程。例如执行unlink key / flushdb async / flushall async等命令,会把这些删除操作交给后台线程来执行,好处是不会导致Redis主线程卡顿。因此,当我们要删除一个大key的时候,不要使用del命令删除,因为del是在主线程处理的,这样会导致Redis主线程卡顿,因此我们应该使用unlink命令来异步删除大key.
之所以Redis为「关闭文件、AOF 刷盘、释放内存」这些任务创建单独的线程来处理,是因为这些任务的操作都是很耗时的,如果把这些任务都放在主线程来处理,那么Redis主线程就很容易发生阻塞,这样就无法处理后续的请求了。
后台线程相当于一一个消费者,生产者把耗时任务丢到任务队列中,消费者(BIO) 不停轮询这个队列,拿出任务就去执行对应的方法即可
关闭文件、AOF 刷盘、释放内存这三个任务都有各自的任务队列:
●BIO. _CLOSE_ FILE, 关闭文件任务队列:当队列有任务后,后台线程会调用close(fd),将文件关闭;
●BIO. _AOF_ _FSYNC, AOF刷盘任务队列:当AOF日志配置成everysec选项后,主线程会把AOF写日志操作封装成一个任务,也放到队列中。当发现队列有任务后,后台线程会调用fsync(fd),将AOF文件刷盘,
●BIO. _LAZY_ FREE, lazy free任务队列:当队列有任务后,后台线程会free(obj)释放对象/free(dict)删除数据库所有对象/ free(skiplist)释放跳表对象;
7. Redis单线程模式是怎样的?
Redis 6.0版本之前的单线模式如下图:
图中的浅蓝色部分是-个事件循环,是由主线程负责的,可以看到网络I/O和命令处理都是单线程。
Redis初始化的时候,会做~ 下面这几件事情:
●首先,调用epoll create()创建一个epoll 对象和调用socket() 创建一个服务端socket, 然后,调用bind()绑定端口和调用listen(监听该socket;
●然后,将调用epoll. _ctl() 将listen socket加入到epoll, 同时注册「连接事件」处理函数。初始化完后,主线程就进入到一一个事件循环函数,主要会做以下事情:
●首先,先调用处理发送队列函数,看是发送队列里是否有任务,如果有发送任务,则通过write函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完, 就会注册写事件处理函数,等待epoll wait发现可写后再处理。
●接着,调用epoll _wait函数等待事件的到来:。如果是连接事件到来,则会调用连接事件处理函数,该函数会做这些事情:调用accpet获取已连接的socket ->调用epoll _ctl 将已连接的socket加入到epoll ->注册「读事件」处理函数;
。如果是读事件到来,则会调用读事件处理函数,该函数会做这些事情:调用read获取客户端发送的数据->解析命令->处理命令->将客户端对象添加到发送队列->将执行结果写到发送缓存区
等待发送;
。如果是写事件到来,则会调用写事件处理函数,该函数会做这些事情:通过write函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完, 就会继续注册写事件处理函数,等待epoll_ wait发现可写后再处理。
8. Redis采用单线程为什么还这么快?
官方使用基准测试的结果是,单线程的Redis吞吐量可以达到10W/每秒,如下图所示: .
之所以Redis采用单线程(网络I/0和执行命令)那么快,有如下几个原因: .
●Redis的大部分操作都在内存中完成,并且采用了高效的数据结构,因此Redis瓶颈可能是机器的内
存或者网络带宽,而并非CPU,既然CPU不是瓶颈,那么自然就采用单线程的解决方案了;
●Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。
●Redis采用了I/O多路复用机制处理大量的客户端Socket请求,IO多路复用机制是指-一个线程处理多个IO流,就是我们经常听到的select/epoll 机制。简单来说,在Redis只运行单线程的情况下,该机制允许内核中,同时存在多个监听Socket和已连接Socket。内核会-直监听这些Socket上的连接请求或数据请求。- -旦有请求到达,就会交给Redis线程处理,这就实现了- -个Redis线程处理多个1O流的效果。
9. Redis 6.0之前为什么使用单线程?
我们都知道单线程的程序是无法利用服务器的多核CPU的,那么早期Redis版本的主要工作(网络
I/O和执行命令)为什么还要使用单线程呢?我们不妨先看一下Redis官方给出的FAQ
核心意思是: CPU 并不是制约Redis性能表现的瓶颈所在,更多情况下是受到内存大小和网络I/O的
限制,所以Redis核心网络模型使用单线程并没有什么问题,如果你想要使用服务的多核CPU,可以在一台服务器.上启动多个节点或者采用分片集群的方式。
除了.上面的官方回答,选择单线程的原因也有下面的考虑。
使用了单线程后,可维护性高,多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一-系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。
10. Redis 6.0之后为什么引入了多线程?
虽然Redis的主要工作(网络I/O和执行命令) -直是单线程模型,但是在Redis 6.0版本之后,也
采用了多个I/0线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络I/0的处理.上。
所以为了提高网络/O的并行度, Redis 6.0对于网络/O采用多线程来处理。但是对于命令的执行,Redis仍然使用单线程来处理,所以大家不要误解Redis有多线程同时执行命令。
Redis官方表示,Redis 6.0版本引入的多线程I/O特性对性能提升至少是一倍以上。
Redis 6.0版本支持的I/O多线程特性,默认情况下I/O多线程只针对发送响应数据(write client
socket),并不会以多线程的方式处理读请求(read client socket)。要想开启多线程处理客户端读请
求,就需要把Redis.conf配置文件中的io-threads-do-reads配置项设为yes.
同时,Redis.conf配置文件中提供了10多线程个数的配置项。
关于线程数的设置,官方的建议是如果为4核的CPU,建议线程数设置为2或3,如果为8核CPU建议线程数设置为6,线程数-定要小于机器核数,线程数并不是越大越好。
因此,Redis 6.0版本之后,Redis 在启动的时候,默认情况下会额外创建6个线程(这里的线程数不包括主线程) :
●Redis-server : Redis的主线程, 主要负责执行命令;
●bio_ _close. file、 bio_ _aof. _fsync. bio_ lazy_ free:三个后台线程,分别异步处理关闭文件任务、AOF刷
盘任务、释放内存任务;
●io. _thd_1. io _thd_ 2、io. _thd_ 3:三个I/O线程,io-threads默认是4,所以会启动3 (4-1) 个
I/0多线程,用来分担Redis网络I/O的压力。