redis复习总结

redis的数据结构:

string list hash set zset

Redis跳表

链表加上多级索引的结构,称为跳表,应用在Redis ZSet数据结构当中。ZSet要求数据唯一有序,且支持插入删除。

实现

跳表包含zskiplistNode和skiplist两个结构定义。

  1. zskiplistNode表示跳表节点。
  2. zskiplist保存跳表节点的信息。
    • 节点的数量,
    • 表头节点和表尾节点的指针等等。

参考文章

  1. Redis-跳跃表实现
  2. MySQL索引和跳表

redis为什么这么快

单线程多路IO复用模型 纯内存交互无线程切换开销

redis事务模型

使用命令watch & multi & execRedis 参考了 CAS(Compare And Swap)乐观锁.

可以在 multi 命令之前使用 watch 命令监控某些键值对,然后使用 multi 命令开启事务,数据操作命令就会进入队列。redis收到exec 会检测watch监控的是键值对,无变化,执行,提交事务;如果发生变化,那么它不会执行任何事务中的命令,事务回滚。无论事务是否回滚,Redis 都会去取消执行事务前的 watch 命令.

redis 在接受到multi 只是收到事务信号,不会开始执行命令,当收到exec时候才会一次性执行被multi和exec包裹的命令。

redis持久化方式 优缺点

持久化方式有RDB & AFO

  • RDB的提供2种持久化命令 BGSAVESAVE 都会调用rdbSave.

SAVE会阻塞主进程,在快照保存完成前无法处理客户端的任何请求
BGSAVE 会fork出来一个子进程来调用rdbSave,不会干扰主进程。缺点是子进程处理较慢,随着redis的内存占升高,创建子进程耗时会增加可能会导致redis的长时间不可用。

解决办法: 关闭自动保存,在合适的时间自己调用SAVE速率较高,但是会有小段时间的数据丢失。

  • AOF append only fileaof持久化会把被执行写命令写道aof文件的末尾可以记录所有数据变化。恢复数据时候只需要执行一次aof文件即可。

可以设置的参数: always[每条同步一次]/everysec/[每秒一次]/no[系统决定何时写入 类似Java write]

缺点是aof文件可能会过大,需要设置定期重写BG write AOF[距离上次重写增大的% 或者距离上次重写的时间]

redis实现分布式锁

使用SETNX命令[set if not exsit 不存在就设置] ,同时使用expire来设置超时时间。为了防止在设置超时时间时候客户端崩溃导致设置失败,每个客户端在获取锁时候,需要检测下锁是否设置了过期时间,给未设置过期时间的锁设置时间,避免无法释放锁。

简单讲下redis主从复制启动过程

置主从使用配置 SLAVE of host port

  1. 从服务器连接主服务器发送SYNC命令
  2. 主服务器开始执行BGSAVE命令,并使用缓冲区记录快照生成期间的命令
  3. BGSAVE执行完成主服务器通过SCP向从服务器发送快照文件
  4. 从服务器丢弃老数据,开始执行快照的写入
  5. 从服务器完成对快照的解释,开始接受新得请求,同时接收主服务器缓冲区发来的写命令。
  6. 主服务缓冲区命令同步完成开始正常执行接受写命令,没接收一次给从服务器同步一次。

redis如何清除过期key

redis默认过期策略 惰性删除 + 定期删除

支持的过期策略有

  • 定时删除:给每个设置了过期时间的都配置上定时器
  • 定期删除:key的定期删除会在Redis的周期性执行任务(serverCron,默认每100ms执行一次)中进行,而且是发生Redis的master节点,因为slave节点会通过主节点的DEL命令同步过来达到删除key的目的。依次遍历每个db(默认配置数是16),针对每个db,每次循环随机选择20个(ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)key判断是否过期,如果一轮所选的key少于25%过期,则终止迭次,此外在迭代过程中如果超过了一定的时间限制则终止过期删除这一过程。
  • 惰性删除:查询这个数据的时候再判断是否过期需要删除。

内存淘汰策略通过maxmemory-policy volatile-lru配置

redis 内存淘汰机制(内存淘汰策略)有以下几个:

  • noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的 key 给干掉啊。
  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。

如果redis中不存在设置了过期时间的key 那么配置volatile-lru-random-ttl 等效于noeviction 。新加入的数据会报错。

redis的批处理

mset和mget 因为我们的项目使用的 Redis Cluster的集群,不同的key会被
CRC16(key) Mod 16384 到不同的slot上去,直接使mset和mget是不太好的,所以我们一般不使用批处理。

你知道那些redis的高可用方案

Redis常见的三种高可用模式 主从 哨兵 集群 :https://zhuanlan.zhihu.com/p/177000194

主从一般是一主多重

  • 配置主从有2中方式 要么在配置文件中指定 slaveof「表示是谁的主节点」要么使用redis-server命令,在启动从节点时,通过参数--slaveof指定主节点「 ./redis-server --slaveof 192.168.1.10 6379」
  • 系统运行时,如果master挂掉了,可以在一个从库(如slave1)上手动执行命令slaveof no one,将slave1变成新的master;在slave2和slave3上分别执行slaveof 192.168.1.11 6379 将这两个机器的主节点指向的这个新的 master;同时,挂掉的原master启动后作为新的slave也指向新的master上。执行命令slaveof no one命令,可以关闭从服务器的复制功能。同时原来同步的所得的数据集都不会被丢弃。
  • 机器启动 首先启动主节点,然后一台一台启动从节点。

优点
支持主从复制,主机会自动将数据同步到从机,可以进行读写分离;
为了分载Master的读操作压力,Slave服务器可以为客户端提供只读操作的服务,写服务依然必须由Master来完成;
Slave同样可以接受其他Slaves的连接和同步请求,这样可以有效地分载Master的同步压力;
Master是以非阻塞的方式为Slaves提供服务。所以在Master-Slave同步期间,客户端仍然可以提交查询或修改请求;
Slave同样是以阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis则返回同步之前的数据。
缺点
Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复;
主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性;
如果多个Slave断线了,需要重启的时候,尽量不要在同一时间段进行重启。因为只要Slave启动,就会发送sync请求和主机全量同步,当多个Slave重启的时候,可能会导致Master IO剧增从而宕机。
Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂;
redis的主节点和从节点中的数据是一样的,降低的内存的可用性

redis哨兵模式 「 就是主从加上一个 Sentinel」

主从模式下,当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这种方式并不推荐,实际生产中,我们优先考虑哨兵模式。这种模式下,master宕机,哨兵会自动选举master并将其他的slave指向新的master。

Sentinel的作用有三个:

  1. 监控:Sentinel 会不断的检查主服务器和从服务器是否正常运行。「通过发送命令,让Redis服务器返回监控其运行状态」
    通知:当被监控的某个redis服务器出现问题,Sentinel通过API脚本向管理员或者其他的应用程序发送通知。
    自动故障转移:当主节点不能正常工作时,Sentinel会开始一次自动的故障转移操作,它会将与失效主节点是主从关系 的其中一个从节点升级为新的主节点,并且将其他的从节点指向新的主节点。「然后通过发布订阅模式通过其他的从服务器,修改配置文件,让它们切换主机;」

缺点: 主从切换会有部分数据丢失且 不支持扩容,单机内存限制集群上线。

redis哨兵模式+proxy

单纯哨兵模式的升级版 在vip后加入了proxy层 均衡到后端多个master节点。可以支持水平扩容,但运维困难。

集群模式「redis Cluster」

Redis 的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台 Redis 服务器都存储相同的数据,很浪费内存,所以在redis3.0上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,对数据进行分片,也就是说每台 Redis 节点上存储不同的内容;

当前我们项目就使用的这种 关键算法 crc16(Key) mod 16384

Redis集群如何扩缩容
对redis集群的扩容就是向集群中添加机器,缩容就是从集群中删除机器,并重新将16383个slots分配到集群中的节点上(数据迁移)。

扩缩容也是使用集群管理工具 redis-tri.rb。

扩容时,先使用redis-tri.rb add-node将新的机器加到集群中,这是新机器虽然已经在集群中了,但是没有分配slots,依然是不起做用的。在使用 redis-tri.rb reshard进行分片重哈希(数据迁移),将旧节点上的slots分配到新节点上后,新节点才能起作用。

缩容时,先要使用 redis-tri.rb reshard移除的机器上的slots,然后使用redis-tri.rb add-del移除机器。

缺点:
1.Redis Cluster是无中心节点的集群架构,依靠Goss协议(谣言传播)协同自动化修复集群的状态

但 GosSIp有消息延时和消息冗余的问题,在集群节点数量过多的时候,节点之间需要不断进行 PING/PANG通讯,不必须要的流量占用了大量的网络资源。虽然Reds4.0对此进行了优化,但这个问题仍然存在。

2.数据迁移问题

Redis Cluster可以进行节点的动态扩容缩容,这一过程,在目前实现中,还处于半自动状态,需要人工介入。在扩缩容的时候,需要进行数据迁移。

而 Redis为了保证迁移的一致性,迁移所有操作都是同步操作,执行迁移时,两端的 Redis均会进入时长不等的阻塞状态,对于小Key,该时间可以忽略不计,但如果一旦Key的内存使用过大,严重的时候会接触发集群内的故障转移,造成不必要的切换。

项目的持久化方式

master节点关闭持久化
slave开启 AOF 也可以开启AOF&RDB

缓存击穿/缓存雪崩

击穿

  1. 使用锁,当缓存失效时候,先取锁,拿到锁访问数据库,更新缓存。没获取锁返回空。
  2. 在访问缓存前先校验key是否有效,例如使用布隆过滤器,将所有可能的查询key 先映射到布隆过滤器中,查询时先判断key是否有效,有效继续向下执行,无效直接返回。

雪崩

缓存时间不一致,给缓存的失效时间,加上一个随机值,避免集体失效

Redis和 memcached 的区别

  1. redis可以持久化 memcached数据全在内存中断电就是失效。
  2. redis的支持的数据类型更多,memcached只支持k-V
  3. redis的单个value大于memcached

以下是面试以来被问到过的题,当然也包括一些没被问的题,有备无患。

缓存和数据库一致性

先更新数据库再删缓存

常见的四种更新策略

  1. 先更新数据库再更新缓存

    • 线程安全角度:2线程同时更新统一条数据,然后再更新缓存的时候无法保障缓存被更新的时间和数据库更新的时间一致。
  2. 先更新数据库再删缓存 对比操作1上增加了删除操作,可能会删除非脏值,但是不影响一致性。

    • 步骤
      1. 查询:缓存空,查数据库放入缓存。/非空 直接查询缓存拿到数据。
      2. 更新:更新数据库,删除缓存。
    • 极端情况:A读B写 针对统一条数据 此时缓存失效。
      1. A缓存读不到 去数据库拿到旧值。
      2. B写数据库。
      3. B删除缓存。
      4. A将数据库的旧值写入缓存。
    • 极端情况解决:
      1. 要求B写操作比A读操作更快,不现实。
      2. 可在每一次写操作后再加上一次异步删除 双删保平安。
  3. 先删除缓存再更新数据库

    • 若A写数据B读数据,A删除缓存时候B读数据 此时B会读数据库更新缓存。出现脏数据。
    • 解决办法:加上休眠时间。A写完数据库后1S再次删除缓存,但是在休眠时间内依然会出现脏数据。为避免吞吐量降低可以使用异步删除,队列或者异步线程均可。
  4. 先更新缓存再更新数据库

    • AB更新同一条数据 无法保证更新数据库的顺序和更新缓存的顺序一致,且更新缓存成功更新数据库失败无法容错。

Redis cluser 集群原理

集群扩容 & 新增节点

扩容对应着集群新增节点,步骤如下:

  1. 启动redis节点 加入集群设置为Master节点
  2. 为当前增加的master节点增加slave节点[master节点没分配slot前不会实际进行工作]
  3. 准备slot的数据迁移。可以选择 all所有数据源 nodeId指定数据源。也就是reShared操作。
  4. 迁入数据slot状态设置为: IMPROTING
  5. 迁出数据slot状态设置为:MIGRATING
  6. 数据迁移完成,广播更新slot和nodeId的映射信息,扩容完成。
    以上4&5顺序不能乱。
    假设我们有两个 Redis 主节点,分别称为 A 和 B。我们想将哈希槽 8 从 A 移动到 B,因此我们发出如下命令:
  • 迁入节点B: CLUSTER SETSLOT 8 IMPORTING[from] A 在目标节点设置 迁入slot 从源节点。
  • 迁出节点A:CLUSTER SETSLOT 8 MIGRATING [to] B 在迁出的节点设置 移动slot到目标。

若顺序乱且迁出设置迁入还未设置: 客户端访问A对应slot 重定向到B B对应slot找不到数据,且未设置 IMPORTING。会返回moved A & slot.形成死。

初次设置master节点 slot如何分配

在 Redis 集群中,当第一个主节点启动时,它会自动分配所有的槽位(slots)。Redis 集群使用槽位来分割数据,每个槽位可以容纳一个键值对。Redis 集群默认使用 16384 个槽位。

当你启动第一个主节点时,它会自动成为集群的主节点,并负责管理所有的槽位。此时,该节点会将所有槽位都分配给自己。这意味着该主节点将负责处理所有键值对的操作。

具体分配过程如下:

  1. 主节点启动后,它会创建一个包含 16384 个槽位的槽位映射表(slot map)。
  2. 主节点将所有槽位都标记为未分配状态。
  3. 主节点会将自己的节点 ID 添加到槽位映射表中,表示该主节点负责管理这些槽位。
  4. 主节点会将槽位映射表广播给其他节点,让它们知道哪些槽位由该主节点负责。
  5. 其他节点在接收到槽位映射表后,会更新自己的槽位信息,并知道哪些槽位由该主节点负责。
  6. 一旦槽位映射表广播完成,集群就开始正常工作,每个节点都知道自己负责处理哪些槽位的数据。

值得注意的是,如果你后续添加更多的主节点,集群会自动将一些槽位从原始主节点重新分配给新的主节点,以实现数据的平衡和高可用性。这个过程是自动进行的,无需手动干预。

正在扩容的节点如何提供数据访问

迁出的节点

  1. 迁出的slot状态为:MIGRATING
    若待查询数据存在,正常返回,若查询数据正在migrate状态当中,会阻塞等命令结束继续处理客户端请求,若待查询数据不存在,返回ASK以及目标节点的信息。此时客户端会带上ASKING信息去查询目标节点。所有不存在的查询都会重定向到MIGRATING的slot上。

  2. 目标slot状态为:IMPORTING
    节点会接受当前slot所有携带ASKING的查询。若查询是客户端发起,正常返回,若查询非客户端发起,则会返回MOVED&对该次查询绑定真实slot的目标节点。

MOVED & ASK 差异

  1. 如果收到 ASK 重定向,则只发送被重定向到指定节点的查询,但继续向旧节点发送后续查询。
  2. 使用 ASKING 命令启动重定向查询。ASKING 命令在客户端上设置一个一次性标志,强制节点为有关 IMPORTING 插槽的查询提供服务。
  3. 不要更新本地客户端表以将 slot 映射到新的节点上。
       
    从A->B 迁移到B
    一旦哈希槽 8 迁移完成,A 将发送一条 MOVED 消息,客户端可以将哈希槽 8 永久映射到新的 IP 和端口对。请注意,如果有问题的客户端更早地执行映射,这不是问题,因为它不会在发出查询之前发送 ASKING 命令,因此 B 将使用 MOVED 重定向错误将客户端重定向到 A。

客户端更新slot和节点的映射关系

两种情况下,客户端通常需要获取完整的插槽列表和映射的节点地址:

  1. 在启动时填充初始插槽配置。
  2. 当MOVED收到重定向时。

客户端可以通过MOVED仅更新当前slot来处理重定向,但这通常效率不高,因为通常会同时修改多个slot映射.

集群容错 心跳和gossip消息

心跳机制是指redis cluster节点之间不停的交换ping pong信息,节点发送 ping 数据包,这将触发接收者回复 pong 数据包,节点也可以只发送pong不触发回复,ping 和 pong 数据包的总和称为心跳数据包。

故障检测

  1. 前提
    集群节点都确保不超过一半NODE_TIMEOUT时间内和其他所有节点ping/pong一次,NODE_TIMEOUT过去之前,节点还尝试重新连接与另一个节点的 TCP 链接,以确保不会因为当前 TCP 连接存在问题而认为节点不可达。
  2. PFAIL状态
    若当前节点ping某个节点在NODE_TIMEOUT时间结束都没有收到回复,则会被计为PFAILpossible fail 可能失败
  3. PFAIL升级为FAIL
    a. 某个节点,我们称之为 A,将节点 B 标记为PFAIL。
    b. 从集群中大多数主节点的角度来看,节点 A 通过gossip消息收集了有关 B 状态的信息。
    c. 大多数主人及时发出信号PFAIL或FAIL条件NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT。(在当前实现中,有效性因子设置为 2,所以这只是两倍的NODE_TIMEOUT时间)。
    d. a/b/c满足的情况下 B被A标记为FAIL 向所有可达阶节点传递FAIL消息,且所有可达节点对B的的状态被强制标记为FAIL.

FAIL状态清除

  1. 该节点已经可达并且是一个从节点。在这种情况下,FAIL可以清除标志,因为从站没有故障转移。
  2. 该节点已经可以访问并且是一个不为任何slot提供服务的主节点。在这种情况下,FAIL可以清除标志,因为没有slot的主节点并没有真正参与集群,并且正在等待配置以加入集群。
  3. 该节点已经可以访问并且是一个主节点,但是很长一段时间(N 倍NODE_TIMEOUT)过去了,没有任何可检测的从节点提升。在这种情况下,最好重新加入集群并继续。

节点重新加入集群

  1. 当失效节点重新加入集群,且向其他节点发送心跳广播自己对slot的映射关系时,其他节点看到相同slot有当前更新的从属关系时候,UPDATE slot的新配置向 A发送消息,A更新配置。
  2. 当前master 会成为他FAIL前所管理的最后一个slot,现在对应的master节点的slave.
  3. 重新成为slave前会将映射的slot信息逐渐清空。

选举机制

选举条件:

  1. master节点处于FAIL状态。
  2. master节点对非零数量的slot提供服务。
  3. slave和master最近一次通讯时间不超过设定时间。

选举过程:

  1. 一旦 master 处于FAIL状态,slave 会在尝试被选举之前等待一小段时间,避免其他master还没收到当前master的FAIL状态而拒绝投票。
  2. slave 增加计数器currentEpoch,广播FAILOVER_AUTH_REQUEST信息,在2倍NODE_TIMEOUT等待返回。
  3. 在时间内其余master只能给同一master下的其中一个slave回复FAILOVER_AUTH_ACK。
  4. 收到FAILOVER_AUTH_ACK 超过多数的slave将成为新的master.若当前没有任何一个slave得到超过一半选票,那么尝试新的选举,且选举时间为上一次的2倍。

副本迁移算法

若某一段slot对应的master失效,slave成为新的master,且当前无新的slave. 集群将尝试从slave最多的master节点当中,将较低节点 ID 的非故障slave迁移为当前master的slave,以保证集群副本的动态平衡。

参考文档

  1. 官方文档:Redis Cluster Specification
  2. 官方文档:Redis cluster tutorial
  3. 社区沙龙:腾讯会议用户暴涨 Redis集群的无缝扩容
  4. 公司实践:前东家软测项目 Redis实践。
posted @ 2020-10-16 18:25  刘三茶  阅读(259)  评论(0编辑  收藏  举报