Problem-Based Learning - Redis

对应关系

image.png

应用场景

Redis 五种数据类型的应用场景:

  • String 类型的应用场景:缓存对象、常规计数、分布式锁、共享session信息等。
  • List 类型的应用场景:消息队列(有两个问题:1. 生产者需要自行实现全局唯一 ID(避免重复消费);2. 不能以消费组形式消费数据)等。
  • Hash 类型:缓存对象、购物车等。
  • Set 类型:**聚合计算(并集、交集、差集)场景,**比如点赞、共同关注、抽奖活动等。
  • Zset 类型:排序场景,比如排行榜、电话和姓名排序等。

结构

quicklist

image.png
image.png
ziplist:
image.png

  1. previous_entry_length: 这个属性记录的是上一个结点的长度
  2. encoding: 这个属性,记录着content的数据类型(同时通过它还可以知道当前结点的长度,以此可以找到下一个结点)
  3. content: 这个属性是负责保存结点数据的,当然,这个数据包括字符串或者整数。

listpack

Ziplist由于元素的previous_entry_length字段大小是动态变化的,所以有可能会产生连锁更新。
那该如何优化这个问题呢?
优化这个问题,其实我们只需要考虑previous_entry_length的实际作用,然后找个方式将其替代就好。
previous_entry_length只是用来做遍历时判断当前位置,那如果我们直接记录当前元素的总长度不就可以了吗?如果要考虑逆向遍历,直接从结尾标识符开始逆向遍历就可以了

struct listpack<T>{
    int32 total_bytes; // 总字节数
    int16 size;        // 元素个数
    T[] entries;       // 元素列表
    int8 end;          //与zlend一样,结尾表示符
}


Listpack采用len记录当前节点总长度,替代Ziplist中previous_entry_length记录上一节点长度,解决了Ziplist的连锁更新问题。

intset

image.png

SDS

说说SDS好的地方
空间预分配和惰性释放

  • 对SDS 进行修改之后,如果其的长度 newlen < 1MB,那么总分配长度 alloc 将等于 2 * newlen,即 free = len。举个例子,如果进行修改之后,SDS 的 len 将变成13字节,那么程序也会分配 13 字节的未使用空间,SDS的 buf 数组的实际长度将变成 13 + 13 + 1 = 27 字节(额外的一字节用于保存空字符)。
  • 对 SDS 进行修改之后,如果其的长度 newlen >= 1MB,那么将会会分配 1MB 的未使用空间。举个例子,如果进行修改之后,SDS 的 len 变成 30MB,那么程序会分配 1MB 的未使用空间,SDS 的 buf 数组的实际长度将为 30MB + 1MB + 1byte。

惰性释放时机?
未使用大小达到一半时
二进制安全 不以\0结尾

数据类型

String

使用int和SDS实现。
int:
image.png
SDS:
image.png
image.png

List

使用quicklist实现
image.png

Hash

  • 如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用listpack作为 Hash 类型的底层数据结构;
  • 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的 底层数据结构。

Set

无序集合
底层由哈希表或者整数集合(集合中的元素都是整数并且小于512个)

ZSet

有序集合
listpack 或者跳表实现

  • 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
  • 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;

Bitmap

普通的位图

HyperLogLog

统计基数的数据集合类型。
提供不精确的去重。

GEO

GeoHash实现的存储
使用Zset

分布式

哨兵模式/故障转移

Redis哨兵模式是怎么实现故障转移的?
哨兵模式引入了一个Sentinel系统去监视主服务器及其所属的所有从服务器。一旦发现有主服务器宕机后,会自动选举其中的一个从服务器升级为新主服务器以达到故障转义的目的。同样的Sentinel系统也需要达到高可用,所以一般也是集群,互相之间也会监控。而Sentinel其实本身也是一个以特殊模式的Redis服务器。
流程:
1.Sentinel与主从服务器建立连接

  • Sentinel服务器启动之后便会创建于主服务器的命令连接,并订阅主服务器的**sentinel:hello频道以创建订阅连接**
  • Sentinel默认会每10秒向主服务器发送 INFO 命令,主服务器则会返回主服务器本身的信息,以及其所有从服务器的信息。
  • 根据返回的信息,Sentinel服务器如果发现有新的从服务器上线后也会像连接主服务器时一样,向从服务器同时创建命令连接与订阅连接

2.判定主服务器是否下线
每一个Sentinel服务器每秒会向其连接的所有实例包括主服务器,从服务器,其他Sentinel服务器)发送 PING命令,根据是否回复 PONG 命令来判断实例是否下线。
判定主观下线
如果实例在收到 PING命令的down-after-milliseconds毫秒内(根据配置),未有有效回复。则该实例将会被发起 PING命令的Sentinel认定为主观下线。
判定客观下线
当一台主服务器没某个Sentinel服务器判定为客观下线时,为了确保该主服务器是真的下线,Sentinel会向Sentinel集群中的其他的服务器确认,如果判定主服务器下线的Sentinel服务器达到一定数量时(一般是N/2+1),那么该主服务器将会被判定为客观下线,需要进行故障转移。
3.选举领头Sentinel
当有主服务器被判定客观下线后,Sentinel集群会选举出一个领头Sentinel服务器来对下线的主服务器进行故障转移操作。整个选举其实是基于RAFT一致性算法而实现的,大致的思路如下:

  • 每个发现主服务器下线的Sentinel都会要求其他Sentinel将自己设置为局部领头Sentinel。
  • 接收到的Sentinel可以同意或者拒绝
  • 如果有一个Sentinel得到了半数以上Sentinel的支持则在此次选举中成为领头Sentinel。
  • 如果给定时间内没有选举出领头Sentinel,那么会再一段时间后重新开始选举,直到选举出领头Sentinel。

4.选举新的主服务器
领头服务器会从从服务中挑选出一个最合适的作为新的主服务器。挑选的规则是:

  • 选择健康状态的从节点,排除掉断线的,最近没有回复过 INFO命令的从服务器。
  • 选择优先级配置高的从服务器
  • 选择复制偏移量大的服务器(表示数据最全)

挑选出新的主服务器后,领头服务器将会向新主服务器发送 SLAVEOF no one命令将他真正升级为主服务器,并且修改其他从服务器的复制目标,将旧的主服务器设为从服务器,以此来达到故障转移。

主从模式/重同步

说一下Redis的主从模式
redis使用主从的经典设计:
image.png

  • 读操作:主库、从库都可以执行处理;
  • 写操作:先在主库执行,再由主库将写操作同步给从库。

Redis的重同步怎么做的
Redisd的采用同步异步混合进行的模式,在Redis中被称为完整重同步和部分重同步。
image.png

  • 完整重同步用于初次复制的情况,通过让主服务器发送RDB文件,以及向从服务器发送保存在缓冲区中的写命令来同步。
  • 部分重同步处理断线后重复制的情况,主服务器将从服务器断开后执行的写命令发送到从服务器。偏移量通过运行ID来得到。

集群模式的哈希槽怎么做的
**Redis Cluster的整个数据库将会被分为16384个哈希槽,数据库中的每个键都属于这16384个槽中的其中一个,集群中的每个节点可以处0个或者最多16384个槽。**Redis作者认为集群在一般情况下是不会超过1000个实例,取了16384个,即可以将数据合理打散至Redis集群中的不同实例,又不会在交换数据时导致带宽占用过多

计算键属于哪个槽
**CRC16(key) & 16383**

说下集群模式的查找流程

  1. 当客户端发起对键值对的操作指令后,将任意分配给其中某个节点
  2. 节点计算出该键值所属插槽
  3. 判断当前节点是否为该键所属插槽
  4. 如果是的话直接执行操作命令
  5. 如果不是的话,向客户端返回moved错误,moved错误中将带着正确的节点地址与端口,客户端收到后可以直接转向至正确节点

重新平衡分区过程中,向Redis发送指令会发生什么?
image.png
如果已经迁移完成,会发送MOVED指令指向有相应槽的节点。ask指令客户端不会更新缓存,MOVED指令客户端会更新缓存。

脑裂

Redis集群模式如何解决脑裂问题?
脑裂的主要原因其实就是哨兵集群认为主节点已经出现故障了,重新选举其它从节点作为主节点,而原主节点其实是假故障,从而导致短暂的出现两个主节点,那么在主从切换期间客户端一旦给原主节点发送命令,就会造成数据丢失。
所以应对脑裂的解决办法应该是去限制出现网络分区的原主库接收请求,Redis提供了两个配置项。

  • min-slaves-to-write:与主节点通信的从节点数量必须大于等于该值主节点,否则主节点拒绝写入。
  • min-slaves-max-lag:主节点与从节点通信的ACK消息延迟必须小于该值,否则主节点拒绝写入。

这两个配置项必须同时满足,不然主节点拒绝写入。
在假故障期间满足min-slaves-to-write和min-slaves-max-lag的要求,那么主节点就会被禁止写入,脑裂造成的数据丢失情况自然也就解决了。

一致性

如何保持DB-Cache一致性?
新增数据
**新增数据时 ,**写⼊数据库;访问数据时,缓存缺失,查数据库,更新缓存。
这个过程不会有不一致的问题。
修改数据
先修改数据库,再删除缓存。
订阅binlog日志或者用kafka保证缓存删除成功。这里只需要保证消费就可以了,不需要保证幂等。

为什么是删除缓存而不是更新缓存?
因为删除操作更加轻量级,减少不一致的时间,而且缓存并不是一定能用上的

分布式锁

如何实现分布式锁?
使用redission或者zookeeper
image.png
**Redlock有什么问题?
持久化机制导致重复加锁 – 通过马上刷盘解决
主从下重复加锁 – 同步的时候挂了 通过设置全同步解决 【Redission也有这个问题】
时钟跳跃导致重复加锁 – 通过集群zk服务同步时间解决

这里我们也可以看出redis设计上的致命缺陷:

  1. 缺乏预写日志,导致刷盘代价高
  2. 缺乏半同步和全同步,导致最终一致不好保障

Lua脚本有原子性吗?
Lua脚本同样不能进行回滚。lua脚本的意义在于:在执行一个lua脚本时,不会有其它lua脚本或者命令在运行。

热k 大k 缓存穿透/击穿/雪崩

热k和大k怎么办

  1. 通过中间件分析找出热k和大k
  2. 拆分或者使用unlink删除大k
  3. 热k 多级缓存 负载均衡

缓存穿透怎么解决
穿透是数据库和redis都没有
可以加布隆过滤器、业务层校验、缓存空值等

缓存雪崩怎么解决
设置过期时间均匀分布
服务降级,直接从配置中心获取数据

缓存击穿怎么解决
击穿是击穿到了数据库
设置热点数据不过期
定时更新热点key
**数据库加载时上锁 **

持久化

AOF和重写

AOF的回写策略
image.png

AOF(Append only)的重写策略
在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」
image.png
当子进程完成 AOF 重写工作(扫描数据库中所有数据,逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志)后,会向主进程发送一条信号,信号是进程间通讯的一种方式,且是异步的。
主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作:

  • 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致;
  • 新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。

RDB和AOF的不同

  • AOF 文件的内容是操作命令;
  • RDB 文件的内容是二进制数据。
  • 可以使用混合持久化策略加快恢复速度

删除和内存淘汰策略

**删除策略?
Redis 使用的过期删除策略是「惰性删除+定期删除」这两种策略配和使用。

内存淘汰策略?
Redis 内存淘汰策略共有八种,这八种策略大体分为「不进行数据淘汰」和「进行数据淘汰」两类策略。
1、不进行数据淘汰的策略
noeviction(Redis3.0之后,默认的内存淘汰策略) :它表示当运行内存超过最大设置内存时,不淘汰任何数据,而是不再提供服务,直接返回错误。
2、进行数据淘汰的策略
针对「进行数据淘汰」这一类策略,又可以细分为「在设置了过期时间的数据中进行淘汰」和「在所有数据范围内进行淘汰」这两类策略。 在设置了过期时间的数据中进行淘汰:

  • volatile-random:随机淘汰设置了过期时间的任意键值;
  • volatile-ttl:优先淘汰更早过期的键值。
  • volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值;
  • volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;

在所有数据范围内进行淘汰:

  • allkeys-random:随机淘汰任意键值;
  • allkeys-lru:淘汰整个键值中最久未使用的键值;
  • allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值