深入Redis

《Redis深度历险:核心原理和应用实践》 基础 应用1 原理1 原理3 拓展4

目录

基础-Redis数据结构

string(字符串)

list(列表)

hash(字典)

set(集合)

zset(有序列表)

容器型数据结构的通用规则

过期时间

应用1-分布式锁

分布式锁

超时问题

可重入性

原理1-线程IO模型

非阻塞IO 

事件轮询(多路复用)

指令队列

响应队列

定时任务

原理3-持久化

快照原理

fork(多进程)

AOF原理

AOF重写

fsync 

运维

Redis 4.0 混合持久化 

扩展4-过期策略

过期的key集合

定时扫描策略

淘汰策略

主从同步

redis常见的问题有哪些?都怎么解决?


基础-Redis数据结构

Redis有五种基础数据结构,分别为:string(字符串)、list(列表)、set(集合)、hash(哈希)和zset(有序集合)

string(字符串)

key只能时字符串类型,所有数据都有唯一的字符串key。

常见的用途就是缓存用户信息。比如将用户信息通过JSON序列化成字符串,然后将序列化后的字符串塞进Redis来缓存。同样,取用户信息会经过一次反序列化过程。

Redis的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于Java的Arraylist,采用预分配冗余空间的方式来减少内存的频繁分配。

如图中所示,内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len。当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M。

  • 批量键值对

mset mget

  • 过期和set命令扩展

可以对 key 设置过期时间,到点自动删除,这个功能常用来控制缓存的失效时间。

expire setex setnx

  • 计数

如果 value 值是一个整数,还可以对它进行自增操作。自增是有范围的,它的范围是signed long 的最大最小值,超过了这个值,Redis 会报错。 

incr incrby

list(列表)

Redis的列表相当于Java语言中的LinkedList,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)。

当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收

Redis 的列表结构常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串塞进 Redis 的列表,另一个线程从这个列表中轮询数据进行处理。 

  • 右边进左边出:队列

rpush llen lpop

  • 右边进右边出:栈

rpush rpop

  • 慢操作

lindex  ltrim lrange

lindex 相当于 Java 链表的 get(int index)方法,它需要对链表进行遍历,性能随着参数index 增大而变差。 ltrim 和字面上的含义不太一样,个人觉得它叫 lretain(保留) 更合适一些,因为 ltrim 跟的两个参数 start_index 和 end_index 定义了一个区间,在这个区间内的值,ltrim 要保留,区间之外统统砍掉。我们可以通过 ltrim 来实现一个定长的链表,这一点非常有用。index 可以为负数,index=-1 表示倒数第一个元素,同样 index=-2 表示倒数第二个元素。

  • 快速列表

首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是 ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成 quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间,而且会加重内存的碎片化。比如这个列表里存的只是 int 类型的数据,结构上还需要两个额外的指针 prev 和 next 。所以 Redis 将链表和 ziplist 结合起来组成了 quicklist。也就是将多个 ziplist 使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。(压缩表和双向链表的结合)(快速表)(数据量较小时使用压缩表,较大时使用快速表)

压缩列表将所有的元素紧挨在一起存储,分配连续内存。当数据量较大的时候将多个压缩列表使用双向指针串起来使用。若单纯只使用链表,附加指针空间浪费空间而且易产生内存碎片。

hash(字典)

Redis的字典相当于Java语言中的HashMap,它是无序字典。内部实现结构上同Java的HashMap也是一致的,同样使用数组+链表二维结构。第一维 hash 的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。 

不同的是,Redis 的字典的值只能是字符串,另外它们 rehash 的方式不一样,因为 Java 的 HashMap 在字典很大时,rehash 是个耗时的操作,需要一次性全部 rehash。Redis 为了高性能,不能堵塞服务,所以采用了渐进式 rehash 策略。

渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,查询时会同时查询两个 hash 结构,然后在后续的定时任务中以及 hash 的子指令中,循序渐进地将旧 hash 的内容一点点迁移到新的 hash 结构中。  

当 hash 移除了最后一个元素之后,该数据结构自动被删除,内存被回收。 

hash 结构也可以用来存储用户信息,不同于字符串一次性需要全部序列化整个对象,hash 可以对用户结构中的每个字段单独存储。这样当我们需要获取用户信息时可以进行部分获取。而以整个字符串的形式去保存用户信息的话就只能一次性全部读取,这样就会比较浪费网络流量。

hash 也有缺点,hash 结构的存储消耗要高于单个字符串,到底该使用 hash 还是字符串,需要根据实际情况再三权衡。

hset hgetall hlen hget hmset

同字符串一样,hash 结构中的单个子 key 也可以进行计数,它对应的指令是 hincrby,和 incr 使用基本一样。

hincrby

set(集合)

Redis的集合相当于Java语言里面的HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL。 

当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。 set 结构可以用来存储活动中奖的用户 ID,因为有去重功能,可以保证同一个用户不会中奖两次。 

sadd smemebers sismember scard spop

zset(有序列表)

zset 可能是 Redis 提供的最为特色的数据结构,它也是在面试中面试官最爱问的数据结构。它类似于 Java 的 SortedSet HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。它的内部实现用的是一种叫着「跳跃列表」的数据结构。 

zset 中最后一个 value 被移除后,数据结构自动删除,内存被回收。 zset 可以用来存粉丝列表,value 值是粉丝的用户 ID,score 是关注时间。我们可以对粉丝列表按关注时间进行排序。

zset 还可以用来存储学生的成绩,value 值是学生的 ID,score 是他的考试成绩。我们可以对成绩按分数进行排序就可以得到他的名次。 

总结就是可以按指定的顺序进行排序

zadd zrange zrevrange zcard zscore zrank zrangebyscore zrem

  • 跳跃列表

zset 内部的排序功能是通过「跳跃列表」数据结构来实现的,它的结构非常特殊,也比较复杂。 

跳跃列表就是类似于这种层级制,最下面一层所有的元素都会串起来。然后每隔几个元素挑选出一个代表来,再将这几个代表使用另外一级指针串起来。然后在这些代表里再挑出二级代表,再串起来。最终就形成了金字塔结构。

如果使用链表不支持随机的插入和删除。

定位插入点时,先在顶层进行定位,然后下潜到下一级定位,一直下潜到最底层找到合适的位置,将新元素插进去。

顶层数据较少找到合适的位置之后就去下一层寻找,直到最后一层。这样的效率仅仅o(logn)

定位插入点时,先在顶层进行定位,然后下潜到下一级定位,一直下潜到最底层找到合适的位置,将新元素插进去。

跳跃列表采取一个随机策略来决定新元素可以兼职到第几层。 

首先 L0 层肯定是 100% 了,L1 层只有 50% 的概率,L2 层只有 25% 的概率,L3 层只有 12.5% 的概率,一直随机到最顶层 L31 层。绝大多数元素都过不了几层,只有极少数元素可以深入到顶层。列表中的元素越多,能够深入的层次就越深,能进入到顶层的概率就会越大。

容器型数据结构的通用规则

list/set/hash/zset 这四种数据结构是容器型数据结构,它们共享下面两条通用规则:

  • create if not exists 

如果容器不存在,那就创建一个,再进行操作。比如 rpush 操作刚开始是没有列表的,Redis 就会自动创建一个,然后再 rpush 进去新元素。  

  • drop if no elements 

如果容器里元素没有了,那么立即删除元素,释放内存。这意味着 lpop 操作到最后一个元素,列表就消失了。 

过期时间

Redis 所有的数据结构都可以设置过期时间,时间到了,Redis 会自动删除相应的对象。需要注意的是过期是以对象为单位,比如一个 hash 结构的过期是整个 hash 对象的过期,而不是其中的某个子 key。

还有一个需要特别注意的地方是如果一个字符串已经设置了过期时间,然后你调用了 set 方法修改了它,它的过期时间会消失。 

应用1-分布式锁

分布式应用进行逻辑处理时经常会遇到并发问题。 

比如一个操作要修改用户的状态,修改状态需要先读出用户的状态,在内存里进行修改,改完了再存回去。如果这样的操作同时进行了,就会出现并发问题,因为读取和保存状态这两个操作不是原子的。

分布式锁

分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”,当别的进程也要来占时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。 
占坑一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占坑。先来先占, 用完了,再调用 del 指令释放茅坑。

但是有个问题,如果逻辑执行到中间出现异常了,可能会导致 del 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。 
于是我们在拿到锁之后,再给锁加上一个过期时间,比如 5s,这样即使中间出现异常也可以保证 5 秒之后锁会自动释放。 

但是以上逻辑还有问题。如果在 setnx 和 expire 之间服务器进程突然挂掉了,可能是因为机器掉电或者是被人为杀掉的,就会导致 expire 得不到执行,也会造成死锁。 

为了治理这个乱象,Redis 2.8 版本中作者加入了 set 指令的扩展参数,使得 setnx 和 expire 指令可以一起执行,彻底解决了分布式锁的乱象。从此以后所有的第三方分布式锁 library 可以休息了。 > set lock:codehole true ex 5 nx OK ... do something critical ... > del lock:codehole 上面这个指令就是 setnx 和 expire 组合在一起的原子指令,它就是分布式锁的奥义所在

set lock2 value2 NX EX 5

超时问题

Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。因为这时候锁过期了,第二个线程重新持有了这把锁,但是紧接着第一个线程执行完了业务逻辑,就把锁给释放了,第三个线程就会在第二个线程逻辑执行完之间拿到了锁。 (超时之后锁失效,其它线程拿到了锁,后面超时的线程又释放了锁,这时候其它线程可以拿到相同的锁,对同一个数据进行操作)

为了避免这个问题,Redis 分布式锁不要用于较长时间的任务。如果真的偶尔出现了,数据出现的小波错乱可能需要人工介入解决。 

有一个更加安全的方案是为 set 指令的 value 参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后再删除 key。但是匹配 value 和删除 key 不是一个原子操作,Redis 也没有提供类似于 delifequals 这样的指令,这就需要使用 Lua 脚本来处理了,因为 Lua 脚本可以保证连续多个指令的原子性执行。 


 

可重入性

可重入性是指线程在持有锁的情况下再次请求加锁,如果一个锁支持同一个线程的多次加锁,那么这个锁就是可重入的。比如 Java 语言里有个 ReentrantLock 就是可重入锁。Redis 分布式锁如果要支持可重入,需要对客户端的 set 方法进行包装,使用线程的 Threadlocal 变量存储当前持有锁的计数。

以上还不是可重入锁的全部,精确一点还需要考虑内存锁计数的过期时间,代码复杂度将会继续升高。老钱不推荐使用可重入锁,它加重了客户端的复杂性,在编写业务方法时注意在逻辑结构上进行调整完全可以不使用可重入锁。下面是 Java 版本的可重入锁。 

跟 Python 版本区别不大,也是基于 ThreadLocal 和引用计数。 

原理1-线程IO模型

Redis 是个单线程程序!

因为它所有的数据都在内存中,所有的运算都是内存级别的运算。正因为 Redis 是单线程,所以要小心使用 Redis 指令,对于那些时间复杂度为 O(n) 级别的指令,一定要谨慎使用,一不小心就可能会导致 Redis 卡顿。 

Redis 单线程如何处理那么多的并发客户端连接? 

非阻塞IO 

当我们调用套接字的读写方法,默认它们是阻塞的,比如 read 方法要传递进去一个参数n,表示读取这么多字节后再返回,如果没有读够线程就会卡在那里,直到新的数据到来或者连接关闭了,read 方法才可以返回,线程才能继续处理。而 write 方法一般来说不会阻塞,除非内核为套接字分配的写缓冲区已经满了,write 方法就会阻塞,直到缓存区中有空闲空间挪出来了。

 非阻塞 IO 在套接字对象上提供了一个选项 Non_Blocking,当这个选项打开时,读写方法不会阻塞,而是能读多少读多少,能写多少写多少。能读多少取决于内核为套接字分配的读缓冲区内部的数据字节数,能写多少取决于内核为套接字分配的写缓冲区的空闲空间字节数。读方法和写方法都会通过返回值来告知程序实际读写了多少字节。 

有了非阻塞 IO 意味着线程在读写 IO 时可以不必再阻塞了,读写可以瞬间完成然后线程可以继续干别的事了。 

事件轮询(多路复用)

非阻塞 IO 有个问题,那就是线程要读数据,结果读了一部分就返回了,线程如何知道何时才应该继续读。也就是当数据到来时,线程如何得到通知。写也是一样,如果缓冲区满了,写不完,剩下的数据何时才应该继续写,线程也应该得到通知。 

事件轮询 API 就是用来解决这个问题的,最简单的事件轮询 API 是 select 函数,它是操作系统提供给用户程序的 API。输入是读写描述符列表 read_fds & write_fds,输出是与之对应的可读可写事件。同时还提供了一个 timeout 参数,如果没有任何事件到来,那么就最多等待 timeout 时间,线程处于阻塞状态。一旦期间有任何事件到来,就可以立即返回。时间过了之后还是没有任何事件到来,也会立即返回。拿到事件后,线程就可以继续挨个处理相应的事件。处理完了继续过来轮询。于是线程就进入了一个死循环,我们把这个死循环称为事件循环,一个循环为一个周期。

每个客户端套接字 socket 都有对应的读写文件描述符。 

因为我们通过 select 系统调用同时处理多个通道描述符的读写事件,因此我们将这类系统调用称为多路复用 API。现代操作系统的多路复用 API 已经不再使用 select 系统调用,而改用 epoll(linux)和 kqueue(freebsd & macosx),因为 select 系统调用的性能在描述符特别多时性能会非常差。它们使用起来可能在形式上略有差异,但是本质上都是差不多的,都可以使用上面的伪代码逻辑进行理解。

服务器套接字 serversocket 对象的读操作是指调用 accept 接受客户端新连接。何时有新连接到来,也是通过 select 系统调用的读事件来得到通知的。 

事件轮询 API 就是 Java 语言里面的 NIO 技术

Java 的 NIO 并不是 Java 特有的技术,其它计算机语言都有这个技术,只不过换了一个词汇,不叫 NIO 而已。 

指令队列

Redis 会将每个客户端套接字都关联一个指令队列。客户端的指令通过队列来排队进行顺序处理,先到先服务

响应队列

Redis 同样也会为每个客户端套接字关联一个响应队列。Redis 服务器通过响应队列来将指令的返回结果回复给客户端。 如果队列为空,那么意味着连接暂时处于空闲状态,不需要去获取写事件,也就是可以将当前的客户端描述符从 write_fds 里面移出来。等到队列有数据了,再将描述符放进去。避免 select 系统调用立即返回写事件,结果发现没什么数据可以写。出这种情况的线程会飙高 CPU

定时任务

服务器处理要响应 IO 事件外,还要处理其它事情。比如定时任务就是非常重要的一件事。如果线程阻塞在 select 系统调用上,定时任务将无法得到准时调度。那 Redis 是如何解决这个问题的呢

Redis 的定时任务会记录在一个称为最小堆的数据结构中。这个堆中,最快要执行的任务排在堆的最上方。在每个循环周期,Redis 都会将最小堆里面已经到点的任务立即进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是 select 系统调用的 timeout 参数。因为 Redis 知道未来 timeout 时间内,没有其它定时任务需要处理,所以可以安心睡眠 timeout 的时间

Nginx 和 Node 的事件处理原理和 Redis 也是类似的 

原理3-持久化

Redis 的数据全部在内存里,如果突然宕机,数据就会全部丢失,因此必须有一种机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的持久化机制。

 Redis 的持久化机制有两种,第一种是快照,第二种是 AOF 日志快照是一次全量备份AOF 日志是连续的增量备份快照是内存数据的二进制序列化形式,在存储上非常紧凑,而 AOF 日志记录的是内存数据修改的指令记录文本。AOF 日志在长期的运行过程中会变的无比庞大,数据库重启时需要加载 AOF 日志进行指令重放,这个时间就会无比漫长。所以需要定期进行 AOF 重写,给 AOF 日志进行瘦身。 

快照原理

Redis在服务线上请求的同时,还要进行内存快照进行持久化,内存快照要求Redis对文件进行IO操作,但是文件IO操作不允许使用多路复用API。

这意味着单线程同时在服务线上的请求还要进行文件 IO 操作,文件 IO 操作会严重拖垮服务器请求的性能。还有个重要的问题是为了不阻塞线上的业务,就需要边持久化边响应客户端请求。持久化的同时,内存数据结构还在改变,比如一个大型的 hash 字典正在持久化,结果一个请求过来把它给删掉了,还没持久化完呢,这尼玛要怎么搞? 

Redis 使用操作系统的多进程 COW(Copy On Write) 机制来实现快照持久化

fork(多进程)

Redis在持久化时会调用glibc的函数fork产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求。子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段(为了节约资源,在进程分离的一瞬间,内存的增长几乎没有明显变化)。

用 Python 语言描述进程分离的逻辑如下。fork 函数会在父子进程同时返回,在父进程里返回子进程的 pid,在子进程里返回零。如果操作系统内存资源不足,pid 就会是负数,表示 fork 失败。 

子进程做数据持久化,它不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是父进程不一样,它必须持续服务客户端请求,然后对内存数据结构进行不间断的修改。

这个时候就会使用操作系统的 COW (copy on write写时拷贝)机制来进行数据段页面的分离。数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的页面是没有变化的,还是进程产生时那一瞬间的数据。 (父进程对数据进行修改的时候,将数据复制分离一份进行修改,快照备份的数据不会改变)

随着父进程修改操作的持续进行,越来越多的共享页面被分离出来,内存就会持续增长。但是也不会超过原有数据内存的 2 倍大小。(一般Redis中冷数据较多,不会有太多的页面被分离出来)

Redis快照持久化,在子进程产生的一瞬间数据就不会发生变化了,当主进程要对数据进行修改时,才有cow机制。

AOF原理

AOF日志存储的是Redis服务器的顺序指令序列,AOF日志只记录对内存进行修改的指令。

Redis收到客户端请求之后,先写入AOF日志,再操作指令。若此时服务器宕机可通过指令重放来进行恢复。若时间很长,AOF日志也很长,指令重放耗时较多,需要进行重写对AOF日志进行瘦身。

AOF重写

Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。

其原理就是开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。

开辟子进程遍历内存转换成指令序列化新的AOF文件中,在将这期间发生的增量AOF日志追加到新的AOF日志中。

fsync 

什么时候将缓存中的AOF日志刷到磁盘(一般一秒刷一次,永不执行fsync让操作系统决定aof日志易丢失,一个指令就fsync一次,刷到磁盘是磁盘IO操作,会很慢)

AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操作时,实际上是将内容写到了内核为文件描述符分配的一个内存缓存中,然后内核会异步将脏数据刷回到磁盘的。(向AOF写时,先写到缓存再刷到磁盘,若还没刷到磁盘就宕机,日志丢失)。

Linux 的 glibc 提供了 fsync(int fd)函数可以将指定文件的内容强制从内核缓存刷到磁盘

只要 Redis 进程实时调用 fsync 函数就可以保证 aof 日志不丢失。但是 fsync 是一个磁盘 IO 操作,它很慢!如果 Redis 执行一条指令就要 fsync 一次,那么 Redis 高性能的地位就不保了。

所以在生产环境的服务器中,Redis 通常是每隔 1s 左右执行一次 fsync 操作,周期 1s 是可以配置的。这是在数据安全性和性能之间做了一个折中,在保持高性能的同时,尽可能使得数据少丢失。 (一秒刷一次,可能丢失这一秒的数据)

Redis 同样也提供了另外两种策略,一个是永不 fsync——让操作系统来决定合适同步磁盘,很不安全,另一个是来一个指令就 fsync 一次——非常慢。但是在生产环境基本不会使用,了解一下即可。 

运维

Redis 4.0 混合持久化 

rdb持久化因为是对快照进行备份,会丢失大量数据。AOF持久化重做时会很慢。

混合持久化:将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自
持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。

 

于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。 

扩展4-过期策略

删除过期的数据也会占用线程的处理时间

过期的key集合

redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定时遍历(定时删除)这个字典来删除到期的 key。除了定时遍历之外,它还会使用惰性策略来删除过期的 key,所谓惰性策略就是在客户端访问这个 key 的时候,redis 对 key 的过期时间进行检查,如果过期了就立即删除。定时删除是集中处理,惰性删除是零散处理。

定时扫描策略

Redis 默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略

  1. 从过期字典中随机20个key
  2. 删除这20个key中已过期的key
  3. 如果过期的key比率超过1/4,那就重复步骤1

同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms.

所以业务开发人员一定要注意过期时间,如果有大批量的 key 过期,要给过期时间设置一个随机范围,而不能全部在同一时间过期。

 

从库的过期策略
从库不会进行过期扫描,从库对过期的处理是被动的。主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key 。

因为指令同步是异步进行的,所以主库过期的 key 的 del 指令没有及时同步到从库的话,会出现主从数据的不一致,主库没有的数据在从库里还存在,比如上一节的集群环境分布式锁的算法漏洞就是因为这个同步延迟产生的。 

淘汰策略

Redis通过maxmemory参数来设定内存的使用上限,当redis使用内存达到设定的最大值的时候,会根据配置的淘汰策略选取要删除的key进行删除,从而给新的键值流出空间。

maxmemory为0的时候表示对redis内存使用没有限制。

6中淘汰策略(默认noeviction)

  • noeviction:当内存达到阈值时,所有申请内存的命令都会报错
  • allkeys-lru:在数据集中优先淘汰最近最少使用的数据
  • volatile-lru:在设置了过期时间的数据集中挑选最近最少使用的数据
  • allkeys-random:在数据集中任意挑选数据淘汰
  • volatile-random:在设置过期时间的数据集中任意挑选数据淘汰
  • volatile-ttl:从设置过期时间的数据集中挑选即将要过期的数据。

主从同步

分布式系统的CPA:

C一致性:在分布式系统的所有数据,在同一时刻的值保持一致。

A可用性:指系统提供的服务必须是可用状态,不保证数据为最新的数据(有一部分节点故障后,系统依旧可用)

P分区容忍性:在网络分区的情况下,仍然可以接收请求(满足一致性和可用性)。网络分区指网络被分成若干个独立的区域,区域之间不能通信。在网络丢失的情况下,系统照样能进行工作。

CP:强一致,同步时间比较长

AP:高可用,可能数据不一致。

对于分布式系统而言,分区容忍性是基本的要求,要根据需求在一致性和可用性之间寻求平衡。

分布式系统的节点往往都是分布在不同的机器上进行网络隔离开的,这意味着必然会有网络断开的风险,这个网络断开的场景的专业词汇叫着「网络分区」。 

一句话概括 CAP 原理就是——网络分区发生时,一致性和可用性两难全。 

主从复制主要是为了解决主redis出现意外宕机的情况下,从库来接管主库的工作,服务就可以继续。否则master要经过重启和数据恢复的过程,可能需要很长时间,影响线上的业务。

Redis的主从同步是异步的。所以不能保证一致性,但是主节点可以一直提供服务,当主节点挂掉之后,可以让从节点接替主节点的工作继续提供服务,所以满足可用性。

Redis保证最终一致性,从节点会努力追赶主节点,从而从节点的状态和主节点的状态保持一致。

Redis 同步支持主从同步和从从同步。

增量同步

Redis同步的是指令流,主节点将那些对自己状态产生修改的指令写入内存的buffer中,然后异步的将buffer中的指令同步到从节点,从节点获取指令流更新自己的数据,同时向主节点反馈自己同步到哪个位置。

buffer有限,Redis的复制内存是一个环形数组,如果数组满了,从头开始覆盖前面的内容。

问题:网络状态不好,从节点还没来的及更新指令流,但前面的指令流已经被覆盖了,从节点将无法通过指令流来进行同步。

快照同步

快照同步是一个非常耗资源的操作。主节点需要先进行bgsave,将内存的数据全部快照到磁盘文件中,然后全部发给从节点,从节点收到快照之后,先清空当前内存的数据,再全量加载,加载完之后通知主节点进行增量备份。

在进行快照同步时,主节点的数据修改指令写入了buffer,如果buffer过小或者同步时间过长,指令可能被覆盖。无法进行增量同步,需要再次进行快照同步,可能陷入死循环。解决:合理设置buffer的大小。

增加从节点

当从节点刚刚加入到系统时,它必须先要进行一次快照同步,同步完成后再继续进行增量同步。

无盘复制

原因:快照同步时会有很重的IO操作,如果这个时候系统正在进行将AOF刷到磁盘,AOF将被推迟,影响主节点服务效率。

解决:所谓无盘复制是指主服务器直接通过套接字将快照内容发送到从节点,生成快照是一个遍历的过程,主节点会一边遍历内存,一遍将序列化的内容发送到从节点,从节点还是跟之前一样,先将接收到的内容存储到磁盘文件中,再进行一次性加载。

wait指令

Redis 的复制是异步进行的,wait 指令可以让异步复制变身同步复制,确保系统的强一致性 (不严格)

redis常见的问题有哪些?都怎么解决?

  1. master写内存快照:会阻塞主线程的工作,快照较大时性能影响较大,主线程可能暂停服务。master最好不要写内存快照
  2. masterAOF持久化:如果不重写AOF文件,这个持久化对性能的影响是最小的,但AOF文件过大时会影响重启时的恢复速度。所以master最好不要做任何的持久化工作。包括写内存快照和AOF文件,特别是内存快照持久化。如果要持久化数据,可以在某个从节点做AOF数据备份,策略为每秒一次。
  3. master调用BGREWRITEAOF:重写会占用大量系统CPU和资源,导致服务暂停的现象。
  4. redis主从复制的性能问题:为了主从复制的速度和连接的稳定性,主节点和从节点最好在一个局域网内。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值