Redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。
不过,增量式迭代命令也不是没有缺点的: 举个例子, 使用 SMEMBERS 命令可以返回集合键当前包含的所有元素, 但是对于 SCAN 这类增量式迭代命令来说, 因为在对键进行增量式迭代的过程中, 键可能会被修改, 所以增量式迭代命令只能对被返回的元素提供有限的保证 。
RDB做镜像全量持久化,AOF做增量持久化。因为RDB会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要AOF来配合使用。在redis实例重启时,会使用RDB持久化文件重新构建内存,再使用AOF重放近期的操作指令来实现完整恢复重启之前的状态。
取决于AOF日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。
你给出两个词汇就可以了,fork和cow。fork是指redis通过创建子进程来进行RDB操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。
RDB
他会生成多个数据文件,每个数据文件分别都代表了某一时刻Redis里面的数据,这种方式,有没有觉得很适合做冷备,完整的数据运维设置定时任务,定时同步到远端的服务器,比如阿里的云服务,这样一旦线上挂了,你想恢复多少分钟之前的数据,就去远端拷贝一份之前的数据就好了。 RDB对Redis的性能影响非常小,是因为在同步数据的时候他只是fork了一个子进程去做持久化的,而且他在数据恢复的时候速度比AOF来的快。
RDB都是快照文件,都是默认五分钟甚至更久的时间才会生成一次,这意味着你这次同步到下次同步这中间五分钟的数据都很可能全部丢失掉。AOF则最多丢一秒的数据,数据完整性上高下立判。 还有就是RDB在生成数据快照的时候,如果文件很大,客户端可能会暂停几毫秒甚至几秒,你公司在做秒杀的时候他刚好在这个时候fork了一个子进程去生成一个大快照,哦豁,出大问题。
AOF
上面提到了,RDB五分钟一次生成快照,但是AOF是一秒一次去通过一个后台的线程fsync操作,那最多丢这一秒的数据。
AOF在对日志文件进行操作的时候是以append-only的方式去写的,他只是追加的方式写数据,自然就少了很多磁盘寻址的开销了,写入性能惊人,文件也不容易破损。
AOF的日志是通过一个叫非常可读的方式记录的,这样的特性就适合做灾难性数据误删除的紧急恢复了,比如公司的实习生通过flushall清空了所有的数据,只要这个时候后台重写还没发生,你马上拷贝一份AOF日志文件,把最后一条flushall命令删了就完事了。
一样的数据,AOF文件比RDB还要大。
AOF开启后,Redis支持写的QPS会比RDB支持写的要低,他不是每秒都要去异步刷新一次日志嘛fsync,当然即使这样性能还是很高,ElasticSearch也是这样的,异步刷新缓存区的数据去持久化
Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将RDB文件全量同步到复制节点,复制节点接受完成后将RDB镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。后续的增量数据通过AOF日志同步即可,有点类似数据库的binlog。
在主从全量同步时,你可能会遇到同步失败的问题,具体场景如下:
slave 向 master 发起全量同步请求,master 生成 RDB 后发给 slave,slave 加载 RDB。
由于 RDB 数据太大,slave 加载耗时也会变得很长。
此时你会发现,slave 加载 RDB 还未完成,master 和 slave 的连接却断开了,数据同步也失败了。
之后你又会发现,slave 又发起了全量同步,master 又生成 RDB 发送给 slave。
同样地,slave 在加载 RDB 时,master / slave 同步又失败了,以此往复。
这是怎么回事?
其实,这就是 Redis 的「复制风暴」问题。
就像刚才描述的:主从全量同步失败,又重新开始同步,之后又同步失败,以此往复,恶性循环,持续浪费机器资源。
为什么会导致这种问题呢?
如果你的 Redis 有以下特点,就有可能发生这种问题:
- master 的实例数据过大,slave 在加载 RDB 时耗时太长
- 复制缓冲区(slave client-output-buffer-limit)配置过小
- master 写请求量很大
主从在全量同步数据时,master 接收到的写请求,会先写到主从「复制缓冲区」中,这个缓冲区的「上限」是配置决定的。
当 slave 加载 RDB 太慢时,就会导致 slave 无法及时读取「复制缓冲区」的数据,这就引发了复制缓冲区「溢出」。
为了避免内存持续增长,此时的 master 会「强制」断开 slave 的连接,这时全量同步就会失败。
之后,同步失败的 slave 又会「重新」发起全量同步,进而又陷入上面描述的问题中,以此往复,恶性循环,这就是所谓的「复制风暴」。
如何解决这个问题呢?我给你以下几点建议:
- Redis 实例不要太大,避免过大的 RDB
- 复制缓冲区配置的尽量大一些,给 slave 加载 RDB 留足时间,降低全量同步失败的概率
- 如果你也踩到了这个坑,可以通过这个方案来解决。
Redis Sentinal 着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。
Redis Cluster 着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。
虽然上面redis做了备份,看上去很完美。但由于redis目前只支持主从复制备份(不支持主主复制),当主redis挂了,从redis只能提供读服务,无法提供写服务。所以,还得想办法,当主redis挂了,让从redis升级成为主redis。
哨兵组件的主要功能:
- 集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
- 消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
- 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
- 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
Redis的过期策略,是有定期删除+惰性删除两种。
定期好理解,默认100ms就随机抽一些设置了过期时间的key,去检查是否过期,过期了就删了。
假如Redis里面所有的key都有过期时间,都扫描一遍?那太恐怖了,而且我们线上基本上也都是会设置一定的过期时间的。全扫描跟你去查数据库不带where条件不走索引全表扫描一样,100ms一次,Redis累都累死了。
好问题,惰性删除,见名知意,惰性嘛,我不主动删,我懒,我等你来查询了我看看你过期没,过期就删了还不给你返回,没过期该怎么样就怎么样。
官网上给到的内存淘汰机制是以下几个:
- noeviction: 返回错误。当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
- allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
- volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
- allkeys-random: 回收随机的键使得新添加的数据有空间存放。
- volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
- volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
某个时刻,多个系统实例都去更新某个 key。可以基于 Zookeeper 实现分布式锁。每个系统通过 Zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操作某个 Key,别人都不允许读和写。
你要写入缓存的数据,都是从 MySQL 里查出来的,都得写入 MySQL 中,写入 MySQL 中的时候必须保存一个时间戳,从 MySQL 查出来的时候,时间戳也查出来。
每次要写之前,先判断一下当前这个 Value 的时间戳是否比缓存里的 Value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。
Redis是一个CS结构的TCP服务器,使用”请求-应答”的模式。,客户端发起一个请求是这样的步骤:
客户端发送一个请求给服务器,然后等待服务器的响应,一般客户端使用阻塞模式来等待服务器响应。服务器收到请求并处理完毕后,发送结果给客户端。
客户端和服务器通过网络连接,网速可以非常快, 也可以非常慢。不管是快还是慢,消息包从客户端到服务器,再从服务器返回到客户端,总是要需要时间的。这个时间被称之为RTT(Round Trip Time,往返延时)。显然,当客户端需要发送多条请求时(比如往一个list中加很多元素,或者往一个数据库中填充很多keys),这个往返延时会影响到性能。假设网络非常慢,往返延时达到250毫秒,就算服务器每秒可以处理10万个请求,客户端也只能每秒处理4个请求。就算使用环回接口,往返延时非常小,如果需要执行很多写的操作, 也是要浪费许多时间的。
“请求-响应”模式的服务器在处理完一个请求后就开始处理下一个请求,不管客户端是否读取到前一个请求的响应结果。这让客户端不需要发一个请求等一个响应的串行,可以一次发送多个请求,再最后一次性读取所有响应。这就叫piplining(管道化),这种技术几十年来广泛的使用。比如很多POP3协议支持这个特性,大大的加速了从服务器上下载新邮件的速度。
Redis在很早的版本就支持pipeling,所以无论你用的是什么版本的redis,都可以用pipeling。下面是一个使用netcat的演示例子:
注意:当客户端使用pipelining发送很多请求时,服务器将在内存中使用队列存储这些指令的响应。所以批量发送的指令数量,最好在一个合理的范围内,比如每次发1万条指令,读取完响应后再发送另外1万条指令。2万条指令,一次性发送和分2次发送,对客户端来说速度是差不多的,但是对服务器来说,内存占用差了1万条响应的大小。
大部分使用pipelining的情况都可以用Redis脚本(2.6或高于2.6的版本才支持)来代替,使之更高效的在服务器端执行。使用脚本的最大好处是,在最小的延迟下可以读和写,比如可以:让“读,计算,写”这样一个流程非常快(pipeling不能处理这种情景,因为客户端需要得到响应之后才能计算和写)。有时候,应用程序可能需要在一个pipeline中发送多个EVAL或EVALSHA指令,redis的SCRITP LOAD指令能很好的满足这种需求(它保证了EVALSHA不会有调用失败的风险)。
流水线主要是一种网络优化。它本质上意味着客户端缓冲一堆命令,并一次性将它们发送到服务器。这些命令不能保证在事务中执行。这样做的好处是节省了每个命令的网络往返时间。
命令不能保证在事务中执行。也就是说pipeline只是为了节省命令网络时间,但不会将pipeline的命令顺序执行。需要事务的帮助。
Redis是单线程的,因此单个命令总是原子的,但是来自不同客户机的两个给定命令可以顺序执行,例如在它们之间交替执行。
但是,Multi/exec确保没有其他客户机在Multi/exec序列中的命令之间执行命令。
pipeline批量执行的时候,有可能是会被别的客户端打扰的。但是multi的话,他里面的逻辑是原子的,是一起执行的。
但要注意事务虽然确保命令之间顺序执行。但是没有整个事务原子性,需要配合watch命令保证针对key的事务原子性
是为了避免每个命令的逐个网络传输的消耗,先存放buffer后统一传输过去。但是如果没有事务的语义,其执行顺序不能保证顺序执行的,客户端间会交替执行。
在python的redis客户端中,pipe启动后,执行watch是可以执行get请求的,这个是立刻执行的。专门是为了watch命令服务的。其他都会包在一个网络请求里面进行请求与返回。
事务的意思是让客户端的多个命令能在服务端顺序执行。但不能保证事务的原子性。
- 中间错误不能回滚前面执行的命令
- 要注意锁,使用watch命令来达到select for update 的目的。避免并发更新的错误。
multi是pipeline类的一部分功能。也就是事务会包在pipeline里面的。
- Redis事务、Lua事务和管道的实践探究: 一个比较完整详细的对比
在一个事务中,只有当所有命令都依次执行完后才能得到每个结果的返回值,可是有些情况下需要先获得一条命令的返回值,然后再根据这个返回值进行业务判断后,才去执行下一条命令,比如我们这里就需要先去读取到key="joyo"的值,如果value>0,再去执行下一条命令
此时,对于get命令,这个时候我们是放在事务之外的,也就是当前事务体中只有一条递减的指令decr,是否能执行取决于读指令get取到的value
watch命令能够监控key是否被修改过,如果发现被修改了就不执行事务的操作,直接返回
Lua所有脚本都是以事务的形式来执行的,脚本在执行过程中不会被其他工作打断,也不会引起任何竞争条件,完全可以使用 Lua 脚本来代替事务和乐观锁
如果当前Redis服务端正在执行lua执行脚本,不会再接受其他指令,知道lua脚本执行完成后再去执行别的指令,因此,写lua脚本的时候切记不要写耗时过长的操作,避免出现死循环语句
-
使用脚本可以直接在服务器端执行 Redis 命令,一般的数据处理操作可以直接使用 Lua 语言或者Lua 解释器提供的函数库来完成,不必再返回给客户端进行处理。
-
所有脚本都是以事务的形式来执行的,脚本在执行过程中不会被其他工作打断,也不会引起任何竞争条件,完全可以使用 Lua 脚本来代替事务和乐观锁。
-
所有脚本都是可重用的,重复执行相同的操作时,只要调用储存在服务器内部的脚本缓存就可以了,不用重新发送整个脚本,从而尽可能地节约网络资源,(redis提供了evalsha的指令)
lua只能编写简单的脚本任务,可以利用中间值,但不能进行复杂的外部调用,数据查询、发送队列等,卡住其他工作。事务可以放在复杂的业务代码里面,但不需要事务中间命令的执行结果来编排后面的命令。也要注意失败后的,分布式事务问题?
- 虽然使用事务可以一次执行多个命令,并且通过乐观锁可以防止事务产生竞争条件,但是在实际中,要正确地使用事务和乐观锁并不是一件容易的事情。
- 对于一个业务场景需要考虑需要对哪些键加锁,给不相关的key加锁或者相关的key却不加锁,都会出现意外的错误,因此需要仔细结合业务场景进行全面的综合考虑,需要有一个思考的过程
- 另外一个就是引入事务和乐观锁会让代码显得更加复杂,还有带来额外的损耗
- 相比较之下,Lua脚本可能更加容易接受,上面已经总结了使用Lua脚本的有点,缺点就是需要保证好Lua脚本的准确性,相比较增加了新一门语言语法的掌握,值得庆幸的是Lua基本语法还算简单易懂。
- Lua保证了脚本执行的原子性,在当前脚本没执行完之前,别的命令和脚本都是等待状态,所以一定要控制好脚本中的内容,防止出现需要消耗大量时间的内容(逻辑相对简单)
Redis是基于TCP连接进行通信的,每一个请求/响应过程都需要经历一个RTT往返时间,如果需要执行很多短小的命令,这些往返时间的开销是很大的,在此情形下,redis提出了管道来提高执行效率。
管道的思想是:如果client执行一些相互之间无关的命令或者不需要获取命令的返回值,那么redis允许你连续发送多条命令,而不需要等待前面命令执行完毕。
注意:管道中的多个命令,如果其中一个出现执行错误,仍然会去执行下一个命令,不会停止。
使用管道可能在效率上比使用Lua脚本要好,但是有的情况下只能使用script。因为管道在执行后面的命令时,无法得到前面命令的结果,就像事务一样,所以如果需要在后面命令中使用前面命令的value等结果,则只能使用script或者事务+watch。
事务: 跟 Pipelining 一样,只有在事务执行完成时,才会把事务中多个命令的结果一并返回给客户端,因此客户端在事务还没有执行完的时候,无法获取其命令的执行结果
Pipelining 使用场景
- 对性能有要求
- 需要发送多个指令到服务端
- 不需要上个命令的返回结果作为下个命令的输入
事务使用场景
- 需要原子地执行多个命令
- 不需要事务中间命令的执行结果来编排后面的命令
Lua 脚本的使用场景
- 需要原子性地执行多个命令
- 需要中间值来组合后面的命令
- 需要中间值来编排后面的命令
- 常用于扩展 redis 功能,实现符合自己业务场景的命令
lazy-free是4.0新增的功能,但是默认是关闭的,需要手动开启。
手动开启lazy-free时,有4个选项可以控制,分别对应不同场景下,要不要开启异步释放内存机制:
a) lazyfree-lazy-expire:key在过期删除时尝试异步释放内存
b) lazyfree-lazy-eviction:内存达到maxmemory并设置了淘汰策略时尝试异步释放内存
c) lazyfree-lazy-server-del:执行RENAME/MOVE等命令或需要覆盖一个key时,删除旧key尝试异步释放内存
d) replica-lazy-flush:主从全量同步,从库清空数据库时异步释放内存
即使开启了lazy-free,如果直接使用DEL命令还是会同步删除key,只有使用UNLINK命令才会可能异步删除key。
这也是最关键的一点,上面提到开启lazy-free的场景,除了replica-lazy-flush之外,其他情况都只是可能去异步释放key的内存,并不是每次必定异步释放内存的。
开启lazy-free后,Redis在释放一个key的内存时,首先会评估代价,如果释放内存的代价很小,那么就直接在主线程中操作了,没必要放到异步线程中执行(不同线程传递数据也会有性能消耗)。
什么情况才会真正异步释放内存?这和key的类型、编码方式、元素数量都有关系(详细可参考源码中的lazyfreeGetFreeEffort函数):
a) 当Hash/Set底层采用哈希表存储(非ziplist/int编码存储)时,并且元素数量超过64个
b) 当ZSet底层采用跳表存储(非ziplist编码存储)时,并且元素数量超过64个
c) 当List链表节点数量超过64个(注意,不是元素数量,而是链表节点的数量,List的实现是在每个节点包含了若干个元素的数据,这些元素采用ziplist存储)
只有以上这些情况,在删除key释放内存时,才会真正放到异步线程中执行,其他情况一律还是在主线程操作。
也就是说String(不管内存占用多大)、List(少量元素)、Set(int编码存储)、Hash/ZSet(ziplist编码存储)这些情况下的key在释放内存时,依旧在主线程中操作。
可见,即使开启了lazy-free,String类型的bigkey,在删除时依旧有阻塞主线程的风险。所以,即便Redis提供了lazy-free,我建议还是尽量不要在Redis中存储bigkey。
个人理解Redis在设计评估释放内存的代价时,不是看key的内存占用有多少,而是关注释放内存时的工作量有多大。从上面分析基本能看出,如果需要释放的内存是连续的,Redis作者认为释放内存的代价比较低,就放在主线程做。如果释放的内存不连续(大量指针类型的数据),这个代价就比较高,所以才会放在异步线程中去执行。
我们接着上面节点2向集群广播消息往下讲。当节点3的的两个子节点接收到其主节点的FAIL状态消息时,两个节点就会开始发起故障迁移,竞选成为新的Master节点。两个节点参与竞选之前,首先要检查自身是否有资格参与竞选。
资格检查
Slave节点会不停的与Master节点通信来复制Master节点的数据,如果一个Slave节点长时间不与Master节点通信,那么很可能意味着该Slave节点上的数据已经落后Master节点过多(因为Master节点再不停的更新数据但是Slave节点并没有随之更新)。Redis认为,当一个Slave节点过长时间不与Master节点通信(计算与Master节点上次通信过去的时间),那么该节点就不具备参与竞选的资格。