跳至主要內容

redis

HeChuangJun约 31627 字大约 105 分钟

1. 什么是 Redis?

  • Redis:Remote Dictionary Service 基于键值对(key-value)的 NoSQL 数据库。
  • Redis 中的 value 支持 string(字符串)、hash(哈希)、 list(列表)、set(集合)、zset(有序集合)、Bitmaps(位图)、HyperLogLog(基数估算)、GEO(地理信息定位)等多种数据结构。
  • 因为 Redis 的所有数据都存放在内存当中,所以它的读写性能非常出色。
  • Redis 还可以将内存数据持久化到硬盘上,这样在发生类似断电或者机器故障的时候,内存中的数据并不会“丢失”。
  • Redis 还提供了键过期、发布订阅、事务、流水线、Lua 脚本等附加功能,是互联网技术领域中使用最广泛的缓存中间件。

Redis 和 MySQL 的区别?

  • Redis:数据存储在内存中的 NoSQL 数据库,读写性能非常好。
  • MySQL:数据存储在硬盘中的关系型数据库,适用于需要事务支持和复杂查询的场景

2. 单线程 Redis 的 QPS 是多少?

  • Redis的QPS(Queries Per Second每秒查询率)受多种因素影响,包括硬件配置(如 CPU、内存、网络带宽)、数据模型、命令类型、网络延迟等
    。一个普通服务器的 Redis 实例通常可以达到每秒数万到几十万的 QPS。
  • 基准测试命令:redis-benchmark -h 127.0.0.1 -p 6379 -c 50 -n 10000(-c:并发连接数,-n:请求总数)

3. redis使用场景?

  • 缓存:减轻数据库查询压力,提高应用的响应速度和吞吐量
  • 分布式锁:控制跨多个进程或服务器的资源访问。
  • 热门列表与排行榜:利用 zSet(有序集合)根据score实现
  • Session共享:集群服务的同一个用户session可能落在不同机器上,这会导致用户频繁登陆;采用Redis保存Session后,无论用户落在那台机器上都能够获取到对应的Session
  • 计数器/限速器:利用Redis中原子性的自增操作,统计类似用户点赞数、用户访问数等。限速器比较典型的使用场景是限制某个用户访问某个API的频率
  • 好友关系:利用集合的一些命令,比如求交集、并集、差集等。可以方便解决一些共同好友、共同爱好之类的功能;
  • 消息队列:利用List来实现一个队列机制,异步解耦,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的DB压力;
  • 数据过期处理: 订单30分钟自动取消
  • 交集、并集和差集
  • 发布/订阅
  • 数据持久化
  • 事务

4. Redis 支持的数据类型有哪些?什么场景?相关指令?

  • String:存储字符串(JSON、XML、token)、数字、二进制(图片、音频、视频),但最大不能超过512MB。用于缓存、计数、共享Session、限速
# 设置key的value并则覆盖
set key value 
# 获得key的String值
get key
# 将指定的key的value原子性的递增/减1
incr/decr key
# 将指定的key的value原子性的递增/减crement
incrby/decrby key increment/decrement
  • list:字符串有序集合,可重复,在链表的两头插入或删除元素(高效),模拟队列,堆,栈 ,关注列表,粉丝列表,消息队列
# 在list头/尾部添加多个values,key不存在就创建,成功返回个数,移除时返回个数为0则键删除
lpush/rpush key value1 value2.. 
# 获取链表中从start到end的元素的值lrange key 0 -1 查全部
lrange key start end 
# 返回并弹出链表中的头/尾部value。删除了
lpop/rpop key
# 返回指定链表中的value数量 
llen key 
  • set:字符串无序集合、不重复,多个Sets之间的聚合计算操作效率高。点赞功能。共同关注、二度好友、标签
# 向set中添加数据,如果该key的值已有则不会重复添加,点赞(/收藏)
sadd key value1、value2…
# 删除set中指定的成员 取消点赞(/收藏)
srem key member1、member2…
# 获取set中所有的成员 获取所有点赞(/收藏)用户  
smembers key
# 获取set中成员的数量 获取点赞用户数量
scard key
# 判断参数中指定的成员是否在该set中,1存在,0不在或者key本身就不在 判断是否点赞(/收藏) 
sismember key member
# 随机返回set中的一个成员
srandmember key
# 与key的顺序有关。返回差集”相减“
sdiff key1 key2
# 将key1、key2相差的成员存储在destination上
sdiffstore destination key1 key2
# 返回交集
sinter key key1,key2…
# 将返回的交集存储在destination上
sinterstore destination key1 key2
# 返回并集
sunion key1、key2
# 将返回的并集存储在destination上
sunionstore destination key1 key2
  • sorted set:有序集合。元素唯一,通过分数进行从小到大的排序。添加、删除或更新一个成员的时间复杂度为集合中成员数量的对数。效率高。(top-n)排名:排名/排行榜。
# 将所有成员以及该成员的分数存放到sorted-set中
zadd key score member score2 member2 …
# 移除集合中指定的成员,可以指定多个成员。返回删除的数量
zrem key member[member…]
# 设置指定成员的增加的分数increment。并返回该成员更新后的分数(分数改变后相应它的index也会改变)
zincrby key increment member
# 获取集合中的成员数量
zcard key
# 获取分数在[min,max]之间的成员数量
zcount key min max
# 获取集合中脚标为start-end的成员及分数/倒序 【0-1】表示返回所有成员
zrange/zrerange key start end [withscores]
# 获取集合中分数为min max的成员及分数,并低到高排序
zrangebyscore key min max [withscores] [limit offset count]
# 返回成员在集合中的位置排名,从小到大
zrank key member
# 返回成员在集合中的位置排名,从大到小
zrevrank key member
# 返回指定成员的分数
zscore key member
  • hash:键值对集合,用于保存对象(用户信息)、分组,缓存购物车对象
# 为指定的key设定field/value对(键值对)
hset key field value
# 返回指定的key中的field的值
hget key field
# 获取key中的所有filed-vaule 
hgetall key
# 删除键为 key 的哈希表中的一个或多个字段
HDEL key field
# 判断指定的key中的filed是否存在
hexists key field value
# 获取key所包含的field的数量
hlen key
# 设置key中filed的值增加increment
hincrby key field increment
# 获取所有与pattern匹配的,key * 表示多个字符,?表示任意一个字符
key keys pattern
# 取出所有的key
Key *
# 删除指定key
del kye1  key2
# 判断key是否存在,1在,0不在
exists key
# 为当前的key重命名
rename key newkey 
# 设置过期时间
expire key second
# 获取该key所剩超时时间,没超时则-1
ttl key
# 返回key 的类型,不存在返回none
type key
# 清除key的过期时间。Key持久化。-1是永久保存 -2
persist key
  • Bitmap:通过一个bit 数组来存储特定数据的一种数据结构,每一个bit位都能独立包含信息,大量节省空间。
  • 应用场景:判断海量用户中某个用户是否在线,用户每个月的签到情况,连续签到用户总数
# 判断用户是否在线 key=login_status表示存储用户登陆状态集合数据,将用户ID作为offset,在线就设置为1,下线设置0
SETBIT <key> <offset> <value> // 设置用户是否已登陆
GETBIT <key> <offset>       // 获取用户是否在线

# 统计用户每个月打开情况 key = uid:sign:{userId}:{yyyyMM},月份的每一天的值-1作为 offset(因为offset从0开始)
SETBIT uid:sign:89757:202105 15 1  // 表示记录用户在 2021 年 5 月 16 号打卡
GETBIT uid:sign:89757:202105 15    // 编号 89757 用户在 2021 年 5 月 16 号是否打卡。
BITCOUNT uid:sign:89757:202105     // 统计该用户在 5 月份的打卡次数,使用 BITCOUNT 指令
BITPOS uid:sign:89757:202105 1     // 获取 userID = 89757 在 2021 年 5 月份首次打卡日期

# 统计连续签到用户总数(连续7天) key = {yyyyMMdd}, offset=userId 打卡则设置成1。key对应的集合的每个bit位的数据则是一个用户在该日期的打卡记录。对这7个Bitmap的对应的bit位做与运算。
# 一个一亿个位的Bitmap大约占12MB的内存(10^8/8/1024/1024),7天的Bitmap占用84 MB。同时最好给Bitmap设置过期时间,让Redis删除过期的打卡数据,节省内存
SETBIT 20210500 userId 1                //记录用户在2021年5月1日打卡
BITOP AND[OR、NOT、XOR] destinationmap 20210500 20210501...  //记录用户在2021年5月1-7日连续打卡的userId和状态到destinationmap
BITCOUNT destinationmap               //查询destinationmap连续打卡总人数

- HyperLogLog、Geo、Bitmap、BloomFilter、RedisSearch、Redis-ML、JSON

5. Redis为什么单线程那么快?

  • 基于内存的数据存储,内存的访问速度远超硬盘,
  • 单线程模型,避免了线程切换和锁竞争带来的消耗。
  • IO 多路复⽤,基于 Linux 的 select/epoll 机制。该机制允许内核中同时存在多个监听套接字和已连接套接字,内核会一直监听这些套接字上的连接请求或者数据请求,一旦有请求到达,就会交给 Redis 处理,就实现了所谓的 Redis 单个线程处理多个 IO 读写的请求。
  • 高效的数据结构。如字符串(String)、列表(List)、集合(Set)、有序集合(Sorted Set)等

6. I/O 多路复用?

  • I/O多路复用允许单个线程或进程同时监控多个I/O流(如文件或网络连接),并在有数据可读或可写时进行相应的处理,从而高效地利用系统资源。
  • Linux 系统有三种方式实现 IO 多路复用:select、poll 和 epoll。
  • epoll 方式是将用户 socket 对应的 fd 注册进 epoll,然后 epoll 帮你监听哪些 socket 上有消息到达,这样就避免了大量的无用操作。此时的 socket 应该采用非阻塞模式。这样,整个过程只在进行 select、poll、epoll 这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的 reactor 模式。

7. Redis 是单线程的吗?

  • Redis单线程指的是网络I/O线程以及Set和Get操作是单线程的。但是Redis的持久化、集群同步还是使用其他线程来完成
  • Redis 4.0 之后多线程是用来处理数据的读写和协议解析,但是 Redis执行命令还是单线程的。这样做的⽬的是因为 Redis 的性能瓶颈在于⽹络 IO ⽽⾮ CPU,使⽤多线程能提升 IO 读写的效率,从⽽整体提⾼ Redis 的性能。
    redismultiplethread.png

9. Redis有哪几种持久化方式?有什么区别?

  • 【全量】Rdb持久化(默认):通过创建数据的快照来保存数据。定期把内存中当前时刻内存中的数据以二进制的方式保存到磁盘。产生的数据文件为dump.rdb。数据恢复时直接将RDB文件读入内存完成恢复。实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储.可通过 save 和 bgsave 命令两个命令来手动触发 RDB 持久化操作

    • save命令会同步地将Redis的所有数据保存到磁盘上的一个RDB文件中。会阻塞所有客户端请求。不推荐在生产环境中使用
    • bgsave命令:fork出一个子进程,在后台异步地创建 Redis 的数据快照,并将快照保存到磁盘上的 RDB 文件中。这个命令会立即返回,不阻塞客户端请求。在生产环境中执行 RDB 持久化的推荐方式。
  • 【增量】AOF持久化:通过记录每个写入命令来保存数据。执行命令并将写操作记录日志到追加到aof文件中,数据恢复时则需要将全量AOF日志都执行一遍。工作流程

    • 命令写入(append):当AOF功能开启,Redis会将接收到的所有写命令追加到 AOF 缓冲区(buffer)的末尾。
    • 文件同步(sync):将缓冲区中的命令持久化到磁盘中的 AOF 文件,同步策略包括:
      • always:每次写命令都会同步到AOF文件,安全性搞,但可能因为磁盘 I/O 的延迟而影响性能。
      • everysec(默认):每秒同步一次,提供了较好的性能和数据安全性。如果系统崩溃,最多可能丢失最后一秒的数据。
      • no:只会在AOF关闭、Redis关闭或由操作系统内核触发。如果宕机,那么丢失的数据量由操作系统内核的缓存冲洗策略决定
    • 文件重写(rewrite):随着操作的不断执行,AOF 文件会不断增长,为了减小 AOF 文件大小,Redis重写AOF文件:
      • 重写过程将当前内存中的数据库状态转换为一系列写命令,然后保存到新的AOF文件.由 BGREWRITEAOF 命令触发,它会创建一个子进程来执行重写操作,因此不会阻塞主进程。重写过程中,新的写命令会继续追加到旧的 AOF 文件中,同时也会被记录到一个缓冲区中。一旦重写完成,Redis 会将这个缓冲区中的命令追加到新的 AOF 文件中,然后切换到新的 AOF 文件上,以确保数据的完整性。
    • 重启加载(load):当Redis服务器启动时,如果配置为使用AOF持久化方式,它会读取 AOF 文件中的所有命令并重新执行它们,以恢复数据库的状态
# aof
appendonly no //表示是否开启AOF持久化策略(默认no,关闭)
appendfilename "appendonly.aof"//持久化文件名称

10. rdb持久化触发条件

  • 在 Redis 配置文件(通常是 redis.conf)中,可以通过save <seconds> <changes>指令配置自动触发 RDB 持久化的条件。这个指令可以设置多次,每个设置定义了一个时间间隔(秒)和该时间内发生的变更次数阈值。
save 900 1 如果至少有 1 个键被修改,900 秒后自动触发一次 RDB 持久化。
save 300 10 如果至少有 10 个键被修改,300 秒后自动触发一次 RDB 持久化。
save 60 10000 如果至少有 10000 个键被修改,60 秒后自动触发一次 RDB 持久化。
  • 通过 SHUTDOWN 命令正常关闭时执行 一次RDB 持久化,以确保数据在下次启动时能够恢复。
  • 在 Redis 复制场景中,当一个 Redis 实例被配置为从节点并且与主节点建立连接时,它可能会根据配置接收主节点的 RDB 文件来初始化数据集。这个过程中,主节点会在后台自动触发 RDB 持久化,然后将生成的 RDB 文件发送给从节点。

11. RDB 和 AOF 各自有什么优缺点?

  • RDB文件相对小,恢复速度快,但实时性差,数据容易丢失,备份时间长。适合全量备份
  • AOF文件相对大,恢复速度慢,但实时性好,数据不易丢失,数据写入性能高,适合增量备份

12. 如何选择合适的持久化方式?

  • 用AOF做增量持久化,保证数据不丢失; 用RDB全量持久化,做不同程度的冷备;因为RDB耗时长,在停机的时候会导致大量丢失数据,所以需要AOF来配合使用。

13. Redis 的数据恢复?

  • 把RDB或者AOF文件拷贝到 Redis 的数据目录下,如果使用 AOF 恢复,配置文件开启 AOF,然后启动 redis-server 即可
  • Redis 启动时加载数据的流程:
    • AOF 持久化开启且存在 AOF 文件时,优先加载 AOF 文件。
    • AOF 关闭或者 AOF 文件不存在时,加载 RDB 文件。
    • 加载 AOF/RDB 文件成功后,Redis 启动成功。
    • AOF/RDB 文件存在错误时,Redis 启动失败并打印错误信息

14. Redis 4.0 的混合持久化了解吗?√

  • 在 Redis 4.0 版本中,混合持久化模式会在 AOF 重写的时候同时生成一份 RDB 快照,然后将这份快照作为 AOF 文件的一部分,最后再附加新的写入命令。当需要恢复数据时,Redis 先加载 RDB 文件来恢复到快照时刻的状态,然后应用 RDB 之后记录的 AOF 命令来恢复之后的数据更改,既快又可靠。

15. redis如何实现高可用?

  • 主从复制(Master-Slave Replication):允许一个 Redis 服务器(主节点)将数据复制到一个或多个 Redis 服务器(从节点)。可以实现读写分离,适合读多写少的场景。
  • 哨兵模式(Sentinel):用于监控主节点和从节点的状态,实现自动故障转移和系统消息通知。如果主节点发生故障,哨兵可以自动将一个从节点升级为新的主节点,保证系统的可用性。
  • 集群模式(Cluster):Redis 集群通过分片的方式存储数据,每个节点存储数据的一部分,用户请求可以并行处理。集群模式支持自动分区、故障转移,并且可以在不停机的情况下进行节点增加或删除。

16. 什么是主从复制?

  • 指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为主节点(master),后者称为从节点(slave)。且数据的复制是单向的,只能由主节点到从节点。Redis 主从复制支持 主从同步 和 从从同步 两种

17. 主从复制主要的作用?

  • 数据备份:实现数据的热备份,是持久化之外的一种数据冗余方式。
  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复 (服务冗余)。
  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务,分担服务器负载。尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
  • 不推荐在Redis中使用读写分离。主要有两个原因:
    • Redis Sentinel只保证主节点的故障的失效转移,如Jedis只监听了主节点的变化,但是从节点故障时是不进行处理的。导致Jedis读会访问到从节点。Redisson库功能强大,已经支持从节点的故障监听
    • 如果到达需要读写分离的体量,大量写操作考虑Redis Cluster方案更可靠

18. Redis 主从有几种常见的拓扑结构?

  • Redis的复制拓扑结构可以支持单层或多层复制关系,根据拓扑复杂性可以分为
  • 一主一从结构:用于主节点出现宕机时从节点提供故障转移支持。
  • 一主多从结构:(星形拓扑结构)使得应用端可以利用多个从节点实现读写分离。对于读占比较大的场景,可以把读命令发送到从节点来分担主节点压力。
  • 树状主从结构(树状拓扑结构)使得从节点不但可以复制主节点数据,同时可以作为其他从节点的主节点继续向下层复制。通过引入复制中间层,可以有效降低主节点负载和需要传送给从节点的数据量。
    redismasterslavestructure.png
    redismasterslavestructure2.png

19. Redis 的主从复制原理

  • 保存主节点(master)信息:只是保存主节点信息,ip 和 port。
  • 主从建立连接:从节点(slave)发现新的主节点后,会尝试和主节点建立网络连接。
  • 发送 ping 命令:从节点发送 ping 请求进行首次通信,主要是检测主从之间网络套接字是否可用、主节点当前是否可接受处理命令。
  • 权限验证:如果主节点要求密码验证,从节点必须正确的密码才能通过验证。
  • 同步数据集:主从复制连接正常通信后,主节点会把持有的数据全部发送给从节点。
  • 命令持续复制:接下来主节点会持续地把写命令发送给从节点,保证主从数据一致性
    redismasterslaveprinciple.png

20. 说说主从数据同步的方式?

  • Redis 在 2.8 及以上版本使用 psync 命令完成主从数据同步,同步过程分为:全量复制和部分复制。
  • 全量复制:用于初次复制场景,把主节点全部数据一次性发送给从节点,当数据量较大时,会对主从节点和网络造成很大的开销
    fullreplication.png
    • 建立连接:从服务器和主服务器建立连接,从服务器执行replicaof命令并发送 psync 命令进行数据同步,由于是第一次进行复制,从节点没有复制偏移量和主节点的运行 ID,所以发送 psync ? -1。
    • 主节点根据 psync ? -1 解析出当前为全量复制,回复+FULLRESYNC 响应。
    • 从节点接收主节点的响应数据保存运行 ID 和偏移量 offset
    • 主节点执行 bgsave 保存 RDB 文件到本地
    • 主节点发送 RDB 文件给从节点,从节点把接收的 RDB 文件保存在本地并直接作为从节点的数据文件
    • 对于从节点开始接收 RDB 快照到接收完成期间,主节点仍然响应读写命令,因此主节点会把这期间写命令数据保存在复制客户端缓冲区内,当从节点加载完RDB文件后,主节点再把缓冲区内的数据发送给从节点,保证主从之间数据一致性
    • 从节点接收完主节点传送来的全部数据后会清空自身旧数据并开始加载 RDB 文件
    • 从节点成功加载完 RDB 后,如果当前节点开启了 AOF 持久化功能, 它会立刻做 bgrewriteaof 操作,为了保证全量复制后 AOF 持久化文件立刻可用。
  • 部分复制:使用 psync{runId}{offset}命令实现。当从节点(slave)正在复制主节点 (master)时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向 主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区内存在这部分数据则直接发送给从节点,这样就可以保持主从节点复制的一致性。
    partialreplication.png
    • 当主从节点之间网络出现中断时,如果超过 repl-timeout 时间,主节点会认为从节点故障并中断复制连接
    • 主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,不过主节点内部存在的环形复制积压缓冲区repl_backlog_buffer,依然可以保存最近一段时间的写命令数据,默认最大缓存 1MB。所以当缓冲区写满后,主服务器继续写入的话,就会覆盖之前的数据。用于主从服务器断连后,从中找到差异的数据;主服务器使用 master_repl_offset 来记录自己写到的位置,从服务器使用 slave_repl_offset 来记录自己读到的位置
    • 当主从节点网络恢复后,从节点会再次连上主节点
    • 当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行 ID。因此会把它们当作 psync 参数发送给主节点,要求进行部分复制操作。
    • 主节点接到 psync 命令后首先核对参数 runId 是否与自身一致,如果一 致,说明之前复制的是当前主节点;之后根据参数 slave_repl_offset 和slave_repl_offset在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送+CONTINUE 响应,表示可以进行部分复制。否则全量复制
    • 主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。
  • repl_backlog_buffer缓冲区尽可能的大一些,减少出现从服务器要读取的数据被覆盖的概率,可以根据公式估算second (从服务器断线后重新连接上主服务器所需的平均时间(秒))* write_size_per_second(主服务器平均每秒产生的写命令数据量大小)修改redis.conf的repl-backlog-size 1mb 即可

21. 主从复制存在哪些问题呢?

  • 没法完成自动故障转移:一旦主节点出现故障,需要手动将一个从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令其他从节点去复制新的主节点,整个过程都需要人工干预。(高可用)
  • 主节点的写和存储能力受到单机的限制。(分布式)

22. Redis Sentinel(哨兵)了解吗?

  • 解决了主从复制的没法自动故障转移问题.由两部分组成,哨兵节点和数据节点
    • 哨兵节点: 哨兵系统由一个或多个哨兵节点组成,不存储数据,对数据节点进行监控。
    • 数据节点: 主节点和从节点都是数据节点;
  • 哨兵功能的描述:
    • 监控(Monitoring): 哨兵会不断地检查主节点和从节点是否运作正常。
    • 自动故障转移(Automatic failover): 当 主节点 不能正常工作时,哨兵会开始 自动故障转移操作,它会将失效主节点的其中一个 从节点升级为新的主节点,并让其他从节点改为复制新的主节点。使得哨兵可以及时发现主节点故障并完成转移。而配置提供者和通知功能,则需要在与客户端的交互中才能体现
    • 配置提供者(Configuration provider): 客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。
    • 通知(Notification): 哨兵可以将故障转移的结果发送给客户端。并且通知客户端与新 master 建立连接。
      redissentinel.png

23. Redis Sentinel(哨兵)实现原理

  • 组成命令sentinel monitor <master-name> <ip> <redis-port> <quorum>

  • 哨兵节点之间是通过 Redis 的发布者/订阅者机制来相互发现的。在主从集群中,主节点上有一个名为__sentinel__:hello的频道,不同哨兵就是通过它来相互发现,实现互相通信的

  • 哨兵模式是通过哨兵节点完成对数据节点的监控、下线(主客观下线)、领导者sentinel选举、故障转移。

  • 定时监控

    • 主从信息获取:每隔10秒,每个 Sentinel 节点会向主节点和从节点发送 info 命令获取最新的拓扑结构
    • 哨兵信息发布:每隔2秒,每个 Sentinel 节点会向 Redis 数据节点的sentinel:hello 频道上发送该 Sentinel 节点对于主节点的判断以及当前 Sentinel 节点的信息
    • 节点心跳检测:每隔 1 秒,每个 Sentinel 节点会向主节点、从节点、其余 Sentinel 节点发送一条 ping 命令做一次心跳检测,来确认这些节点当前是否可达
  • 主观下线和客观下线:

    • 主观下线:每个 Sentinel 节点会每隔 1 秒对主节点、从节点、其他 Sentinel 节点发送 ping 命令做心跳检测,当这些节点超过 down-after-milliseconds 没有进行有效回复,Sentinel 节点将该节点标记为主观下线。
    • 客观下线:当 Sentinel 标记主观下线的节点是主节点时,该 Sentinel 节点会通过 is- master-down-by-addr 命令向其他 Sentinel 节点询问对主节点的判断,当超过 <quorum>(一般为哨兵个数的二分之一加1)个数,Sentinel 节点认为主节点确实有问题,这时该 Sentinel 节点会标记主节点客观下线
  • 领导者 Sentinel 节点选举:Sentinel 节点之间会选出一个节点作为领导者进行故障转移的工作。使用Raft算法实现

  • 故障转移:领导者选举出的 Sentinel 节点负责故障转移,过程如下:

    • 选出主节点:在从节点列表中选出一个节点作为新的主节点,
    • 设置主节点:Sentinel 领导者节点会对第一步选出来的从节点执行 slaveof no one 命令让其成为主节点
    • 从节点同步:Sentinel 领导者节点会向剩余的从节点发送SLAVEOF ip port命令,让它们成为新主节点的从节点
    • 通知客户端:哨兵就会向 +switch-master 频道将新主节点的IP地址和信息,通过发布者/订阅者机制通知给客户端;
    • 原节点转从:Sentinel 节点集合继续监视原来的主节点,当其恢复后发送 SLAVEOF IP port命令它去复制新的主节点

24. 领导者 Sentinel 节点选举了解吗?

  • 使用了 Raft 算法实 现领导者选举,大致流程如下:

  • 每个在线的 Sentinel 节点都有资格成为领导者,当它确认主节点主观 下线时候,会向其他 Sentinel 节点发送 is-master-down-by-addr 命令, 要求将自己设置为领导者。

  • 收到命令的 Sentinel 节点,如果没有同意过其他 Sentinel 节点的 is-master-down-by-addr 命令,将同意该请求,否则拒绝。

  • 如果该 Sentinel 节点发现自己的票数已经大于等于 max(quorum, num(sentinels)/2+1),那么它将成为领导者

  • 如果此过程没有选举出领导者,将进入下一次选举

  • 为什么哨兵节点至少要有3个?如果哨兵集群中只有2个哨兵节点,此时如果一个哨兵想要成功成为Leader,必须获得2票,而不是1票。如果有个哨兵挂掉了,剩下的哨兵想要成为Leader,票数就没法达到2票,就无法成功成为Leader,无法进行主从节点切换的。因此,通常配置3个哨兵节点。如果3个哨兵节点挂了2个怎么办?人为介入,或者增加多一点哨兵节点

25. 新的主节点是怎样被挑选出来的

  • 过滤:“不健康”(主观下线、断线)、5 秒内没有回复过 Sentinel 节 点 ping 响应、与主节点失联超过 down-after-milliseconds*10秒
  • 选择 slave-priority(从节点优先级)最高的从节点列表,如果存在则返回,不存在则继续。
  • 选择复制偏移量最大的从节点(复制的最完整),哪个从「主节点」接收的复制数据多(如果某个从节点的 slave_repl_offset 最接近 master_repl_offset),如果存在则返 回,不存在则继续。
  • 选择节点id runid 最小的从节点。

26. Redis 集群了解吗?

  • 主从存在高可用和分布式的问题,哨兵解决了高可用的问题,而集群解决高可用和分布式问题。
  • 数据分区:将数据分散到多个节点,突破了 Redis 单机内存大小的限制,存储容量大大增加;每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。
  • 高可用:集群支持主从复制和主节点的自动故障转移(与哨兵类似),当任一节点发生故障时,集群仍然可以对外提供服务

27. 切片集群了解吗?

  • 切片集群是一种将数据分片存储在多个 Redis 实例上的集群架构,每个 Redis 实例负责存储部分数据。比如说把 25G 的数据平均分为 5 份,每份 5G,然后启动 5 个 Redis 实例,每个实例保存一份数据。
  • 数据和实例之间如何映射呢?Redis Cluster是针对切片集群提供的解决方案;在 Redis Cluster 中,数据和实例之间的映射是通过哈希槽(hash slot)来实现的。Redis Cluster 有 16384 个哈希槽,每个键根据其名字的 CRC16 值被映射到这些哈希槽上。然后,这些哈希槽会被均匀地分配到所有的 Redis 实例上。CRC16 是一种哈希算法,它可以将任意长度的输入数据映射为一个 16 位的哈希值。需要存储或检索一个键值对时,Redis Cluster 会先计算这个键的哈希槽,然后找到负责这个哈希槽的 Redis 实例,最后在这个实例上进行操作。即crc16(key)& 16384

28. 集群中数据如何分区?

在 Redis 集群中,数据分区是通过将数据分散到不同的节点来实现的,常见的数据分区规则有三种:节点取余分区、一致性哈希分区、虚拟槽分区。

  • 节点取余分区:数据项的键经过哈希函数计算后,对节点数量取余,然后将数据项分配到余数对应的节点上。缺点是扩缩容时,大多数数据需要重新分配,因为节点总数的改变会影响取余结果,这可能导致大量数据迁移。
  • 一致性哈希分区:将哈希值空间组织成一个环,数据项和节点都映射到这个环上。数据项由其哈希值直接映射到环上,然后顺时针分配到遇到的第一个节点。从而来减少节点变动时数据迁移的量。相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。节点在圆环上分布不平均,会造成部分缓存节点的压力较大;当某个节点故障时,这个节点所要承担的所有访问都会被顺移到另一个节点上,会对后面这个节点造成压力。
  • 虚拟槽(哈希槽)分区中,存在固定数量的槽位(例如 Redis Cluster 有 16384 个槽),每个键通过哈希算法(CRC16)映射到这些槽上,每个集群节点负责管理一定范围内的槽。可以灵活地将槽(以及槽中的数据)从一个节点迁移到另一个节点,从而实现平滑扩容和缩容;数据分布也更加均匀,Redis Cluster 采用的正是这种分区方式。
    • 在虚拟槽分区中,槽是数据管理和迁移的基本单位。假设系统中有 4 个实际节点,假设为其分配了 16 个槽(0-15);槽 0-3 位于节点 node1;槽 4-7 位于节点 node2;槽 8-11 位于节点 node3;槽 12-15 位于节点 node4。如果此时删除 node2,只需要将槽 4-7 重新分配即可,例如将槽 4-5 分配给 node1,槽 6 分配给 node3,槽 7 分配给 node4,数据在节点上的分布仍然较为均衡。如果此时增加 node5,也只需要将一部分槽分配给 node5 即可,比如说将槽 3、槽 7、槽 11、槽 15 迁移给 node5,节点上的其他槽位保留。当然了,这取决于 CRC16(key) % 槽的个数 的具体结果。因为在 Redis Cluster 中,槽的个数刚好是 2 的 14 次方,这和 HashMap 中数组的长度必须是 2 的幂次方有着异曲同工之妙。它能保证扩容后,大部分数据停留在扩容前的位置,只有少部分数据需要迁移到新的槽上。

29. Redis 集群的原理吗?

  • Redis 集群通过数据分区来实现数据的分布式存储,通过自动故障转移实现高可用。
  • 集群创建:数据分区是在集群创建的时候完成的。
    • 设置节点:Redis 集群一般由多个节点组成,节点数量至少为 6 个才能保证组成完整高可用的集群。每个节点需要开启配置 cluster-enabled yes,让 Redis 运行在集群模式下。
    • 节点握手:指一批运行在集群模式下的节点通过 Gossip 协议彼此通信, 达到感知对方的过程。节点握手是集群彼此通信的第一步,由客户端发起命令:cluster meet{ip}{port}。完成节点握手之后,一个个的 Redis 节点就组成了一个多节点的集群
    • 分配槽(slot):Redis 集群把所有的数据映射到 16384 个槽中。每个节点对应若干个槽,只有当节点分配了槽,才能响应和这些槽关联的键命令。通过 cluster addslots 命令为节点分配槽。
    • 故障转移:Redis 集群的故障转移和哨兵的故障转移类似,但是 Redis 集群中所有的节点都要承担状态维护的任务。
    • 故障发现:Redis 集群内节点通过 ping/pong 消息实现节点通信,集群中每个节点都会定期向其他节点发送 ping 消息,接收节点回复 pong 消息作为响应。如果在 cluster-node-timeout 时间内通信一直失败,则发送节 点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态。当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播。通过 Gossip 消息传播,集群内节点不断收集到故障节点的下线报告。当 半数以上持有槽的主节点都标记某个节点是主观下线时。触发客观下线流程。
    • 故障恢复:故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它 的从节点中选出一个替换它,从而保证集群的高可用。流程如下
      • 资格检查:每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障 的主节点。
      • 准备选举时间:当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该 时间后才能执行后续流程。
      • 发起选举:当从节点定时任务检测到达故障选举时间(failover_auth_time)到达后,发起选举流程。
      • 选举投票:持有槽的主节点处理故障选举消息。投票过程其实是一个领导者选举的过程,如集群内有 N 个持有槽的主节 点代表有 N 张选票。由于在每个配置纪元内持有槽的主节点只能投票给一个 从节点,因此只能有一个从节点获得 N/2+1 的选票,保证能够找出唯一的从节点。
      • 替换主节点:当从节点收集到足够的选票之后,触发替换主节点操作。
  • 部署 Redis 集群至少需要几个物理节点? 在投票选举的环节,故障主节点也算在投票数内,假设集群内节点规模是 3 主 3 从,其中有 2 个主节点部署在一台机器上,当这台机器宕机时,由于从节点无法收集到 3/2+1 个主节点选票将导致故障转移失败。这个问题也适用于故障发现环节。因此部署集群时所有主节点最少需要部署在 3 台物理机上才能避免单点问题。

30. 说说集群的伸缩?

  • Redis 集群提供了灵活的节点扩容和收缩方案,可以在不影响集群对外服务的情况下,为集群添加节点进行扩容也可以下线部分节点进行缩容。集群扩容和缩容的关键点,就在于槽和节点的对应关系,扩容和缩容就是将一部分槽和数据迁移给新节点。扩容实例缩容也是类似,先把槽和数据迁移到其它节点,再把对应的节点下线。

31. redis缓存穿透,缓存击穿,缓存雪崩及其解决方案√

  • 缓存穿透:指查询不存在的数据,由于缓存没有命中(因为数据根本就不存在),请求每次都会穿过缓存去查询数据库。如果这种查询非常频繁,就会给数据库造成很大的压力,可能由自身业务代码或者爬虫恶意攻击造成

  • 缓存击穿:指某一个或少数几个数据被高频访问,当这些数据在缓存中过期的那一刻,大量请求就会直接到达数据库,导致数据库瞬间压力过大。

  • 缓存雪崩:某一个时间点,由于大量的缓存数据同时过期或缓存服务器突然宕机了,导致所有的请求都落到了数据库上,对数据库造成巨大压力,甚至导致数据库崩溃的现象。

  • 缓存穿透:

    • 缓存空对象:不管是数据不存在,还是系统故障都缓存过期时间很短的空结果,最长不超过五分钟(防止浪费内存)(带来的问题:需要更多的内存空间,推荐设置较短的过期时间,让其自动剔除;缓存层和存储层的数据会有一段时间窗口的不一致,可能影响业务。可以利用消息系统或者其他异步方式清除掉缓存层中的空对象),适用于数据命中不高,数据频繁实时性高,代码维护简单,需要更多缓存空间,数据不一致
    • 布隆过滤器拦截:当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在在进入缓存层、存储层。可以使用bitmap当作布隆过滤器,适用于数据命中不高、数据相对固定、实时性低的场景,代码复杂,但缓存空间占用少
  • 缓存击穿:(热点数据,重建缓存)

    • 加锁更新,⽐如请求查询 A,发现缓存中没有,对 A 这个 key 加锁,同时去数据库查询数据,写⼊缓存,再返回给⽤户,这样后⾯的请求就可以从缓存中拿到数据了。如果在查询数据库和重建缓存(key失效后进行了大量的计算)时间过长,可能导致死锁和线程池阻塞,高并发情景下吞吐量会大大降低!但是这种方法能降低后端存储负载,数据一致性好
    • 手动过期:缓存上不设置过期时间并将过期时间存在KEY对应的VALUE里。获取缓存并通过VALUE的过期时间判断是否过期。如果未过期,则直接返回;如果已过期,则通过一个后台的异步线程进行缓存的构建,即“手动”过期。通过后台的异步线程,保证有且只有一个线程去查询DB。然后返回。保证服务的可用性,虽然损失了一定的时效性。
  • 缓存雪崩

    • 提高缓存可用性,
      • 集群部署:降低单点故障的风险。即使某个缓存节点发生故障,其他节点仍然可以提供服务,从而避免对数据库的大量直接访问。利用 Redis Cluster或者第三方集群方案 Codis。
      • 采用多级缓存,本地进程一级缓存,redis二级缓存,不同级别的缓存设置的超时时间不同
        • 本地缓存的实时性怎么保证?
          • 方案一,引入消息队列。在数据更新时,发布数据更新的消息;而进程中相应的消费者消费消息,更新本地缓存
          • 方案二,设置较短的过期时间,请求时从DB重新拉取
          • 方案三,使用 「如果避免缓存"击穿"的问题?」 问题的【方案二】,手动过期。
        • 每个进程可能会本地缓存相同的数据,导致内存浪费?
          • 方案一,配置本地缓存的过期策略和缓存数量上限。
          • 方案二:使用Ehcache、Guava Cache 实现本地缓存的功能。
    • 缓存的过期时间加上随机值,尽量让不同的key过期时间不同
    • 限流和降级:通过设置合理的系统限流策略,如令牌桶或漏斗算法,来控制访问流量,防止在缓存失效时数据库被打垮。此外,系统可以实现降级策略,在缓存雪崩或系统压力过大时,暂时关闭一些非核心服务,确保核心服务的正常运行

32. 能说说布隆过滤器吗?

  • 布隆过滤器由一个长度为 m 的位数组和 k 个哈希函数组成。当一个元素被添加到过滤器中时,它会被 k 个哈希函数分别计算得到 k 个位置,然后将位数组中对应的位设置为 1。当检查一个元素是否存在于过滤器中时,同样使用 k 个哈希函数计算位置,如果任一位置的位为 0,则该元素肯定不在过滤器中;如果所有位置的位都为 1,则该元素可能在过滤器中。
  • 因为布隆过滤器占用的内存空间非常小,所以查询效率也非常高,所以在 Redis 缓存中,使用布隆过滤器可以快速判断请求的数据是否在缓存中。因为哈希算法有一定的碰撞的概率。故存在误判。而且不支持删除元素

33. 如何保证缓存和数据库数据的一致性?

  • 更新缓存还是删除缓存?删除缓存速度快,因为更新缓存的值可能来自不同的表,需要经过复杂计算出来,代价大
  • 产生原因
    • 并发的场景下,导致读取老的DB数据,更新到缓存中。
    • 缓存和DB的操作,不在一个事务中,可能只有一个DB操作成功,而另一个Cache操作失败,导致数据不一致。
  • 缓存和DB的一致性,指的是最终一致性。使用缓存只要是提高读操作的性能,真正在写操作的业务逻辑,还是以数据库为准
  • 都是2步中间被其他线程连续完成2步
  • 先删除缓存,后更新数据库
    • 请求A先删除缓存1,请求B读缓存发现没有,读取到旧值1,请求B存入缓存1,请求A写数据库2,数据不一致。
    • 解决方案:串行写。在写请求时,先淘汰缓存之前,先获取该分布式锁。在读请求时,发现缓存不存在时,先获取分布式锁
  • 先更新数据库,后删除缓存
    • 请求A读缓存发现不存在,读旧值1;请求B刷新数据库2;并删除缓存,请求A将旧值x=1写入缓存,数据不一致
    • 缓存删除失败,并发导致写入了脏数据
    • 问题1:概率低,必须满足3个条件
      • 缓存刚好已失效
      • 读请求 + 写请求并发
      • 请求B更新数据库+删除缓存的时间,要比请求A读数据库+写缓存时间短(概率低,因为写数据库一般会加锁,通常是要比读数据库的时间更长)
    • 问题2:
      • 引入消息队列保证缓存被删除。当数据库更新完成后,将更新事件发送到消息队列。有专门的服务监听这些事件并负责更新或删除缓存。缺点是对业务代码有一定的侵入
      • 数据库订阅+消息队列保证缓存被删除。专门起一个服务(比如 Canal,阿里巴巴 MySQL binlog 增量订阅&消费组件)去监听 MySQL 的 binlog,获取需要操作的数据。然后用一个公共的服务获取订阅程序传来的信息,进行缓存删除。降低了对业务的侵入,但增加了整个系统的复杂度,适合基建完善的大厂。
      • 延时双删防止脏数据。在第一次删除缓存之后,过一段时间之后,再次删除缓存。主要针对缓存不存在,但写入了脏数据的情况。在先删缓存,再写数据库的更新策略下发生的比较多。延时时间需要仔细考量和测试。
      • 设置缓存过期时间兜底:兜底策略,给缓存设置一个合理的过期时间,即使发生了缓存和数据库的数据不一致问题,也不会永远不一致下去,缓存过期后,自然就一致了。

34. 如何保证本地缓存和分布式缓存的一致

  • 本地缓存对应服务器的内存缓存比如Caffeine,分布式缓存基本就是采用 Redis
  • 设置本地缓存的过期时间,当本地缓存过期时,就从 Redis 缓存中去同步。
  • 使用Redis的 Pub/Sub 机制,当 Redis 缓存发生变化时,发布一个消息,本地缓存订阅这个消息,然后删除对应的本地缓存
  • Redis 缓存发生变化时,引入消息队列,比如 RocketMQ、RabbitMQ 去更新本地缓存。

35. 怎么处理热 key?

  • 热 key指在很短时间内被频繁访问的键。 Redis 是集群部署,热 key 可能会造成整体流量的不均衡(网络带宽、CPU 和内存资源),个别节点出现 OPS 过大的情况,极端情况下热点 key 甚至会超过 Redis 本身能够承受的 OPS。表示 Redis 每秒钟能够处理的命令数。
  • 通常以 Key 被请求的频率来判定
    • QPS 集中在特定的 Key:总的 QPS(每秒查询率)为 10000,其中一个 Key 的 QPS 飙到了 8000。
    • 带宽使用率集中在特定的 Key:一个拥有上千成员且总大小为 1M 的哈希 Key,每秒发送大量的 HGETALL 请求
    • CPU 使用率集中在特定的 Key:一个拥有数万个成员的 ZSET Key,每秒发送大量的 ZRANGE 请求。
  • 对热 key 的处理
    • 监控热key
      • 客户端:在客户端设置全局字典(key 和调用次数),每次调用 Redis 命令时,使用这个字典进行记录。
      • 代理端:像 Twemproxy、Codis 这些基于代理的 Redis 分布式架构,所有客户端的请求都是通过代理端完成的,可以在代理端进行监控。
      • Redis 服务端:使用 monitor 命令统计热点 key:redis-cli monitor;分析热 Key。redis-cli --bigkeys
    • 处理热key
      • 热 key 打散到不同的服务器,降低压⼒。
// N 为 Redis 实例个数,M 为 N 的 2倍
const M = N * 2
//生成随机数
random = GenRandom(0, M)
//构造备份新 Key
bakHotKey = hotKey + "_" + random
data = redis.GET(bakHotKey)
if data == NULL {
    data = redis.GET(hotKey)
    if data == NULL {
        data = GetFromDB()
        // 可以利用原子锁来写入数据保证数据一致性
        redis.SET(hotKey, data, expireTime)
        redis.SET(bakHotKey, data, expireTime + GenRandom(0, 5))
    } else {
        redis.SET(bakHotKey, data, expireTime + GenRandom(0, 5))
    }
}

  • 加⼊⼆级缓存,当出现热 Key 后,把热 Key 加载到 JVM 中,后续针对这些热 Key 的请求,直接从 JVM 中读取。比如 Caffeine、Guava 等,或者直接使用 HashMap 作为本地缓存都是可以的。需要防止本地缓存过大。

36. 缓存预热怎么做呢?

  • 缓存预热是提前将相关的缓存数据加载到缓存系统,避免在用户请求的时候,先查询数据库,然后再将数据缓存造成数据库压力过大的问题,用户直接查询事先被预热的缓存数据。
  • 缓存预热方案
    • 数据量不大的时候,项目启动时初始化。
    • 数据量大的时候,定时任务刷新缓存
    • 直接写个界面、接口、脚本,上线时手动操作
  • 缓存数据的淘汰策略有哪些?
    • 缓存服务器自带的缓存自动失效策略
    • 定时去清理过期的缓存。维护大量缓存的 key 是比较麻烦
    • 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!

37. 热点 key 重建?问题?解决?

  • 使用“缓存+过期时间”的策略,既可以加速数据读写,又保证数据的定期更新,基本能够满足绝大部分需求。
  • 但是当 key 是一个热点 key,并发量大。且重建缓存时间长,可能是一个复杂计算,例如复杂的 SQL、多次 IO、多个依赖等。在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。
  • 解决方案
    • 互斥锁(mutex key):只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
    • 永远不过期:为每个 value 设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。

38. 无底洞问题吗?如何解决?

  • 添加 Memcache 节点,但是发现性能不但没有好转反而下降了,这种现象称为缓存的“无底洞”现象。
  • 为什么?键值数据库由于通常采用哈希函数将 key 映射到各个节点上,造成 key 的分布与业务无关,但是由于数据量和访问量的持续增长,造成需要添加大量节点做水平扩容,导致键值分布到更多的 节点上,所以无论是 Memcache 还是 Redis 的分布式,批量操作通常需要从不同节点上获取,相比于单机批量操作只涉及一次网络操作,分布式批量操作会涉及多次网络时间。
  • 无底洞问题如何优化呢?
    • 客户端一次批量操作会涉及多次网络操作,也就意味着批量操作会随着节点的增多,耗时会不断增大。
    • 网络连接数变多,对节点的性能也有一定影响。
  • 优化思路
    • 命令本身的优化,例如优化操作语句等。
    • 减少网络通信次数。
    • 降低接入成本,例如客户端使用长连/连接池、NIO 等。

39. Redis 报内存不足怎么处理?

  • redis.conf配置maxmemory 100mb
  • config set maxmemory 命令动态设置内存上限
  • 修改内存淘汰策略,及时释放内存空间
  • 使用 Redis 集群模式,进行横向扩容。

40. Redis的过期键的删除策略

  • 惰性过期:只有当访问key已过期时才清除。可以节省CPU资源,内存占用大。
  • 定期清除:每隔一定的时间扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。通过调整定时扫描的时间间隔和每次扫描的限定耗时,使得CPU和内存资源达到最优的平衡效果。(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)
  • config get hz每秒执行其内部定时任务(如过期键的清理)的频率几次。CONFIG SET hz 20 进行调整,或者直接通过配置文件中的 hz 设置。

41. Redis内存淘汰策略有哪些?

  • Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

  • no-eviction(默认):当内存不足以容纳新写入数据时,新写入操作会报错。

  • allkeys-lru:从数据集(server. db[i]. dict),移除最近最少使用(访问时间小)的key。

  • allkeys-random:从数据集(server. db[i]. dict),随机移除某个key。

  • allkeys-lfu 从数据集(server. db[i]. dict),移除最近访问的频率/次数最少的key

  • volatile-lru:从设置了过期时间的键空间(server. db[i]. expires)中,移除最近最少使用的key。

  • volatile-random:从设置了过期时间的键空间(server. db[i]. expires)中,随机移除某个key。

  • volatile-lfu 从设置了过期时间的键空间(server. db[i]. expires)中,移除最近访问的频率最少的key

  • volatile-ttl:从设置了过期时间的键空间(server. db[i]. expires)中,移除将要过期的key。

  • Redis使用近似的LRU算法,通过随机采样法淘汰数据,每次随机出maxmenory-samples(默认5)个key,从里面淘汰掉最近最少使用的key。maxmenory-samples越大,淘汰结果越接近于严格的LRU算法,每个key额外增加了一个24bit的字段,用来存储该key最后一次被访问的时间。Redis不实现严格的LRU算是的原因是,因为消耗更多的内存。

  • Redis3.0新算法会维护一个候选池(大小为16),池中的数据根据访问时间进行排序,第一次随机选取的key都会放入池中,随后每次随机选取的key只有在访问时间小于池中最小的时间才会放入池中,直到候选池被放满。当放满后,如果有新的key需要放入,则将池中最后访问时间最大(最近被访问)的移除。当需要淘汰的时候,则直接从池中选取最近访问时间最小(最久没被访问)的key淘汰掉就行。

  • 获取当前内存淘汰策略:config get maxmemory-policy

  • 修改淘汰策略:config set maxmemory-policy allkeys-lru

  • redis.conf文件:maxmemory-policy allkeys-lru

42. Redis 阻塞?怎么解决?

  • API 或数据结构使用不合理:对于高并发的场景,应该尽量避免在大对象上执行算法复杂 度超过 O(n)的命令。对慢查询的处理分为两步:
    • 发现慢查询: slowlog get{n}命令可以获取最近 的 n 条慢查询命令;
    • 优化慢查询:
      • 修改为低算法复杂度的命令,如 hgetall 改为 hmget 等,禁用 keys、sort 等命 令
      • 调整大对象:缩减大对象数据或把大对象拆分为多个小对象,防止一次命令操作过多的数据。
  • CPU饱和的问题 Redis 单核 CPU 使用率跑到接近 100%。
    • 判断当前 Redis 并发量是否已经达到极限,可以使用统计命令 redis-cli-h{ip}-p{port}--stat 获取当前 Redis 使用情况如果 Redis 的请求几万+,那么大概就是 Redis 的 OPS 已经到了极限,应该做集群化水品扩展来分摊 OPS 压力如果只有几百几千,那么就得排查命令和内存的使用
  • 持久化相关的阻塞
    • fork 阻塞:fork 操作发生在 RDB 和 AOF 重写时,Redis 主线程调用 fork 操作产生共享 内存的子进程,由子进程完成持久化文件重写工作。如果 fork 操作本身耗时过长,必然会导致主线程的阻塞。
    • AOF 刷盘阻塞:当开启 AOF 持久化功能时,文件刷盘的方式一般采用每秒一次,后台线程每秒对 AOF 文件做 fsync 操作。当硬盘压力过大时,fsync 操作需要等 待,直到写入完成。如果主线程发现距离上一次的 fsync 成功超过 2 秒,为了 数据安全性它会阻塞直到后台线程执行 fsync 操作完成。
    • HugePage 写操作阻塞:对于开启 Transparent HugePages 的 操作系统,每次写命令引起的复制内存页单位由 4K 变为 2MB,放大了 512 倍,会拖慢写操作的执行时间,导致大量写操作慢查询。

43. 大 key 问题了解吗?

  • 大 key 指的是存储了大量数据的键,比如:单个简单的 key 存储的 value 很大,size 超过 10KB。hash,set,zset,list 中存储过多的元素(以万为单位)
  • 大 key 会造成的问题呢?
    • 客户端超时
    • 对大 key 进行 IO 操作时,会严重占用带宽和 CPU
    • 造成 Redis 集群中数据倾斜
    • 主动删除、被动删等,可能会导致阻塞
  • 如何找到大 key?
    • bigkeys 参数:使用 bigkeys 命令以遍历的方式分析 Redis 实例中的所有 Key,并返回整体统计信息与每个数据类型中 Top1 的大 Key。redis-cli --bigkeys
    • redis-rdb-tools:redis-rdb-tools 是由 Python 语言编写的用来分析 Redis 中 rdb 快照文件的工具。
  • 如何处理大 key?
    • 删除大 key
      • 当Redis版本大于4.0 时,可使用 UNLINK 命令安全地删除大 Key,该命令能够以非阻塞的方式,逐步地清理传入的大 Key。
      • 当Redis版本小于 4.0 时,建议通过 SCAN 命令执行增量迭代扫描 key,然后判断进行删除。
    • 压缩和拆分 key
      • 当vaule是string时,使用序列化、压缩算法将key的大小控制在合理范围内,但是序列化和反序列化都会带来额外的性能消耗
      • 当 value 是 string,压缩之后仍然是大 key 时,则需要进行拆分,将一个大 key 分为不同的部分,记录每个部分的 key,使用 multiget 等操作实现事务读取。
      • 当 value 是 list/set 等集合类型时,根据预估的数据规模来进行分片,不同的元素计算后分到不同的片。

44. Redis 常见性能问题和解决方案?

  • Master 最好不要做任何持久化工作,包括内存快照和 AOF 日志文件,特别是不要启用内存快照做持久化。因为Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,而Master AOF持久化,如果不重写AOF文件,AOF 文件过大会影响 Master 重启的恢复速度。
  • 如果数据比较关键,某个 Slave 开启 AOF 备份数据,策略为每秒同步一次。
  • 为了主从复制的速度和连接的稳定性,Slave 和 Master 最好在同一个局域网内。
  • 尽量避免在压力较大的主库上增加从库。可以考虑在从上挂载其它的从。
  • Master 调用 BGREWRITEAOF 重写 AOF 文件会占大量的 CPU 和内存资源,导致服务 load 过高,出现短暂服务暂停现象。
  • 为了 Master 的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现 Slave 对 Master 的替换,也即,如果 Master 挂了,可以立马启用 Slave1 做 Master,其他不变。从节点在切换主节点作为复制源的时候,会重新发起全量复制。所以此处通过 Slave1 挂在 Slave 下,可以规避这个问题。同时,也减少了 Master 的复制压力。坏处是 Slave1 的延迟可能会高一些些,所以需要取舍。

45. 使用 Redis 如何实现异步队列?

  • 使用 list 作为队列,lpush 生产消息,rpop 消费消息, 消费者死循环 rpop 从队列中消费消息。但是这样,即使队列里没有消息,也会进行 rpop,会导致 Redis CPU 的消耗。可以通过让消费者休眠的方式的方式来处理,但是这样又会又消息的延迟问题。
  • 使用 list 作为队列,lpush 生产消息,brpop 消费消息 brpop 是 rpop 的阻塞版本,list 为空的时候,它会一直阻塞,直到 list 中有值或者超时。只能实现一对一的消息队列。
  • 使用 Redis 的 pub/sub 来进行消息的发布/订阅:可以 1:N 的消息发布/订阅。发布者将消息发布到指定的频道频道(channel),订阅相应频道的客户端都能收到消息。但是这种方式不是可靠的,在消费者下线的情况下,生产的消息会丢失,
  • 所以,一般的异步队列的实现还是交给专业的消息队列。

46. Redis 如何实现延时队列?

使用 Redis 的 zset(有序集合)来实现延时队列。

  • 第一步,将任务添加到 zset 中,score 为任务的执行时间戳,value 为任务的内容。ZADD delay_queue 1617024000 task1
  • 第二步,定期(例如每秒)从 zset 中获取 score 小于当前时间戳的任务,然后执行任务。ZREMRANGEBYSCORE delay_queue -inf 1617024000
  • 第三步,从 zset 中删除任务。ZREM delay_queue task1
  • Redis 真的真的真的不推荐作为消息队列使用,它最多只是消息队列的存储层,上层的逻辑,还需要做大量的封装和支持。
  • 在 Redis 5.0 增加了 Stream 功能,一个新的强大的支持多播的可持久化的消息队列,提供类似 Kafka 的功能。

47. Redis 支持事务吗?

  • Redis支持事务,可将多个命令打包,然后一次性按照顺序执行。主要通过 multi、exec、discard、watch 等命令来实现:
    • multi:标记一个事务块的开始
    • exec:执行所有事务块内的命令
    • discard:取消事务,放弃执行事务块内的所有命令
    • watch:监视一个或多个 key,如果在事务执行之前这个 key 被其他命令所改动,那么事务将被打断
  • Redis 事务的原理:
    • 使用 MULTI 命令开始一个事务。从这个命令执行之后开始,所有的后续命令都不会立即执行,而是被放入一个队列中。在这个阶段,Redis 只是记录下了这些命令。
    • 在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。使用 EXEC 命令触发事务的执行。一旦执行了 EXEC,之前 MULTI 后队列中的所有命令会被原子地(atomic)执行。意味着这些命令要么全部执行,要么(在出现错误时)全部不执行。Lua 脚本也能实现原子操作。
    • 如果在执行 EXEC 之前决定不执行事务,可以使用 DISCARD 命令来取消事务。这会清空事务队列并退出事务状态。
    • WATCH 命令用于实现乐观锁。WATCH 命令可以监视一个或多个键,如果在执行事务的过程中(即在执行 MULTI 之后,执行 EXEC 之前),被监视的键被其他命令改变了,那么当执行 EXEC 时,事务将被取消,并且返回一个错误。
    • Redis事务中如果有某一条命令执行失败,其后的命令仍然会被继续执行。Lua脚本不具备。
      redistransaction.png

48. Redis 事务的注意点有哪些?

  • Redis 事务是不支持回滚的,一旦 EXEC 命令被调用,所有命令都会被执行,即使有些命令可能执行失败。失败的命令不会影响到其他命令的执行。

49. Redis 事务为什么不支持回滚?

  • 引入事务回滚机制会大大增加 Redis 的复杂性,因为需要跟踪事务中每个命令的状态,并在发生错误时逆向执行命令以恢复原始状态。
  • Redis 是一个基于内存的数据存储系统,其设计重点是实现高性能。事务回滚需要额外的资源和时间来管理和执行,这与 Redis 的设计目标相违背。因此,Redis 选择不支持事务回滚。

50. Redis 和 Lua 脚本的使用了解吗?

  • Lua 脚本在 Redis 中是原子执行的,执行过程中间不会插入其他命令。
  • Lua 脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这 些命令常驻在 Redis 内存中,实现复用的效果。
  • Lua 脚本可以将多条命令一次性打包,有效地减少网络开销。
  • 比如这一段很(烂)经(大)典(街)的秒杀系统利用 lua 扣减 Redis 库存的脚本:
  -- 库存未预热
  if (redis.call('exists', KEYS[2]) == 1) then
      return -9;
  end;
  -- 秒杀商品库存存在
  if (redis.call('exists', KEYS[1]) == 1) then
      local stock = tonumber(redis.call('get', KEYS[1]));
      local num = tonumber(ARGV[1]);
      -- 剩余库存少于请求数量
      if (stock < num) then
          return -3
      end;
      -- 扣减库存
      if (stock >= num) then
          redis.call('incrby', KEYS[1], 0 - num);
          -- 扣减成功
          return 1
      end;
      return -2;
  end;
  -- 秒杀商品库存不存在
  return -1;

51. Redis 的管道Pipelining了解吗?

  • Redis提供三种将客户端多条命令打包发送给服务端执行的方式:Pipelining(管道) 、 Transactions(事务) 和 Lua Scripts(Lua 脚本) 。
  • Pipelining(管道):当客户端需要执行多条 redis 命令时,可以通过管道一次性将要执行的多条命令发送给服务端,使用 nc 命令将两条指令发送给 redis 服务端。Redis 服务端接收到管道发送过来的多条命令后,会一直执命令,并将命令的执行结果进行缓存,直到最后一条命令执行完成,再所有命令的执行结果一次性返回给客户端 。
  • Pipelining 的优势
    • 节省了 RTT:将多条命令打包一次性发送给服务端,减少了客户端与服务端之间的网络调用次数
    • 减少了上下文切换:当客户端/服务端需要从网络中读写数据时,都会产生一次系统调用,系统调用是非常耗时的操作,其中设计到程序由用户态切换到内核态,再从内核态切换回用户态的过程。只会产生一次上下文切换。

52. Redis 实现分布式锁了解吗?

  • 分布式锁考虑要点

    • 1.正确的获得锁(保证有且只有一个进程获得到) set 指令附带 nx 参数
    • 2.正确的释放锁:使用 Lua 脚本,比对锁持有的是不是自己。如果是,则进行del指令删除来释放。
    • 3.超时的自动释放锁set 指令附带 expire参数,通过过期机制来实现超时释放。
    • 4.未获得到锁的等待机制:sleep或者基于Redis订阅 Pub/Sub 机制。一些业务场景,可能需要支持获得不到锁,直接返回false ,不等待
    • 5.重入性(可选):通过ThreadLocal<Integer>记录是第几次获得相同的锁。有且第一次计数为1&&获得锁时,才向 Redis 发起获得锁的操作;有且计数为 0 && 释放锁时,才向 Redis 发起释放锁的操作。
    • 6、锁超时的处理:可以考虑告警 + 后台线程自动续锁的超时时间。通过这样的机制,保证有且仅有一个线程,正在持有锁。
    • 7、Redis 分布式锁丢失问题 看方案2 Redlock
  • 方案1。使用set指令SET key_name my_random_value NX PX 30000

    • NX表示if not exist 就设置并返回True,否则不设置并返回False
    • PX表示过期时间用毫秒级, 30000表示这些毫秒时间后此key过期
    • del释放锁
    • 缺点是加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,在Redis的master节点上拿到了锁;但是这个加锁的key还没有同步到slave节点;master故障,发生故障转移,slave节点升级为master节点;导致锁丢失。
  • 方案2:使用Redlock,多Redis节点的场景下,会存在分布式锁丢失的问题。

    • 客户端业务逻辑
    • 获取当前Unix时间,以毫秒为单位。
    • 依次尝试从5个实例,使用相同的key和具有唯一性的value(例如UUID)获取锁。当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试去另外一个Redis实例请求获取锁。
    • 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功
    • 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
    • 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。

53. 说说 Redis 底层数据结构?

  • Redis 的底层数据结构有动态字符串(sds)、链表(list)、字典(ht)、跳跃表(skiplist)、整数集合(intset)、压缩列表(ziplist) 等。
    redisobject.png
    redistype.png
  • list 是通过链表实现的,hash 是通过字典实现的,set 是通过字典实现的,zset 是通过跳跃表实现的。

54. 简单介绍下 SDS

  • string 是通过 SDS(simple dynamic String) 实现的.Redis 是通过 C 语言实现的,但 Redis 实现了一种叫做动态字符串 SDS 的类型。SDS 保存了⻓度信息将获取字符串⻓度的时间由 O(N) 降低到了 O(1)。
  • 针对缓存频繁修改的情况:SDS分配内存不仅会分配需要的空间,还会分配额外的空间。小于1MB的SDS每次分配与len属性同样大小的空间大于1MB的每次分配1MB
  • 两种编码方式:object encoding [key]查看
    • embstr编码:保存长度<=39字符串值
    • raw编码: 对embstr字符串执行任何修改命令时,程序会转换编码为raw。
    • 中文默认占三个字符。
  • 优先使用embstr编码的原因:embstr只会分配一块连续内存,读取快,只用释放1块内存;而raw分配2块不连续内存,读取较慢,需要释放2块内存
struct sdshdr {
    int len; // buf 中已使用的长度 记录SDS所保存的字符串长度,保证了O(1) 时间复杂度查询字符串长度信息。
    int free; // buf 中未使用的长度 惰性释放策略:不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性,记录buf数组中未使用字节的数量。空闲出来的空间,可以让str在进行append的时候重新使用
    char buf[]; // 字节数组,用于保存字符串
};

55. 简单介绍下链表 linkedlist

  • Redis 的链表是⼀个双向⽆环链表结构,链表的节点由⼀个叫做 listNode 的结构来表示,每个节点都有指向其前置节点和后置节点的指针,同时头节点的前置和尾节点的后置均指向 null。
    redislinkedlist.png

56. 简单介绍下字典 dict

  • ⽤于保存键值对的抽象数据结构。Redis 使⽤ hash 表作为底层实现,一个哈希表里可以有多个哈希表节点,而每个哈希表节点就保存了字典里中的一个键值对。
  • 每个字典带有两个 hash 表,供平时使⽤和 rehash 时使⽤,hash 表使⽤链地址法来解决键冲突,被分配到同⼀个索引位置的多个键值对会形成⼀个单向链表,在对 hash 表进⾏扩容或者缩容的时候,为了服务的可⽤性,rehash 的过程不是⼀次性完成的,⽽是渐进式的。
    redisdict.png

57. 简单介绍下跳跃表 skiplist

  • 跳跃表(也称跳表)是有序集合 Zset 的底层实现之⼀。在 Redis 7.0 之前,如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 的底层实现,否则会使用跳表;在 Redis 7.0 之后,压缩列表已经废弃,交由 listpack 来替代。
  • 跳表由 zskiplist 和 zskiplistNode 组成,zskiplist ⽤于保存跳表的基本信息(表头、表尾、⻓度、层高等)。
typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;

zskiplistNode ⽤于表示跳表节点,每个跳表节点的层⾼是不固定的,每个节点都有⼀个指向保存了当前节点的分值和成员对象的指针

typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
} zskiplistNode;
redisskiplist.png
redisskiplist.png

58. 简单介绍下整数集合 intset

  • ⽤于保存整数值的集合抽象数据结构,不会出现重复元素,底层实现为数组。
    redisintset.png

59. 简单介绍下压缩列表 ziplist

  • 压缩列表是为节约内存⽽开发的顺序性数据结构,它可以包含任意多个节点,每个节点可以保存⼀个字节数组或者整数值。
    redisintset.png

60. 简单介绍下紧凑列表 listpack

  • listpack 是 Redis 用来替代压缩列表(ziplist)的一种内存更加紧凑的数据结构。
    redislistpack1.png
    redislistpack2.png
  • 为了避免 ziplist 引起的连锁更新问题,listpack 中的元素不再像 ziplist 那样,保存其前一个元素的长度,而是保存当前元素的编码类型、数据,以及编码类型和数据的长度。
  • listpack 每个元素项不再保存上一个元素的长度,而是优化元素内字段的顺序,来保证既可以从前也可以向后遍历。但因为 List/Hash/Set/ZSet 都严重依赖 ziplist,所以这个替换之路很漫长。

61. Redis 的 SDS 和 C 中字符串相比有什么优势?

  • C 语言使用了一个长度为 N+1 的字符数组来表示长度为 N 的字符串,并且字符数组最后一个元素总是 \0,这种简单的字符串表示方式 不符合 Redis 对字符串在安全性、效率以及功能方面的要求。
  • C 语言的字符串可能有什么问题?
    • 获取字符串长度复杂度高 :因为 C 不保存数组的长度,每次都需要遍历一遍整个数组,时间复杂度为 O(n);
    • 不能杜绝 缓冲区溢出/内存泄漏 的问题 : C 字符串不记录自身长度带来的另外一个问题是容易造成缓存区溢出(buffer overflow),例如在字符串拼接的时候,新的 C 字符串 只能保存文本数据 → 因为 C 语言中的字符串必须符合某种编码(比如 ASCII),例如中间出现的 '\0' 可能会被判定为提前结束的字符串而识别不了;
  • Redis 如何解决?优势?
    • 多增加 len 表示当前字符串的长度:这样就可以直接获取长度了,复杂度 O(1);
    • 自动扩展空间:当 SDS 需要对字符串进行修改时,首先借助于 len 和 alloc 检查空间是否满足修改所需的要求,如果空间不够的话,SDS 会自动扩展空间,避免了像 C 字符串操作中的溢出情况;
    • 有效降低内存分配次数:C 字符串在涉及增加或者清除操作时会改变底层数组的大小造成重新分配,SDS 使用了 空间预分配 和 惰性空间释放 机制,简单理解就是每次在扩展时是成倍的多分配的,在缩容是也是先留着并不正式归还给 OS;
    • 二进制安全:C 语言字符串只能保存 ascii 码,对于图片、音频等信息无法保存,SDS 是二进制安全的,写入什么读取就是什么,不做任何过滤和限制;

62. 字典是如何实现的?Rehash 了解吗?

  • 字典是 Redis中的复合型数据结构。除了 hash 结构的数据会用到字典外,整个 Redis 数据库的所有 key 和 value 也组成了一个 全局字典,还有带过期时间的 key 也是一个字典。(存储在 RedisDb 数据结构中)
  • 字典结构是什么样的呢?
    • Redis 中的字典相当于 HashMap,内部实现也差不多类似,采用哈希与运算计算下标位置;通过 "数组 + 链表" 的链地址法 来解决哈希冲突,同时这样的结构也吸收了两种不同数据结构的优点。
      redisdictstructure.png
  • 字典是怎么扩容的?字典结构内部包含 两个 hashtable,通常情况下只有一个哈希表 ht[0] 有值,在扩容的时候,把 ht[0]里的值 rehash 到 ht[1],然后进行 渐进式 rehash ——指的是这个 rehash 的动作并不是一次性、集中式地完成的,而是分多次、渐进式地完成的。待搬迁结束后,h[1]就取代 h[0]存储字典的元素。

63. 跳表是如何实现的?原理?

  • 跳表(skiplist)是一种有序的数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。
    redisskipliststructure.png

  • 为什么使用跳表?

    • 首先,因为 zset 要支持随机的插入和删除,所以不宜使用数组来实现,关于排序问题,为什么 Redis 不使用 红黑树/ 平衡树 这样的树形结构呢?
      • 性能考虑: 在高并发的情况下,树形结构需要执行一些类似于 rebalance 这样的可能涉及整棵树的操作,相对来说跳跃表的变化只涉及局部;
      • 实现考虑: 在复杂度与红黑树相同的情况下,跳跃表实现起来更简单,看起来也更加直观;
  • 基于以上的一些考虑,Redis 基于 William Pugh 的论文做出一些改进后采用了 跳跃表 这样的结构。本质是解决查找问题。

  • 跳跃表是怎么实现的?

    • 层:跳跃表节点的 level 数组可以包含多个元素,每个元素都包含一个指向其它节点的指针,程序可以通过这些层来加快访问其它节点的速度,一般来说,层的数量月多,访问其它节点的速度就越快。每次创建一个新的跳跃表节点的时候,程序都根据幂次定律,随机生成一个介于 1 和 32 之间的值作为 level 数组的大小,这个大小就是层的“高度”
    • 前进指针:每个层都有一个指向表尾的前进指针(level[i].forward 属性),用于从表头向表尾方向访问节点。跳跃表从表头到表尾,遍历所有节点的路径:
      redisskiplist2.png
    • 跨度:层的跨度用于记录两个节点之间的距离。跨度是用来计算排位(rank)的:在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在跳跃表中的排位。例如查找,分值为 3.0、成员对象为 o3 的节点时,沿途经历的层:查找的过程只经过了一个层,并且层的跨度为 3,所以目标节点在跳跃表中的排位为 3。
      redisskiplist3.png
    • 分值和成员
      • 节点的分值(score 属性)是一个 double 类型的浮点数,跳跃表中所有的节点都按分值从小到大来排序。
      • 节点的成员对象(obj 属性)是一个指针,它指向一个字符串对象,而字符串对象则保存这一个 SDS 值。
  • 为什么 hash 表范围查询效率比跳表低?

    • 哈希表是一种基于键值对的数据结构,主要用于快速查找、插入和删除操作。哈希表通过计算键的哈希值来确定值的存储位置,这使得它在单个元素的访问上非常高效,时间复杂度为 O(1)。然而,哈希表内的元素是无序的。因此,对于范围查询(如查找所有在某个范围内的元素),哈希表无法直接支持,必须遍历整个表来检查哪些元素满足条件,这使得其在范围查询上的效率低下,时间复杂度为 O(n)。
    • 跳表是一种有序的数据结构,能够保持元素的排序顺序。它通过多层的链表结构实现快速的插入、删除和查找操作,其中每一层都是下一层的一个子集,并且元素在每一层都是有序的。当进行范围查询时,跳表可以从最高层开始,快速定位到范围的起始点,然后沿着下一层继续直到找到范围的结束点。这种分层的结构使得跳表在进行范围查询时非常高效,时间复杂度为 O(log n) 加上范围内元素的数量。

64. 压缩列表了解吗?

  • 压缩列表是 Redis 为了节约内存 而使用的一种数据结构,是由一系列特殊编码的连续内存快组成的顺序型数据结构。
  • 一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
  • 压缩列表由这么几部分组成:
    • zlbyttes:记录整个压缩列表占用的内存字节数
    • zltail:记录压缩列表表尾节点距离压缩列表的起始地址有多少字节
    • zllen:记录压缩列表包含的节点数量
    • entryX:列表节点
    • zlend:用于标记压缩列表的末端

65. 快速列表 quicklist 了解吗?

  • Redis 早期版本存储 list 列表数据结构使用的是压缩列表 ziplist 和普通的双向链表 linkedlist,也就是说当元素少时使用 ziplist,当元素多时用 linkedlist。但考虑到链表的附加空间相对较高,prev 和 next 指针就要占去 16 个字节(64 位操作系统占用 8 个字节),另外每个节点的内存都是单独分配,会家具内存的碎片化,影响内存管理效率。
  • 后来 Redis 新版本(3.2)对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist,quicklist 是综合考虑了时间效率与空间效率引入的新型数据结构。
  • quicklist 由 list 和 ziplist 结合而成,它是一个由 ziplist 充当节点的双向链表。
    redisquicklist.png

66. 假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如何将它们全部找出来?

  • keys指令可以扫出指定模式的key列表(keys xxx*)但会导致线程阻塞,线上服务会停顿,直到指令执行完毕,服务才恢复。
  • 使用scan指令无阻塞的提取出指定模式的key列表,但可能重复,在客户端做去重就可以了,但是整体所花费的时间会比直接用 keys指令长。(scan 0 match "api_"...->scan 100 match "api_")一直到cursor为0

==========================================================================================================

67. AOF实现原理?混合模式下什么时候aof,什么时候rdb√?

  • AOF以协议文本的方式,将所有对数据库进行过写入的命令(及其参数)记录到AOF文件

  • aof过程:将写操作命令保存到AOF文件的过程称为同步,则包括下面三个步骤

    • 命令传播:服务端接收到客服端发送的协议文本,转换成执行命令数据,Redis将执行完的命令、命令的参数、命令的参数个数等信息发送到AOF程序中。
    • 缓存追加:AOF程序根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加到服务器的AOF缓存(redis.h/redisServer结构的aof_buf)中。
    • 文件写入和保存:调用aof.c/flushAppendOnlyFile函数,利用SAVE方法将AOF缓存中的内容写入到AOF文件末尾;如果满足保存条件,调用fsync或fdatasync函数,将写入的内容真正地保存S到磁盘中
  • aof保存模式:决定flushAppendOnlyFile函数的WRITE和SAVE的调用条件

    • AOF_FSYNC_NO:不保存。WRITE由主线程执行,阻塞主进程,不执行SAVE;SAVE只会在Redis被关闭、AOF功能被关闭、系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行)执行
    • AOF_FSYNC_ALWAYS:每次执行完命令,SAVE和WRITE都由主进程执行,阻塞主进程。效率最低
    • AOF_FSYNC_EVERYSEC:每秒钟保存一次。WRITE由主进程执行,阻塞主进程。SAVE由子线程执行,不阻塞主进程,但保存操作完成的快慢会影响写入操作的阻塞时长。注意,实际程序在这种模式下对fsync或fdatasync的调用并不是每秒一次,它和调用flushAppendOnlyFile函数时Redis所处的状态有关。可能会出现以下四种情况:如果在情况1中发生故障停机, 用户损失小于2秒内所产生的所有数据。如果在情况2发生故障停机, 那么用户损失的数据是可以超过2秒的。AOF在官网“每一秒钟保存一次”时发生故障, 只丢失1秒钟数据的说法, 实际上并不准确
      aofsavemodel.png
      aofsavemodelcompare.png

68. redis优缺点?

  • 优点

    • 速度快:因为数据存在内存中
    • 支持丰富数据类型String,List,Set,Sorted Set,Hash 五种基础的数据结构。单个Value最大限制是1GB,还提供Bitmap、HyperLogLog、GEO 等高级的数据结构
    • 丰富的特性:订阅发布Pub/Sub功能;Key过期策略;事务;支持多个DB;计数;Stream功能,支持多播的可持久化的消息队列,提供类似Kafka的功能
    • 持久化存储:提供RDB和AOF两种数据的持久化存储方案,解决内存数据库数据丢失问题
    • 高可用:内置Redis Sentinel提供高可用方案,实现主从故障自动转移。内置Redis Cluster提供集群方案,实现基于槽的分片方案,从而支持更大的Redis规模
  • 缺点

    • Redis单台机器存储的数据量,受限于机器本身的内存大小。虽然有Key过期策略,但是还是要预估和节约内存。如果内存增长过快,需要定期删除数据。可用Redis Cluster、Codis等方案进行分区,从单机Redis变成集群Redis
    • 如果进行完整重同步,需要生成RDB文件和传输,会占用主机的 CPU ,并会消耗现网的带宽。
    • 修改配置文件重启,时间比较久。在这个过程中,Redis 不能提供服务。

69. Redis 不适合的场景?

  • 数据量太大、数据访问频率非常低的业务,数据太大会增加成本,访问频率太低,保存在内存中纯属浪费资源。

73. list类型底层数据结构?

  • 底层结构:quickList双链表结构,每个双链表节点中保存一个ziplist,每个ziplist中存一批list中的数据,既避免大量链表指针带来的内存消耗,也可避免ziplist更新导致的大量性能损耗
    quicklist.png

  • ziplist与linkedlist优缺点对比:

    • 两端进行push和pop操作时间复杂度都是O(1)
    • 双向链表linkedlist内存开销比较大。每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。
    • ziplist存储上一个entry的长度和当前entry的长度,通过长度推算下一个元素在什么地方,使用连续的内存块,存储效率很高。但是插入和删除操作需要申请和释放内存(重新生成一个新的ziplist来作为更新后的list)。可能会导致大批量的数据拷贝。适合每个列表项要么就是小整数值,要么就是长度比较短的字符串的场景
  • 当列表对象保存的所有字符串元素的长度都小于64字节且元素数量小于512个时使用ziplist,否则使用linkedlist

  • ziplist结构及遍历过程
    ziplist.jpg

- 从前向后遍历时,程序从指向节点e1的指针p开始,计算节点e1的长度(e1-size), 然后将p加上 e1-size ,就将指针后移到了下一个节点e2 ...如此反复,直到p遇到ZIPLIST_ENTRY_END为止
                              p + e1-size + e2-size
                 p + e1-size     |
           p          |          |
           |          |          |
           V          V          V
+----------+----------+----------+----------+----------+----------+----------+
| ZIPLIST  |          |          |          |          |          | ZIPLIST  |
| ENTRY    |    e1    |    e2    |    e3    |    e4    |   ...    | ENTRY    |
| HEAD     |          |          |          |          |          | END      |
+----------+----------+----------+----------+----------+----------+----------+

           |<-------->|<-------->|
             e1-size    e2-size
- 从后往前遍历时,程序从指向节点eN的指针p出发,取出eN的pre_entry_length值,然后用p减去pre_entry_length,这就将指针移动到了前一个节点eN-1...,如此反复,直到p遇到ZIPLIST_ENTRY_HEAD 为止
                                         p - eN.pre_entry_length
                                            |
                                            |          p
                                            |          |
                                            V          V
+----------+----------+----------+----------+----------+----------+----------+
| ZIPLIST  |          |          |          |          |          | ZIPLIST  |
| ENTRY    |    e1    |    e2    |   ...    |   eN-1   |    eN    | ENTRY    |
| HEAD     |          |          |          |          |          | END      |
+----------+----------+----------+----------+----------+----------+----------+

typedef struct listNode {// listNode:24字节
  struct listNode *prev; // 前置节点
  struct listNode *next; // 后置节点
  void *value; // 节点的值
} listNode;
typedef struct entry{// entry⼤概10字节
  previous_entry_length:1,5字节(前⼀个entrylen,常为1字节)
  encoding:1,2,5(编码格式,常为1字节)
  content:保存实际数据。
}

74. set数据类型底层?

  • 编码可以是intset或者hashtable;intset整数集合作为底层实现,包含的所有元素都被保存在整数集合里面。
  • 当集合对象保存的所有元素都是整数值且元素数量不超过512个时使用使用intset编码否则使用hashtable

75. sorted set数据类型底层?

  • 编码可以是ziplist或者skiplist
    • ziplist按分值从小到大的进行排序,分值小的元素放在靠近表头方向,对象在前,值在后。
    • skiplist编码的有序集合使用zset结构作为底层实现,一个zset结构同时包含一个字典和跳跃表
  • 序集合保存的元素数量小于128个且元素成员的长度都小于64个字节使用ziplist,否则skiplist
  • skiplist 有序,通过在每个节点中维持多个指向其他节点的索引指针,从而达到快速访问节点的目的。 他在链表的基础上,增加了多层级索引,跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找。
    skiplist.png
typedef struct zskiplist {
    struct zskiplistNode *header, *tail; // 表头节点和表尾节点
    unsigned long length;// 表中节点的数量
    int level;// 表中层数最大的节点的层数
} zskiplist;

typedef struct zskiplistNode {
    struct zskiplistNode *backward;// 后退指针
    double score;// 分值
    robj *obj;// 成员对象
    struct zskiplistLevel {// 层
        struct zskiplistNode *forward; // 前进指针
        unsigned int span;// 跨度
    } level[];
} zskiplistNode;

76. hashs数据结构底层?

  • 哈希对象的编码是ziplist(压缩列表)或hashtable
  • ziplist会先保存键再保存值,因此键与值总是靠在一起,其中键的方向为压缩列表的表头方向。
  • 保存的所有字符串元素的长度都小于64字节且保存的元素数量小于512个使用ziplist编码,否则使用hashtable
  • hash冲突:Redis的哈希表使用链地址法解决hash冲突,即冲突的位置上使用单链表的连接,解决冲突的问题。
  • rehash重新散列:随着hash表的操作不断进行,哈希表保存的键值会逐渐地增多或减少,为了让哈希表的负载因子保持在一个合理的范围。
  • 渐进式rehash:在访问节点操作时顺便的把节点的值rehash过去。采取分而治之的方式,将rehash键值对所需的计算工作均摊到字典的每个添加、删除、查找和更新上,从而避免集中式rehash带来的庞大计算量。
  • 动态的负载因子:哈希表的负载因子 = 哈希表已保存的节点/哈希表大小。判断是否需要对哈希表进行扩容或者收缩,扩容及收缩操作可以通过rehash实现
  • 扩容时机:
    • 没有执行BGSAVE或BGREWRITEAOF命令,且哈希表负载因子大于等于1
    • 执行BGSAVE或BGREWRITEAOF命令,且哈希表负载因子大于等于5
    • 由于BGSAVE或BGREWRITEAOF命令都是开启子线程进行操作,而操作系统正常都使用COW(Copy-On-Write)技术优化子线程效率。避免子线程运行时进行扩容,可以避免不必要的写操作,进而节省内存。
  • 收缩时机:当哈希表负载因子小于0.1时
  • 扩容过程:
    • redis的hash使用了两个全局哈希表。开始默认使用「hashtable 0」保存键值对数据,「hashtable 1」没有分配空间。
    • 触发扩容时。系统给「hashtable 1」分配的大小为第一个大于等于 「hashtable 0」.used*2的 2的n次方幂的值
    • 将「hashtable 0 」的数据重新映射拷贝到 「hashtable 1」中;采用了渐进式 rehash,每次处理客户端请求hashtable执行增删改查操作时,顺带将节点rehash到hashtable 1中。
    • 释放「hashtable 0」的空间。
    • rehash进行时,字典会同时操作hashtable 0与hashtable 1,如查找就要在两个表中查找,而新增只操作到新表。

77. bitmap数据结构底层?

  • 使用String类型的SDS数据结构来保存位数组,把每个字节数组的8个bit位利用起来,每个bit位表示一个元素的二值状态(0或1)。
    bitmap.png

78. Redis是单线程的,如何提高多核CPU的利用率?

  • 可以在同一个服务器部署多个Redis实例,当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的, 所以如果想使用多个CPU可以考虑分区。

79. 为什么不建议在主Redis节点开启RDB功能呢?

  • 因为会带来一定时间的阻塞,特别是数据量大的时候。
  • 子进程fork相关的阻塞:bgsave时,Redis主进程会fork一个子进程,利用操作系统的写时复制技术,子进程在拷贝父进程虽然不需要全拷贝,但耗时跟主进程占用的内存是成正比,可以通过统计项info stats里的 last_fork_usec查看
  • CPU单线程相关的阻塞:如果绑定了CPU,则子进程会与主进程共享一个CPU ,而子进程进行持久化的时候是非常占CPU,可能导致提供服务的主进程发生阻塞(因此如果需要持久化功能,不建议绑定CPU)
  • 内存相关的阻塞:虽然利用写时复制技术可以大大降低进程拷贝的内存消耗,但这也导致了父进程在处理写请求时需要维护修改的内存页,因此这部分内存过大的话(修改页数多或每页占空间大)也会导致父进程的写操作阻塞。(而不巧的是,Linux中TransparentHugePage 会将复制内存页面单位有 4K 变成 2M ,这对于 Redis 来说是比较不友好的,也是建议优化的)
  • 磁盘相关的阻塞:极端情况下,假设整个机器的内存已经所剩无几,触发了内存交换(SWAP),则整个 Redis的效率将会非常低下(不仅仅针对 save/bgsave ),因此,关注系统的io情况,也是定位阻塞问题方法

80. AOF 文件的读取和数据还原?

  • 创建一个不带网络连接的伪客户端(fake client)反复读取AOF保存的文本,并根据协议内容还原出命令、命令的参数以及命令的个数。使用伪客户端执行该命令。直到AOF文件中的所有命令执行完毕。

81. 什么是AOF重写?实现原理?

  • 原因:AOF文件过大时。Redis需要对AOF文件进行重写(rewrite)
  • 原理:读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到新的AOF文件,等到全部记录完后,就将新的AOF文件替换掉现有的AOF文件。
  • AOF重写是在子进程里执行的
    • 优点:子进程进行AOF重写期间,主进程可以继续处理命令请求。子进程带有主进程的数据副本,使用子进程而不是线程,可以在避免锁的情况下,保证数据的安全性。
    • 缺点:子进程进行AOF重写期间,主进程处理命令请求可能对现有的数据进行修改,会让当前数据库的数据和重写后的AOF文件中的数据不一致;如果Redis内存大,可能会因为fork阻塞主进程;
  • BGREWRITEAOF命令工作原理:Redis增加了一个AOF重写缓存, 在fork出子进程之后开始启用, Redis主进程在接到新的写命令之后, 会将这个写命令的协议内容追加到现有的AOF文件和这个缓存中。当子进程完成AOF重写之后,将AOF重写缓存中的内容全部写入到新AOF文件中。对新的AOF文件进行改名,覆盖原有的AOF文件。在整个AOF后台重写过程中, 只有最后的写入重写缓存和改名操作会阻塞主进程, 在其他时候不阻塞主进程,将AOF重写对性能造成的影响降到了最低
  • 触发条件
    • 调用BGREWRITEAOF手动触发
    • AOF功能开启的情况下,每次serverCron函数执行时。没有BGSAVE、BGREWRITEAOF命令在进行。当前AOF文件大小大于server.aof_rewrite_min_size(默认1MB)。当前AOF文件大小aof_current_size和最后一次AOF重写后的大小aof_rewrite_base_size之间的比率aof_rewrite_perc大于等于指定的增长百分比(100%)。即当前AOF文件大小比最后一次AOF重写时的大小要大一倍

82. SAVE、 BGSAVE、 AOF写入和BGREWRITEAOF 4个指令执行的互斥关系

  • SAVE执行期间,AOF写入可以在后台线程进行,BGREWRITEAOF可以在子进程进行,所以这三种操作可以同时进行。其他的不能与SAVE同时进行
  • BGSAVE执行期间不能与SAVE、新的BGSAVE同时执行,为了避免两个rdbSave交叉执行造成竞争条件。
  • 为了避免两个子进程大量IO操作造成性能问题,BGSAVE和BGREWRITEAOF不能同时执行。

83. AOF采用的是“写后日志”的方式,MySQL则采用的是“写前日志”,Redis为什么要先执行命令,再把数据写入日志呢? 后写日志又有什么风险?

  • 避免出现记录错误命令的情况,不会阻塞写操作的执行
  • 执行写操作命令后宕机,丢失写操作日志数据,可能阻塞下个命令,因为写日志操作由主线程完成

84. 如何保证Redis中的数据都是热点数据?

  • 如果对缓存的访问符合幂律分布,即存在相对热点数据,或者我们不太清楚应用的缓存访问分布状况,可以选择allkeys-lru策略。如果在Redis 4.0版本,可使用volatile-lfu ,频率越高,代表越热。

85. Redis 回收进程如何工作的?

  • 一个客户端运行了新的写命令,添加了新的数据。
  • Redis 检查内存使用情况,如果大于 maxmemory 的限制, 则根据设定好的策略进行回收。
  • Redis 执行新命令。
  • 所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下(跌宕起伏)。

86. 如果有大量的 key 需要设置同一时间过期,一般需要注意什么?

  • Redis可能会出现短暂的卡顿现象。需要在时间上加一个随机值,使得过期时间分散一些。
  • 方案是调大hz参数(一秒钟内,后台任务期望被调用的次数),提高Redis 主动淘汰的频率,每次过期的key更多,从而最终达到避免一次过期过多。
  • 如果Redis中包含很多冷数据占用内存过大的话,调大hz,但不要超过 100。当值调大到100 CPU增加2%左右,对冷数据的内存释放速度有明显的提高(观察keyspace个数和used_memory大小)

88. Redis如何做内存优化?

  • redisObject对象:Redis存储的所有值对象在内部定义为redisObject结构体

    • 1.type字段:表示当前对象使用的数据类型,可使用type {key}命令查看对象所属类型,type命令返回的是值对象类型,键都是string类型。
    • 2.encoding字段:表示Redis内部编码类型,代表当前对象内部采用哪种数据结构实现。同一个对象采用不同的编码实现内存占用存在明显差异,
    • 3.lru字段:记录对象最后一次被访问的时间,当配置了maxmemory和maxmemory-policy=volatile-lru | allkeys-lru 时, 用于辅助LRU算法删除键数据。可以使用object idletime {key}命令在不更新lru字段情况下查看当前键的空闲时间。开发提示:可以使用scan [cursor] + object idletime  命令批量查询哪些键长时间未被访问,找出长时间不访问的键进行清理降低内存占用。
    • 4.refcount字段:记录当前对象被引用的次数,用于通过引用次数回收内存,当refcount=0时,可以安全回收当前对象空间。使用object refcount {key}获取当前对象引用。当对象为整数且范围在[0-9999]时,Redis可以使用共享对象的方式来节省内存。具体细节见之后共享对象池部分。
    • *ptr字段:与对象的数据内容相关,如果是整数直接存储数据,否则表示指向数据的指针。Redis在3.0之后对值对象是字符串且长度<=39字节的数据,内部编码为embstr类型,字符串sds和redisObject一起分配,从而只要一次内存操作。开发提示:高并发写入场景中,在条件允许的情况下建议字符串长度控制在39字节以内,减少创建redisObject内存分配次数从而提高性能。
  • 控制key的数量

  • 缩减键值对象:值对象可以使用通用压缩算法压缩json,xml后再存入Redis,从而降低内存占用,例如使用Snappy压缩

  • 编码优化

  • 共享对象池:Redis内部维护[0-9999]的整数对象池。用于节约内存。除了ziplist其他所有类型都使用整数对象池。开发尽量使用整数对象以节省内存。

  • 字符串优化

  • 为什么开启maxmemory和LRU淘汰策略后对象池无效?LRU算法需要获取对象最后被访问时间,以便淘汰最长未访问数据,每个对象最后访问时间存储在redisObject对象的lru字段。对象共享意味着多个引用共享同一个redisObject,这时lru字段也会被共享,导致无法获取每个对象的最后访问时间。如果没有设置maxmemory,直到内存被用尽Redis也不会触发内存回收,所以共享对象池可以正常工作。共享对象池与maxmemory+LRU策略冲突,对于ziplist编码的值对象,即使内部数据为整数也无法使用共享对象池,因为ziplist使用压缩且内存连续的结构,对象共享判断成本过高,

  • 为什么只有整数对象池?

    • 整数对象池复用的几率最大
    • 整数比较算法时间复杂度为O(1),效率高,只保留一万个整数为了防止对象池浪费。如果是字符串判断相等性,时间复杂度变为O(n),特别是长字符串更消耗性能(浮点数在Redis内部使用字符串存储)。对于更复杂的数据结构如hash,list等,相等性判断需要O(n2)。因此Redis只保留整数共享对象池。

89. 修改配置不重启 Redis 会实时生效吗?

  • 可通过CONFIG SET命令修改则无需重启。其他情况这必须重启

90. Redis如何实现高可用? Redis 集群都有哪些方案?选择条件?

  • redis主从模式:一主多从模式主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器只读。从服务器也可以当主服务器
  • redis哨兵(Sentinel)模式Redis Sentinel
  • redis集群Redis Cluster
  • Twemproxy
  • Codis
  • 客户端分片:在业务代码层实现,起几个无关联的Redis实例,在代码层对Key进行hash计算,然后去对应的Redis实例操作数据。代码要求高,考虑部分包括,节点失效后的替代算法方案,数据震荡后的自动脚本恢复,实例的监控等
  • 体量较小时,选择Redis Sentinel,单主Redis足以支撑业务。
  • 体量较大时,选择Redis Cluster ,通过分片,使用更多内存。
  • 多大体量需要使用Redis Cluster呢?10G+ 主要原因是:
    • 1、一次 RDB 时间随着内存越大,会变大越来越久。同时,一次 fork 的时间也会变久。还有,重启通过 RDB 文件,或者 AOF 日志,恢复时间都会变长。
    • 2、体量大之后,读写的 QPS 势必比体量小的时候打的多,那么使用 Redis Cluster 相比 Redis Sentinel ,可以分散读写压力到不同的集群中。

93. Redis Cluster(集群)redis集群有哪几种实现方式?√集群中那么多Master节点,redis cluster在存储的时候如何确定选择哪个节点呢?

  • Redis集群是分布式数据库方案,通过分片实现数据共享,将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高读写性能。并提供复制和故障转移功能。主要解决了大数据量存储导致的各种慢问题,同时也便于横向拓展
  • Redis集群中每个Redis要放开两个端口(如6379,16379)6379是用来节点间通信的(cluster bus)、故障检测、配置更新、故障转移授权。 集群之间通过Gossip协议相互交互集群信息,最后每个节点都保存着其他节点的 slots 分配情况。
  • Redis集群内节点通过ping/pong消息实现节点通信,消息包括节点槽信息,主从状态、节点故障等。因此故障发现也是通过消息传播机制实现的,
  • 节点选择实现:Redis Cluster有16384个哈希槽,每个key通过CRC16校验计算后对16384取模来决定放置哪个槽.集群的每个节点负责一部分hash槽,添加或删除节点只需要将节点上的槽移动即可,此时服务并不会停止。所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态.为什么是 16384 呢?主要考虑集群内的网络带宽,而 16384 刚好是 2K 字节大小。

96. Redis Cluster 会有写操作丢失吗?为什么不能保证数据的强一致性.?

  • Redis并不能保证数据的强一致性,而是异步复制,这意味集群在特定的条件下可能会丢失写操作。无论Redis Sentinel还是Cluster方案,都是通过主从复制,在数据的复制方面都存在相同的情况。

97. Redis集群如何选择数据库?

  • Redis集群目前无法做数据库选择,默认在0数据库。

98. 为什么要有Redis集群?

  • 哨兵模式下每台Redis服务器都存储相同的数据,很浪费内存空间;数据量太大,主从同步时严重影响了master性能。
  • 哨兵模式是中心化的集群实现方案,每个从机和主机的耦合度很高,master宕机到salve选举master恢复期间服务不可用。
  • 哨兵模式始终只有一个Redis主机来接收和处理写请求,写操作还是受单机瓶颈影响,没有实现真正的分布式架构。

99. Redis 是怎么部署?

  • Redis Cluster,10台机器,5台机器部署了Redis主实例,另外5台机器部署了Redis从实例,每个主实例挂了一个从实例,5个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒5万,5台机器最多是25万读写请求每秒
  • 机器是什么配置?32G内存+8核CPU+1T磁盘,但是分配给Redis进程的是10G内存,一般线上生产环境,Redis 的内存尽量不要超过 10G,5台机器对外提供读写,一共有 50G 内存。因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,Redis 从实例会自动变成主实例继续提供读写服务。
  • 你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb 。100 条数据是 1mb ,10 万条数据是 1G 。常驻内存的是 200 万条商品数据,占用内存是 20G ,仅仅不到总内存的 50% 。目前高峰期每秒就是 3500 左右的请求量。

100. redis分区

  • Redis分区是一种模式,将数据分区到不同的Redis节点上,而Redis集群的Redis Cluster、Twemproxy、Codis、客户端分片( 不包括 Redis Sentinel ) 这四种方案,是 Redis 分区的具体实现。
  • Redis 每个分区,如果想要实现高可用,需要使用到 Redis 主从复制。
  • Redis分区方案,主要分成两种类型:
    • 客户端分区,就是在客户端就已经决定数据会被存储到哪个 Redis 节点或者从哪个 Redis 节点读取。大多数客户端已经实现了客户端分区。如Redis Cluster 和客户端分区。
    • 代理分区,意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis响应结果返回给客户端。如Twemproxy 和 Codis

101. 使用哨兵模式在数据上有副本数据做保证,在可用性上又有哨兵监控,一旦master宕机会选举salve节点为master节点,这种已经满足了我们的生产环境需要,那为什么还需要使用集群模式呢?

  • 哨兵模式还是主从模式,在主从模式下我们可以通过增加slave节点来扩展读并发能力,但是没办法扩展写能力和存储能力,存储能力只能是master节点能够承载的上限。所以为了扩展写能力和存储能力,我们就需要引入集群模式。

102. 请用 Redis 和任意语言实现一段恶意登录保护的代码,限制 1 小时内每用户 Id 最多只能登录 5 次。分布式限流

  • 使用 Lua 脚本,实现令牌桶限流算法。具体可以看看艿艿对 《Spring-Cloud-Gateway 源码解析 —— 过滤器 (4.10) 之 RequestRateLimiterGatewayFilterFactory 请求限流》 的源码解析。
  • 使用 Lua 脚本,实现简单的滑动窗口。具体可以看看艿艿对 《精尽 Redisson 源码分析 —— 限流器 RateLimiter》 的源码解析。

103. Redis如何做大量数据插入?

  • Redis2.6开始,Redis-cli支持pipe mode的模式用于执行大量数据插入工作。

104. 如何实现Redis CAS操作?

  • 在Redis事务中,WATCH命令提供CAS功能。假设通过WATCH命令在事务执行之前监控了多个keys,倘若在WATCH之后有任何Key值发生了变化,EXEC命令执行的事务都将被放弃,同时返回nil应答以通知调用者事务执行失败

105. redis的健康指标

  • 存活情况:检查redis是否还活着,可以通过命令PING的响应是否是PONG来判断。
  • 连接数:连接的客户端数量,可通过命令src/redis-cli info Clients | grep connected_clients得到,这个值跟使用redis的服务的连接池配置关系比较大,建议不要超过5000,如果太大可能是redis处理太慢、拒绝连接数(rejected_connections)如果大于0,说明创建的连接数超过了maxclients,需要排查原因。是redis连接池配置不合理还是连接这个redis实例的服务过多等。
  • 阻塞客户端数量:blocked_clients,一般是执行了list数据类型的BLPOP或者BRPOP命令引起的,可通过命令src/redis-cli info Clients | grep blocked_clients得到,最好为0。
  • 使用内存峰值:命令config set maxmemory 10737418240设置允许使用的最大内存(不要超过20G),为了防止发生swap导致Redis性能骤降,甚至由于使用内存超标导致被系统kill,建议used_memory_peak的值与maxmemory的值有个安全区间,例如1G,那么used_memory_peak的值不能超过9663676416(9G)。另外,我们还可以监控maxmemory不能少于多少G,比如5G。
  • 内存碎片率:mem_fragmentation_ratio=used_memory_rss/used_memory,如果是redis4.0之前的版本只能重启。而redis4.0有一个主要特性就是优化内存碎片率问题(Memory de-fragmentation)。在redis.conf配置文件中有ACTIVE DEFRAGMENTATION:碎片整理允许Redis压缩内存空间,从而回收内存。默认是关闭的,可通过命令CONFIG SET activedefrag yes热启动。当这个值大于1时,表示分配的内存超过实际使用的内存,数值越大,碎片率越严重。当这个值小于1时,表示发生了swap,即可用内存不够。建议used_memory至少1G以上才考虑对内存碎片率进行监控。
  • 缓存命中率:keyspace_misses/keyspace_hits这两个指标用来统计缓存的命令率,keyspace_misses指未命中次数,keyspace_hits表示命中次数。keyspace_hits/(keyspace_hits+keyspace_misses)就是缓存命中率。建议0.9以上,如果缓存命中率过低,那么要排查对缓存的用法是否有问题!
  • OPSinstantaneous_ops_per_sec这个指标表示缓存的OPS,如果业务比较平稳,那么这个值也不会波动很大,这个字段的监控要结合自己的具体业务,不同时间段波动范围可能有所不同。
  • 持久化:rdb_last_bgsave_status/aof_last_bgrewrite_status,即最近一次或者说最后一次RDB/AOF持久化是否有问题,这两个值都应该是"ok"。由于redis持久化时会fork子进程,且fork是一个完全阻塞的过程,所以可以监控fork耗时即latest_fork_usec,单位是微妙,如果这个值比较大会影响业务,甚至出现timeout。
  • 失效KEY:如果把Redis当缓存使用,建议所有的key都设置了expire属性,通过命令src/redis-cli info Keyspace得到每个db中key的数量和设置了expire属性的key的属性,且expires需要等于keys:
    db0:keys=30,expires=30,avg_ttl=0
    db0:keys=23,expires=22,avg_ttl=0
  • 慢日志:通过命令slowlog get得到Redis执行的slowlog集合,理想情况下,slowlog集合应该为空,有时候由于网络波动等原因造成set key value这种命令执行也需要几毫秒,不能看到slowlog就想着去优化,简单的set/get可能也会出现在slowlog中。

106. 如何提高Redis命中率?

  • 缓存的命中率越高则表示使用缓存的收益越高,应用的性能越好(响应时间越短、吞吐量越高),抗并发的能力越强。
  • 查看命中率:telnet localhost 6379或者客户端执行info,命中率=keyspace_hits/(keyspace_hits+keyspace_misses)
  • 影响缓存命中率的几个因素
    • 1.业务场景和业务需求:缓存适合“读多写少”的业务场景,反之,命中率会很低。时效性要求越低,就越适合缓存。在相同key和相同请求数的情况下,缓存时间越长,命中率会越高。
    • 2.缓存的设计(粒度和策略):通常情况下,缓存的粒度越小,命中率会越高。当数据发生变化时,直接更新缓存的值会比移除缓存(或者让缓存过期)的命中率更高,当然,系统复杂度也会更高。
    • 3.缓存容量和基础设施:缓存的容量有限,则容易引起缓存失效和被淘汰(目前多数的缓存框架或中间件都采用了LRU算法)。同时,缓存的技术选型也是至关重要的,比如采用应用内置的本地缓存就比较容易出现单机瓶颈,而采用分布式缓存则毕竟容易扩展。所以需要做好系统容量规划,并考虑是否可扩展。此外,不同的缓存框架或中间件,其效率和稳定性也是存在差异的。
    • 4.其他因素:当缓存节点发生故障时,需要避免缓存失效并最大程度降低影响,这种特殊情况也是架构师需要考虑的。业内比较典型的做法就是通过一致性Hash算法,或者通过节点冗余的方式。
  • 既然业务需求对数据时效性要求很高,而缓存时间又会影响到缓存命中率,那么系统就别使用缓存了。其实这忽略了一个重要因素--并发。通常来讲,在相同缓存时间和key的情况下,并发越高,缓存的收益会越高,即便缓存时间很短。
  • 提高缓存命中率的方法:需要在业务需求,缓存粒度,缓存策略,技术选型等各个方面去通盘考虑并做权衡。尽可能的聚焦在高频访问且时效性要求不高的热点业务上(如字典数据、session、token),通过缓存预加载(预热)、增加存储容量、调整缓存粒度、更新缓存等手段来提高命中率。对于时效性很高(或缓存空间有限),内容跨度很大(或访问很随机),并且访问量不高的应用来说缓存命中率可能长期很低,可能预热后的缓存还没来得被访问就已经过期了。

107. 一个 Redis 实例最多能存放多少的 keys?List、Set、Sorted Set 他们最多能存放多少元素?

  • 理论上,Redis可以处理多达2^32的keys ,并且在实际中进行了测试,每个实例至少存放了亿5千万的keys。
  • 任何 list、set、和 sorted set 都可以放 2^32 个元素。

108. 缓存如何存储POJO对象?

  • 将POJO对象序列化进行存储,适合Redis和Memcached。StringRedisSerializer、FastJsonRedisSerializer和KryoRedisSerializer。对于POJO对象比较大,考虑使用压缩算法,例如说Snappy、zlib、GZip等
  • 使用Hash数据结构,适合Redis。使用JSON序列化。也可通过ziplist的编码方式,压缩数据