1. 概述
什么是Redis,为什么用Redis?
Redis(Remote Dictionary Server)是一种支持key-value等多种数据结构的存储存系统。可用于缓存,事件发布或订阅,高速队列等场景。支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。
1)读写性能优异:读速度110000次/s 写速度81000次/s
2)数据类型丰富:支持二进制String Lists Hashes Sets OrderedSets
3)原子性:所有操作都是原子性的,同时还支持对几个操作合并后的原子执行
4)丰富的特性:支持publish/subscribe,通知,key过期
5)持久化:RDB AOF
6) 分布式:Redis Cluster
为什么Redis是单线程的以及为什么这么块?
1)redis采用了单线程模型,避免了不不要的上下文切换和竞争条件。Redis 是单线程,主要是指 Redis 的网络 IO和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。
文件事件处理器 采用io多路复用机制,同时监听多个socket,根据socket上的事件来选择对应的事件处理器进行处理。多个socket可能会并发产生不同的操作,每个操作对应不同的文件事件,但IO多路复用程序会监听多个socket,会将socket产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
redis客户端与服务端一次通信过程:
1)接收连接请求:客户端socket01向redis的server socket请求建立连接,此时server socket会产生一个AR_READABLE事件。IO多路复用程序监听到server socket产生的事件后,将该事件压入队列中。文件事件分派器从队列中获取该事件,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的socket01,并将该socket01的AE_READABLE事件与命令请求处理器关联。
2)读取请求内容:假设此时客户端发送了一个set key value 请求,此时redis中的socket01会产生AE_READABLE事件。IO多路复用程序将事件压入队列。事件分派器从队列中获取到该事件,由于前面socket01的AE_READABLE事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取socket01的key value 并在自己内存中完成key value的设置。操作完成后,它会将socket01的AE_WRITABLE事件与命令恢复处理器关联
3)恢复请求:如果此时客户端准备好接收返回结果,redis中的socket01会产生一个AE_WRITABLE事件,同样压入队列中。事件分派器找到相关联的命令恢复处理器。有命令恢复处理器对socket01输入本次操作的一个结果,比如ok,之后解除socket01的AE_WRITABLE事件与命令恢复处理器的关联。
2)基于内存,绝大部分请求是存粹的内存操作
3)数据结构简单,对数据操作简单
Redis一般使用场景
热点数据的缓存
限时业务: expire命令设置一个键的生存事件,到时间后redis会删除它。
计数器相关:incrby命令可以实现原子性的递增,可以运用于高并发的秒杀活动,分布式序列号生成,请求限流
分布式锁:setnx命令进行。setnx : “set if not exists” 就是如果不存在则成功设置缓存同时返回1,否则返回0。
在定时任务中首先通过setnx设置一个lock,如果成功设置则执行,如果没有成功则表明该任务已经执行。结合具体业务,给lock设置过期时间。
2. Redis数据类型
5种基础数据类型 String List Set Zset Hash
项目 | Value | 结构的读写能力 |
---|---|---|
String | 字符串,整数,浮点数 | 对整个字符串或字符串的一部分进行操作;对整数或浮点数进行自增或自减操作 |
List | 链表,链表上的每个节点都包含一个字符串 | 对链表的两端进行push和pop操作,读取单个或多个元素,根据值查找或删除元素 |
Set | 包含字符串的无序集合 | 字符串的集合,包含基础的方法是否存在添加,获取,删除;还包含计算交集,并集,差集等 |
Hash | 包含键值对的无序散列表 | 包含方法有添加,获取,删除单个元素 |
Zset | 和散列一样,用于存储键值对 | 字符串成员与浮点数分数之间的有序映射;元素的排列顺序由分数的大小决定;包含方法有添加,获取,删除单个元素以及根据分值范围或成员来获取元素 |
Redis的对象机制(redisObject)
比如说,集合类型就可以由字典和整数集合两种不同的数据结构实现,但是当用户执行zadd命令时,不必关心集合使用的是什么编码,只要Redis能按照zadd命令的指示,将新元素添加到集合就可以。
操作数据类型的命令处理要对键的类型进行检查之外,还需要根据数据类型的不同编码进行多态处理
为了解决以上问题,Redis构建了自己的类型系统,这个系统的主要功能包括:
1)redisObject对象
2)基于redisObject对象的类型检查
3)基于redisObjcet对象的显式多态函数
4)对redisObject进行分配,共享和销毁的机制
typedef struct redisObject {
unsigned type:4; //类型
unsigned encoding:4; //编码方式
unsigned lru:LRU_BITS; //24位,记录最末一次访问时间
int refcount; //引用计数
void *ptr //指向底层数据结构实例
} robj
Redis数据类型有哪些底层数据结构:
为什么要什么SDS?
1)常数复杂度获取字符串的长度:len属性
2)杜绝缓冲区溢出:在进行字符修改的时候,会首先根据记录的len属性检查内存空间是否满足需求,如果不满足,会进行相应的空间扩展,然后进行修改操作,所以不会出现缓冲区溢出
3)减少修改字符串的内存重新分配次数:len属性和alloc属性,对于修改字符串实现了空间预分配和惰性空间释放两种策略:
空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存从分配次数
惰性空间分配释放:对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用alloc属性将这些字节的数量记录下来,等待后续使用
4)二进制安全:C字符串以空字符作为字符串结束的标识;SDS以len属性表示的长度判断字符串是否结束
5)兼容部分C字符串函数:
Redis一个字符串类型的值能存储最大的容量
512M
为什么设计Stream?
Redis做消息队列:
PUB/SUB,订阅发布模式:无法持久化,如果网络断开,Redis宕机,消息就会被丢弃
List LPUSH+BRPOP 或者 基于Sorted-Set的实现:支持了持久化,但不支持多播,分组消费
3. Redis持久化和内存
Redis的持久化机制是什么?各自的优缺点?一般怎么用?
1)RDB持久化是把当前进程数据生成快照保存到磁盘上的过程;针对RDB不适合实时持久化的问题,Redis提供了AOF持久化方式来解决
2)AOF是“写后”日志,Redis先执行命令,把数据写入内存,然后才记录日志。日志里记录的是Redis收到的每一条命令,这些命令以文本形式保存
3)Redis 4.0提出了混合使用AOF和内存快照的方法:内存快照以一定的频率执行,在两次快照之间,使用AOF日志记录这期间的所有命令操作。
这样一来,快照不用很频繁的执行,就避免了fork对主线程的影响。而且,AOF日志也只用记录两次快照之间的操作,可以避免重写开销。
RDB触发方式:
手动触发:save命令:阻塞当前Redis服务器,直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞,线程环境不建议使用
bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短
自动触发:配置文件save m n,即在m秒内有n次修改时,自动触发bgsave生成rdb文件;
主从复制时,从节点要从主节点进行全量复制时也会触发bgsave操作,生成当时的快照发送的从节点
执行debug reload命令重新加载redis时也会触发bgsave操作
默认情况下执行shutdown命令时,如果没有开启aof持久化,也触发bgsave
RDB由于生产环境中位Redis开辟的内存区域都比较大,将内存中的数据同步到硬盘的过程会持续较长时间,实际情况是这段时间Redis服务一般都会收到数据写操作请求。如何保证数据一致性?
RDB的核心思路是Copy-on-Write,来保证进行快照操作的这段时间,需要压缩写入磁盘上的数据在内存中不会发生变化。在正常的快照操作中,一方面Redis主进程会fork一个新的快照进程专门做这件事,这就保证了Redis服务不会停止对客户端包括写请求在内的任何响应。另一方面,这段时间发生的数据变化会以副本的方式存放在另一个新的内存区域,带快照操作结束后才会同步到原来的内存区域。
在进行RDB快照操作的这段时间,如果发生服务崩溃?
在没有将数据全部写入到磁盘前,这次快照操作都不算成功。如果出现了服务崩溃的情况,将以上一次完整的RDB快照文件作为恢复内存数据的参考。也就是说,在快照操作过程中不能影响上一次的备份数据。Redis服务会在磁盘上创建一个临时文件进行数据操作,带操作成功后才会用这个临时文件替换掉上一次的备份。
可以每秒做一次RDB快照吗?
增量快照:做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。但需要额外的元数据信息记录那些数据被修改了,会带来额外的空间开销问题。RDB的快速恢复,以较小的开销尽量少丢数据:RDB和AOF混合。
AOF是写前日志还是写后日志?
先写内存,后写日志
Redis要求高性能:
避免额外的检查开销:Redis在向AOF里记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令,日志中就有可能记录了错误的命令,Redis在使用日志恢复数据时,就可能会出错。
不会阻塞当前的写操作
潜在风险:如果命令执行完成,写日志之前就宕机了,会丢失数据。
主线程写磁盘压力大,导致写盘慢,阻塞后续操作。
如何实现AOF
AOF日志记录Redis的每个写命令,步骤分为:命令追加append 文件写入write 文件同步 sync
命令追加:当AOF持久化功能打开了,服务器在执行完一个写命令,会以协议格式将被执行的写命令追加到服务器的aof_buf缓冲区
文件写入和同步:关于合适将aof_buf缓冲区的内容写入AOF文件中,Redis提供了三种写回策略:
always,同步写回:每个写命令执行完,立马同步的将日志写回磁盘
everysec,每秒写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区的内如写入磁盘;
No,操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定合适将缓冲区内容写回磁盘。
三种写回策略的优缺点:
为了提高文件写入效率,在现代操作系统中,当用户调用write函数,将一些数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区的空间被填满或超过了指定时限后,才真正将缓冲区的数据写入到磁盘里。
这样的操作虽然提高了效率,但也为数据写入带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失。为此,系统提供了fsync、fdatasync同步函数,可以强制操作系统立刻将缓冲区中的数据写入到硬盘里,从而确保写入数据的安全性。
AOF重写:
Redis 通过创建一个新的AOF文件来替换现有的AOF,新旧两个AOF文件保存的数据相同,但新AOF文件没有了冗余命令。
AOF重写会阻塞吗?
AOF重写过程是有后台进程bgrewriteaof来完成的。主线程fork出后台的bgrewriteaof子进程,fork会把主线程的内存拷贝一份给bgrewriteaof子进程,这里包含了数据库最新数据。然后,bgrewriteaof子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
所以在aof重写时,在fork进程时是会阻塞主线程的。
AOF日志何时会重写?
有两个配置项控制AOF重写的触发:
auto-aof-rewrite-min-size:表示运行AOF重写时文件的最小大小,默认为64MB
auto-aof-rewrite-percentage:当前aof文件的相比上一次重写后aof文件的增量大小
AOF重写日志时,有新数据写入?
主线程fork出子进程如何复制内存数据?
再重写日志整个过程时,主线程有哪些地方会被阻塞:
1)fork子进程时,需要拷贝虚拟页表,会对主线程阻塞
2)主进程有bigkey写入时,操作系统会创建页面的副本,并拷贝原有的数据,会对主线程阻塞
3)子进程重写日志完成后,主进程追加aof重写缓冲区时可能会对主线程阻塞
为什么AOF重写不复用原AOF日志?
1)父子进程写同一个文件会产生竞争问题,影响父进程的性能
2)如果AOF重写过程中失败了,相当于污染了原本的AOF文件,无法做恢复数据使用
Redis过期键的删除策略有哪些?
惰性删除:服务器不主动删除数据,当客户端查询某个数据时,服务器判断该数据是否过期,如果过期则删除
定期删除:服务器执行定时任务删除过期数据。
Redis内存淘汰算法:
1)不淘汰:
- noevication (v4.0后默认的)
2) 对设置了过期时间的数据进行淘汰
- 随机:volatile-random
- ttl: volatile-ttl
- lru:volatile-lru
- lfu:volatile-lfu
3)全部数据进行淘汰: - 随机:allkeys-randoms
- lru:allkeys-lru
- lfu:allkeys-lfu
LRU算法:least recently used,按照最近最少使用的原则来筛选数据。这种模式下会使用LRU算法筛选设置了过期时间的键值对
Redis优化的LRU算法:Redis会记录每隔数据的最近一次被访问的时间戳。在Redis决定淘汰数据时,第一次会随机选出N个数据,把它们作为一个候选集合。接下来,Redis会比较这N个数据的LRU字段,把LRU字段值最小的数据从缓存中淘汰。通过随机读取待删除集合,可以让Redis不用维护一个巨大的链表,也不用操作链表,进而提升性能
LFU算法:在LRU的基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用LFU策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU策略在比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。
Redis的LFU算法实现:当LFU策略筛选数据时,Redis会在候选集合中根据LRU字段后8bit选择访问次数最少的数据进行淘汰。访问次数相同时,再根据LRU字段的前16bit值大小,选择访问时间最久远的数据进行淘汰。
Redis只是用了8bit记录数据的访问次数,而8bit记录的最大值是255,这样访问快速的情况下,如果每次被访问就加一,很快某条数据就达到最大值255,可能很多数据都是255,那么就退化成LRU。所以Redis为了解决这个问题,实现了一个更优的计数规则,并可可以通过配置项,来控制计数器增加的速度。
Redis的内存用完了会发生?
如果达到设置的上限,Redis的写命令会返回错误信息(但读命令还可以正常返回)或者配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。
Redis如何做内存优化?
缩减键值对象:缩减键(key)和值(value)的长度。key值越短越好,value值把业务对象序列化成二进制数组放入Redis。首先在业务上精简业务对象,去掉不必要的属性避免存储无效数据。其次选择高效序列化工具。
共享对象池:Redis内部维护[0-9999]的整数对象池。
字符串优化
编码优化
控制key的数量
Redis key的过期时间和永久有效分别如何设置?
expire和persist
Redis 管道:
一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应,这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。
3. Redis事务:
本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
Redis事务相关命令
- multi 开启事务,redis会将后续的命令逐个放入队列中,然后使用exec命令来原子化执行这个命令系列
- exec 执行事务中的所有操作命令
- discard 取消事务,放弃执行事务块中的所有命令
- watch 监视一个或多个key,如果事务在执行前,这个key被其他命令修改,则事务被中断,不会执行事务中的任何命令
- unwatch 取消watch对所有key 的监视
Redis 事务的三个阶段
- 开启: 以multi开始一个事务
- 入队:将多个命令入队到事务中,接到这些命令并不立即执行,而是放入等待执行的事务队列
- 执行:由exec命令触发事务
Redis 事务的其他实现?
- 基于Lua脚本
- 基于中间标记变量
Redis事务中出现错误的处理?
- 语法错误 编译器错误
- Redis类型错误 运行时错误
Redis事务中watch是如何监视实现的?
Redis使用WATCH命令来决定事务是继续执行还是回滚,那就需要在MULTI之前使用WATCH来监控某些键值对,然后使用MULTI命令来开启事务,执行对数据结构操作的各种命令,此时这些命令入队列。
当使用EXEC执行事务时,首先会比对WATCH所监控的键值对,如果没发生改变,它会执行事务队列中的命令,提交事务;如果发生变化,将不会执行事务中的任何命令,同时事务回滚。当然无论是否回滚,Redis都会取消执行事务前的WATCH命令。
为什么Redis不支持回滚?
- Redis命令只会因为错误的语法而失败,或命令用在了错误类型的键上面。
- 因为不需要对回滚进行支持,所以Redis的内部可以保持简单且快速