WikiWiki
首页
Java开发
Java面试
Linux手册
  • AI相关
  • Python Flask
  • Pytorch
  • youlo8
SEO
uniapp小程序
Vue前端
work
数据库
软件设计师
入门指南
首页
Java开发
Java面试
Linux手册
  • AI相关
  • Python Flask
  • Pytorch
  • youlo8
SEO
uniapp小程序
Vue前端
work
数据库
软件设计师
入门指南
  • Java基础面试题
  • Mybatis面试
  • Redis面试解决方案
  • java集合面试题
  • 慕课_剑指Offer1
  • 慕课_剑指Offer2
  • 数据库SQL语句优化
  • 简历要背的

[TOC]

Redis

1 基本概念

1.1 常见考点

1、Redis 为何这么快?

  • 基于内存: Redis是一个基于内存的数据库,这意味着它将数据存储在内存中。由于RAM的读写速度远高于硬盘,因此Redis能够快速读写数据,从而大大提高了性能。
  • 单线程减少上下文切换,同时保证原子性: Redis是单线程的,这意味着它在任何给定时刻只会执行一个操作。虽然听起来似乎会降低性能,但实际上这有助于减少多线程并发操作中的锁竞争和线程切换开销。此外,由于Redis是基于内存的,因此它的操作通常很快,这意味着单线程处理速度足够快。此外,Redis通过使用事务和命令原子性来确保数据的一致性和可靠性,这也有助于提高性能。
  • IO多路复用: Redis使用IO多路复用技术来处理并发连接。这意味着Redis可以同时处理多个客户端的请求,而不需要为每个请求创建一个新的线程或进程。这减少了系统资源的消耗,提高了系统的并发性能。
  • 高级数据结构: Redis提供了许多高级数据结构,如字符串(SDS)、哈希表、有序集合、列表等。这些数据结构经过优化,能够在内存中高效存储和操作数据。例如,哈希表和有序集合可以提供常量时间复杂度的查找、插入和删除操作,而跳表则可以提供对有序集合的高效实现。这些高级数据结构使得Redis能够更快地处理复杂的数据操作,从而提高了整体性能。

2、为何使用单线程?

  • 官方答案

    因为 Redis 是基于内存的操作,CPU 不会成为 Redis 的瓶颈,而最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。

  • 详细原因 1)不需要各种锁的性能消耗

    ​ Redis 的数据结构并不全是简单的 Key-Value,还有 List,Hash 等复杂的结构,这些结构有可能会进行很细粒度的操作,比如在很长的列表后面添加一个元素,在hash当中添加或者删除一个对象。这些操作可能就需要加非常多的锁,导致的结果是同步开销大大增加。

    2)单线程多进程集群方案

    ​ 单线程的威力实际上非常强大,每核心效率也非常高,多线程自然是可以比单线程有更高的性能上限,但是在今天的计算环境中,即使是单机多线程的上限也往往不能满足需要了,需要进一步摸索的是多服务器集群化的方案,这些方案中多线程的技术照样是用不上的。所以单线程、多进程的集群不失为一个时髦的解决方案。

3、缓存三大问题以及解决方案?

  • 缓存穿透:当查询的数据不存在于缓存中时,每次请求都会直接查询数据库,这可能导致数据库压力过大。
    • 解决方案:
      1. 缓存空值:将不存在的键对应的值设置为空值或占位符存入缓存,以避免频繁查询数据库。
      2. Key 值校验:使用布隆过滤器等技术在缓存层面对查询的键进行校验,只有合法的键才会进一步查询数据库。
  • 缓存击穿:当某个缓存键过期时,如果此时有大量请求同时访问该键,这些请求将直接访问数据库,导致数据库压力剧增。
    • 解决方案:
      1. 互斥锁:在缓存失效时,通过加锁来保证只有一个线程能够重新生成缓存,其他线程等待其完成并使用新缓存。
      2. 热点数据永不过期:对于热点数据,设置其缓存永不过期,避免因缓存过期导致的击穿问题。
      3. 熔断降级:当缓存失效后,临时关闭对该数据的访问或返回默认值,以减少数据库压力。
  • 缓存雪崩:在同一时间,大量缓存键同时过期,导致大量请求直接访问数据库,造成数据库压力激增。
    • 解决方案:
      1. 热点数据不过期:对于热点数据,可以设置其缓存永不过期,以避免同时过期引发的问题。
      2. 随机分散过期时间:为缓存键设置随机的过期时间,使其不会在同一时间大规模失效,从而分散请求到达数据库的时间,减少压力。

4、先删后写还是先写后删?

  • 先删缓存后写数据库:这种方法会产生脏数据的概率较大,因为在删除缓存后写入数据库之前,其他并发操作可能会查询到旧的数据并将其放入缓存中,导致缓存中的数据是脏的。即使后续更新了数据库,缓存中的数据仍然是旧的,这可能会导致一段时间内系统读取到的都是旧数据。
  • 先写数据库再删缓存:这种方法脏数据的概率较小,但可能会出现一致性问题。当更新操作进行时,如果同时有查询操作并且命中了缓存,那么查询得到的数据是旧的。但是这种情况不会影响后续的查询。

解决方案:

  1. 设置缓存过期时间:通过设置合适的缓存过期时间,即使出现脏数据,也能在一定时间后自动更新,实现最终一致性。
  2. 使用中间件进行异步更新:例如,使用消息队列或发布订阅模式,监听数据库变更日志(binlog),在数据更新时异步更新缓存,保持数据的一致性。
  3. 使用分布式一致性协议:如两阶段提交(2PC)或 Paxos 算法等,保证写入数据库和删除缓存的原子性操作,从而确保数据的一致性。

5、如何保证 Redis 的高并发?

Redis 通过主从加集群架构,实现读写分离,主节点负责写,并将数据同步给其他从节点,从节点负责读,从而实现高并发。

具体参考

img

单机的redis几乎不太可能说QPS超过10万+,一般在几万。

除非一些特殊情况,比如你的机器性能特别好,配置特别高,物理机,维护做的特别好,而且你的整体的操作不是太复杂。

img

Redis通过主从架构,实现读写分离,主节点负责写,并将数据同步给其他从节点,从节点负责读,从而实现高并发。

Redis高并发的同时,还需要容纳大量的数据:一主多从,每个实例都容纳了完整的数据,比如redis主就10G的内存量,其实你就最对只能容纳10g的数据量。如果你的缓存要容纳的数据量很大,达到了几十g,甚至几百g,或者是几t,那你就需要redis集群,而且用redis集群之后,可以提供可能每秒几十万的读写并发。

replication的核心机制

redis采用异步方式复制数据到slave节点,不过redis 2.8开始,slave node会周期性地确认自己每次复制的数据量   一个master node是可以配置多个slave node的   slave node也可以连接其他的slave node   slave node做复制的时候,是不会block master node的正常工作的   slave node在做复制的时候,也不会block对自己的查询操作,它会用旧的数据集来提供服务; 但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了   slave node主要用来进行横向扩容,做读写分离,扩容的slave node可以提高读的吞吐量

master持久化对于主从架构的安全保障的意义

如果采用了主从架构,那么建议必须开启master node的持久化!

不建议用slave node作为master node的数据热备,因为那样的话,如果你关掉master的持久化,可能在master宕机重启的时候数据是空的,然后可能一经过复制,salve node数据也丢了

第二个,master的各种备份方案,要不要做,万一说本地的所有文件丢失了; 从备份中挑选一份rdb去恢复master; 这样才能确保master启动的时候,是有数据的

img

master同步数据给slave的过程

当启动一个slave node的时候,它会发送一个PSYNC命令给master node

如果这是slave node重新连接master node,那么master node仅仅会复制给slave部分缺少的数据; 否则如果是slave node第一次连接master node,那么会触发一次full resynchronization

开始full resynchronization的时候,master会启动一个后台线程,开始生成一份RDB快照文件,同时还会将从客户端收到的所有写命令缓存在内存中。

RDB文件生成完毕之后,master会将这个RDB发送给slave,slave会先写入本地磁盘,然后再从本地磁盘加载到内存中。然后master会将内存中缓存的写命令发送给slave,slave也会同步这些数据。

slave node如果跟master node有网络故障,断开了连接,会自动重连。master如果发现有多个slave node都来重新连接,仅仅会启动一个rdb save操作,用一份数据服务所有slave node。

主从复制的断点续传

从redis 2.8开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份

master node会在内存中常见一个backlog,master和slave都会保存一个replica offset还有一个master id,offset就是保存在backlog中的。

如果master和slave网络连接断掉了,slave会让master从上次的replica offset开始继续复制

但是如果没有找到对应的offset,那么就会执行一次resynchronization

无磁盘化复制

master在内存中直接创建rdb,然后发送给slave,不会在自己本地落地磁盘了

repl-diskless-sync   repl-diskless-sync-delay,等待一定时长再开始复制,因为要等更多slave重新连接过来

过期key处理

slave不会过期key,只会等待master过期key。

如果master过期了一个key,或者通过LRU淘汰了一个key,那么会模拟一条del命令发送给slave。

复制的完整流程

slave node启动,仅仅保存master node的信息,包括master node的host和ip(redis.conf里面的slaveof配置的),但是复制流程没开始

slave node内部有个定时任务,每秒检查是否有新的master node要连接和复制,如果发现,就跟master node建立socket网络连接   slave node发送ping命令给master node   口令认证,如果master设置了requirepass,那么salve node必须发送masterauth的口令过去进行认证   master node第一次执行全量复制,将所有数据发给slave node   master node后续持续将写命令,异步复制给slave node

数据同步相关的核心机制

指的就是第一次slave连接msater的时候,执行的全量复制,那个过程里面你的一些细节的机制

(1)master和slave都会维护一个offset

master会在自身不断累加offset,slave也会在自身不断累加offset   slave每秒都会上报自己的offset给master,同时master也会保存每个slave的offset

这个倒不是说特定就用在全量复制的,主要是master和slave都要知道各自的数据的offset,才能知道互相之间的数据不一致的情况

(2)backlog

master node有一个backlog,默认是1MB大小   master node给slave node复制数据时,也会将数据在backlog中同步写一份   backlog主要是用来做全量复制中断候的增量复制的

(3)master run id

info server,可以看到master run id   如果根据host+ip定位master node,是不靠谱的,如果master node重启或者数据出现了变化,那么slave node应该根据不同的run id区分,run id不同就做全量复制   如果需要不更改run id重启redis,可以使用redis-cli debug reload命令

(4)psync

从节点使用psync从master node进行复制,psync runid offset   master node会根据自身的情况返回响应信息,可能是FULLRESYNC runid offset触发全量复制,可能是CONTINUE触发增量复制

全量复制

master执行bgsave,在本地生成一份rdb快照文件   master node将rdb快照文件发送给salve node,如果rdb复制时间超过60秒(repl-timeout),那么slave node就会认为复制失败,可以适当调节大这个参数   对于千兆网卡的机器,一般每秒传输100MB,6G文件,很可能超过60s   master node在生成rdb时,会将所有新的写命令缓存在内存中,在salve node保存了rdb之后,再将新的写命令复制给salve node   client-output-buffer-limit slave 256MB 64MB 60,如果在复制期间,内存缓冲区持续消耗超过64MB,或者一次性超过256MB,那么停止复制,复制失败   slave node接收到rdb之后,清空自己的旧数据,然后重新加载rdb到自己的内存中,同时基于旧的数据版本对外提供服务   如果slave node开启了AOF,那么会立即执行BGREWRITEAOF,重写AOF

增量复制

如果全量复制过程中,master-slave网络连接断掉,那么salve重新连接master时,会触发增量复制   master直接从自己的backlog中获取部分丢失的数据,发送给slave node,默认backlog就是1MB   msater就是根据slave发送的psync中的offset来从backlog中获取数据的

heartbeat

主从节点互相都会发送heartbeat信息

master默认每隔10秒发送一次heartbeat,salve node每隔1秒发送一个heartbeat

异步复制

master每次接收到写命令之后,现在内部写入数据,然后异步发送给slave node

6、Redis 如何保证原子性?

答案很简单,因为 Redis 是单线程的,所以 Redis 提供的 API 也是原子操作。

但我们业务中常常有先 get 后 set 的业务常见,在并发下会导致数据不一致的情况。

如何解决

​ 1)使用 incr、decr、setnx 等原子操作;

​ 2)客户端加锁;

​ 3)使用 Lua 脚本实现 CAS 操作。

7、有哪些应用场景?

  • Redis 在互联网产品中使用的场景实在是太多太多,这里分别对 Redis 几种数据类型做了整理:

    1)String:缓存、限流、分布式锁、计数器、分布式 Session 等。

    2)Hash:用户信息、用户主页访问量、组合查询等。

    3)List:简单队列、关注列表时间轴。

    4)Set:赞、踩、标签等。

    5)ZSet:排行榜、好友关系链表。

1.2 常用命令

1、Redis 有哪些常用操作?

  • cmd访问redis

    redis-cli.exe -h 127.0.0.1 -p 6379
    
  • key

    keys *: 这个命令用于获取当前数据库中的所有键(keys)。
    select 0: 这个命令用于选择数据库,数字表示数据库的索引,0表示第一个数据库。Redis支持多个数据库,默认有16个数据库(编号从0到15)。
    move myString 1: 这个命令用于将指定键移动到另一个数据库中,第一个参数是要移动的键,第二个参数是目标数据库的索引。如果目标数据库中已经存在同名的键,则无法移动。
    flush db: 这个命令用于清空当前数据库中的所有键值对,相当于将当前数据库重置为空。
    randomkey: 这个命令用于随机获取当前数据库中的一个键。
    type key: 这个命令用于获取指定键对应的值的数据类型,常见的数据类型有字符串、哈希、列表、集合和有序集合等。
    set key1 value1: 这个命令用于设置指定键的值为指定的内容。
    get key1: 这个命令用于获取指定键的值。
    mset key1 value1 key2 value2 key3 value3: 这个命令用于同时设置多个键值对。
    mget key1 key2 key3: 这个命令用于同时获取多个键对应的值。
    del key1: 这个命令用于删除指定的键及其对应的值。
    exists key: 这个命令用于判断指定的键是否存在。
    expire key 10: 这个命令用于设置指定键的过期时间,单位为秒。
    pexpire key 1000: 这个命令用于设置指定键的过期时间,单位为毫秒。
    persist key: 这个命令用于移除指定键的过期时间,使其永久有效。
    
  • string

set name cxx: 将键名为 name 的字符串键设置为值 cxx。
get name: 获取键名为 name 的字符串键的值。
getrange name 0 -1: 获取键名为 name 的字符串键的指定范围内的子串。在这里,0 表示起始偏移量,-1 表示结束偏移量(表示整个字符串)。
getset name new_cxx: 设置键名为 name 的字符串键的值为 new_cxx,并返回原来的值。
mset key1 key2: 同时设置多个键值对。
mget key1 key2: 同时获取多个键对应的值。
setnx key value: 如果键 key 不存在,则设置键值为 value。
setex key time value: 设置键 key 的值为 value,并同时设置过期时间为 time 秒。
setrange key index value: 从指定偏移量 index 开始替换键 key 的值为 value。
incr age: 将键 age 的值递增1。
incrby age 10: 将键 age 的值递增10。
decr age: 将键 age 的值递减1。
decrby age 10: 将键 age 的值递减10。
incrbyfloat: 将键的值以浮点数的形式增加或减少。
append: 将指定的值追加到键的当前值的末尾。
strlen: 获取键值的长度。
getbit/setbit/bitcount/bitop: 用于位操作,可以对存储在字符串值中的二进制位进行操作,包括获取、设置、统计、逻辑运算等。
  • hash
hset myhash name cxx: 将哈希表 myhash 中的字段 name 设置为值 cxx。
hget myhash name: 获取哈希表 myhash 中字段 name 对应的值。
hmset myhash name cxx age 25 note "i am notes": 同时设置哈希表 myhash 中多个字段和值。
hmget myhash name age note: 获取哈希表 myhash 中多个字段对应的值。
hgetall myhash: 获取哈希表 myhash 中的所有字段和值。
hexists myhash name: 判断哈希表 myhash 中是否存在字段 name。
hsetnx myhash score 100: 如果哈希表 myhash 中不存在字段 score,则设置字段 score 的值为 100。
hincrby myhash id 1: 将哈希表 myhash 中字段 id 的值增加 1。
hdel myhash name: 删除哈希表 myhash 中的字段 name。
hkeys myhash: 获取哈希表 myhash 中的所有字段。
hvals myhash: 获取哈希表 myhash 中的所有值。
hlen myhash: 获取哈希表 myhash 中字段的数量。
  • list
lpush mylist a b c: 将值 a、b、c 从左侧依次插入到列表 mylist 中。
rpush mylist x y z: 将值 x、y、z 从右侧依次插入到列表 mylist 中。
lrange mylist 0 -1: 获取列表 mylist 中所有元素,索引从 0 到 -1 表示取所有元素。
lpop mylist: 从列表 mylist 的左侧弹出一个元素,并返回该元素的值。
rpop mylist: 从列表 mylist 的右侧弹出一个元素,并返回该元素的值。
llen mylist: 获取列表 mylist 的长度,即列表中元素的个数。
lrem mylist count value: 从列表 mylist 中删除值为 value 的元素,最多删除 count 个。
lindex mylist 2: 获取列表 mylist 中索引为 2 的元素的值。
lset mylist 2 n: 将列表 mylist 中索引为 2 的元素的值设为 n。
ltrim mylist 0 4: 保留列表 mylist 中索引从 0 到 4 的元素,其余元素删除。
linsert mylist before a: 在列表 mylist 中值为 a 的元素之前插入一个新元素。
linsert mylist after a: 在列表 mylist 中值为 a 的元素之后插入一个新元素。
rpoplpush list list2: 将列表 list 中的最后一个元素弹出,并将其添加到列表 list2 的开头。
  • set
sadd myset redis: 将元素 redis 添加到集合 myset 中。
smembers myset: 获取集合 myset 中的所有成员(元素)。
srem myset set1: 从集合 myset 中移除元素 set1。
sismember myset set1: 判断元素 set1 是否在集合 myset 中,如果存在返回 1,否则返回 0。
scard key_name: 获取集合 key_name 中的元素个数。
sdiff | sinter | sunion: 这些命令分别用于计算多个集合的差集、交集和并集。
srandmember: 随机获取集合中的一个元素。
spop: 从集合中随机弹出一个元素并返回。相较于 srandmember,它会将弹出的元素从集合中移除。
  • zset
zadd zset 1 one: 将成员 one 添加到有序集合 zset 中,并指定分数为 1。
zincrby zset 1 one: 将有序集合 zset 中成员 one 的分数增加 1。
zscore zset two: 获取有序集合 zset 中成员 two 的分数。
zrange zset 0 -1 withscores: 获取有序集合 zset 中所有成员,并按照分数从小到大的顺序排列,同时返回成员及其分数。
zrangebyscore zset 10 25 withscores: 获取有序集合 zset 中分数在 10 到 25 之间的所有成员及其分数。
zrangebyscore zset 10 25 withscores limit 1 2: 分页查询,获取有序集合 zset 中分数在 10 到 25 之间的成员及其分数,返回从第 1 个成员开始的 2 个成员。
zcard zset: 获取有序集合 zset 中的成员数量。
Zcount zset 0 1: 获取有序集合 zset 中分数在 0 到 1 之间的成员个数。
Zrem zset one two: 从有序集合 zset 中移除成员 one 和 two。
Zremrangebyrank zset 0 1: 移除有序集合 zset 中排名在 0 到 1 之间的所有成员。
Zremrangebyscore zset 0 1: 移除有序集合 zset 中分数在 0 到 1 之间的所有成员。
Zrank zset 0 -1: 获取有序集合 zset 中分数最小的成员的排名。
Zrevrank zset 0 -1: 获取有序集合 zset 中分数最大的成员的排名。
Zinterstore、zunionstore: 这两个命令分别用于计算多个有序集合的交集和并集,并将结果存储在一个新的有序集合中。
  • 排序:
sort mylist: 对列表 mylist 中的元素进行排序,默认是按照数字大小升序排列。
sort mylist alpha desc limit 0 2: 对列表 mylist 中的元素进行字母顺序排序,并按照降序排列,同时限制返回结果的范围为从第0个元素开始的2个元素。
sort list by it:* desc: 对列表 list 中的元素进行排序,排序的依据是列表中每个元素的键名为 it:* 的值(假设键名为 it:* 的键都存在),并按照降序排列。
sort list by it:* desc get it:*: 对列表 list 中的元素进行排序,排序的依据是列表中每个元素的键名为 it:* 的值,并按照降序排列,同时获取排序后的元素的值。
sort list by it:* desc get it:* store sorc:result: 对列表 list 中的元素进行排序,排序的依据是列表中每个元素的键名为 it:* 的值,并按照降序排列,同时获取排序后的元素的值,并将排序后的结果存储到一个新的有序集合 sort:result 中。
  • 订阅与发布:
subscribe chat1: 订阅频道 chat1,即开始接收该频道的消息。
publish chat1 "hello ni hao": 向频道 chat1 发布消息 "hello ni hao",所有订阅了该频道的客户端都会收到这条消息。
pubsub channels: 查看当前有哪些频道。
pubsub numsub chat1: 查看频道 chat1 的订阅者数量。
unsubscribe chat1: 退订频道 chat1,即停止接收该频道的消息。
punsubscribe java.*: 退订所有以 java.* 开头的频道,即停止接收所有符合该模式的频道的消息。
psubscribe java.*: 订阅所有以 java.* 开头的频道,即开始接收所有符合该模式的频道的消息。
  • redis事务:
  1. 开启事务:使用 MULTI 命令开启事务。一旦事务开始,后续的命令不会立即执行,而是被缓存起来等待执行。
  2. 执行命令:在事务中,可以像平时一样执行 Redis 命令。这些命令不会立即执行,而是被加入到事务队列中。
  3. 提交事务:使用 EXEC 命令提交事务。当执行 EXEC 命令时,Redis 会按照事务队列中的命令顺序依次执行。如果在执行期间没有发生错误,事务中的所有命令都会被执行,否则事务会被回滚。
MULTI                // 开启事务
SADD myset a b c     // 添加元素到集合 myset
SADD myset e f g
LPUSH mylist aa bb cc  // 左插入元素到列表 mylist
LPUSH mylist dd ff gg
EXEC                 // 提交事务
  • 服务器管理
dump.rdb:RDB 是 Redis 数据库快照的默认持久化方式。保存数据库当前状态的 RDB 文件名通常是 dump.rdb。
appendonly.aof:AOF 是另一种 Redis 持久化方式,用于记录服务器接收到的写命令。AOF 文件名通常是 appendonly.aof。
BgRewriteAof:异步执行 AOF 文件的重写,即在后台进行 AOF 文件的优化处理,以创建当前 AOF 文件体积的优化版本。
BgSave:后台异步保存数据到磁盘,生成一个 RDB 文件(通常是 dump.rdb)。这个过程不会阻塞主进程,允许服务器继续处理命令请求。
save:同步保存数据到磁盘,但这会阻塞主进程,导致其他客户端无法连接。
client kill:关闭指定客户端的连接。
client list:列出所有当前连接到服务器的客户端。
client setname:给客户端设置一个名称,可以方便地识别和追踪客户端。
config get port:获取 Redis 服务器监听的端口号配置。
configRewrite:对 Redis 的配置文件进行重写,使其包含最新的配置信息。
rdb save 900 1 save 300 10 save 60 10000:设置了三个条件,表示当在指定的秒数内有指定数量的改动时进行 RDB 快照的保存。例如,save 900 1 表示如果在 900 秒内至少有一个键被修改,则执行 RDB 快照保存。
appendonly yes:启用 AOF 持久化。
appendfsync everysec:每秒将 AOF 写入磁盘一次,这是一种折中方案,既保证了数据的安全性,又避免了频繁的磁盘写入。

命令:

  1. bgsave:该命令用于在后台异步保存数据到磁盘,生成一个 RDB 文件(快照保存)。它不会阻塞服务器的主线程,允许服务器继续处理其他命令请求。
  2. lastsave:该命令返回上次成功将数据保存到磁盘的 UNIX 时间戳。这个时间戳通常与最近一次成功执行 bgsave 或自动执行快照保存时的时间相对应。
  3. shutdown:该命令用于优雅地关闭 Redis 服务器。在执行 shutdown 命令时,Redis 会首先尝试将数据保存到磁盘(执行 bgsave),然后再关闭服务器。这个过程是同步的,会阻塞主线程,直到数据成功保存到磁盘并且服务器关闭。
  4. bgrewriteaof:该命令用于在后台进行 AOF 文件的重写,以压缩和优化 AOF 文件。重写过程会生成一个新的 AOF 文件,包含了 Redis 服务器当前数据的最小副本。这个过程不会阻塞服务器的主线程,允许服务器继续处理其他命令请求。

1.3 数据结构

1、Redis 有哪些数据结构?

  • 字符串 String
  • 字典 Hash
  • 列表 List
  • 集合 Set
  • 有序集合 Zset

2、String 类型的底层实现?

  • 为了将性能优化到极致,Redis 作者为每种数据结构提供了不同的实现方式,以适应特定应用场景。以最常用的 String 为例,
  • 其底层实现就可以分为 int、embstr 以及 raw 这三种类型。这些特定的底层实现在 Redis 中被称为编码(Encoding),可以通过 OBJECT ENCODING [key] 命令查看。
struct sdshdr{
     //记录buf数组中已使用字节的数量
     //等于 SDS 保存字符串的长度
     int len;
     //记录 buf 数组中未使用字节的数量
     int free;
     //字节数组,用于保存字符串
     char buf[];
}

3、说说 SDS 带来的好处?

  • 我们知道 Redis 是使用 C 语言写的,那么相比使用 C 语言中的字符串(即以空字符 \0 结尾的字符数组),自己实现一个 SDS 的好处是什么?

    1)常数复杂度获取字符串长度

    由于 len 属性的存在,我们获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)。

    2)杜绝缓冲区溢出

    3)减少修改字符串的内存重新分配次数

    4)二进制安全

    5)兼容部分 C 字符串函数

img

  • 一般来说,SDS 除了保存数据库中的字符串值以外,还可以作为缓冲区(Buffer):包括 AOF 模块中的 AOF 缓冲区以及客户端状态中的输入缓冲区。
  • ref Redis详解(四)------ redis的底层数据结构

4、Redis 实现的链表有哪些特性?

typedef struct list{
     //表头节点
     listNode *head;
     //表尾节点
     listNode *tail;
     //链表所包含的节点数量
     unsigned long len;
     //节点值复制函数
     void (*free) (void *ptr);
     //节点值释放函数
     void (*free) (void *ptr);
     //节点值对比函数
     int (*match) (void *ptr,void *key);
}list;

typedef  struct listNode{
       //前置节点
       struct listNode *prev;
       //后置节点
       struct listNode *next;
       //节点的值
       void *value;  
}listNode

​ 1)双端:链表具有前置节点和后置节点的引用,获取这两个节点时间复杂度都为 O(1)。

​ 2)无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问都是以 NULL 结束。

​ 3)带长度计数器:通过 len 属性获取链表长度的时间复杂度为 O(1)。

​ 4)多态:链表节点使用指针来保存节点值,可以保存各种不同类型的值。

5、Redis 是如何实现字典的?

字典又称为符号表或者关联数组、或映射(Map),是一种用于保存键值对的抽象数据结构。

typedef struct dictht{
     //哈希表数组
     dictEntry **table;
     //哈希表大小
     unsigned long size;
     //哈希表大小掩码,用于计算索引值
     //总是等于 size-1
     unsigned long sizemask;
     //该哈希表已有节点的数量
     unsigned long used;
}dictht
  • 哈希算法

Redis计算哈希值和索引值方法如下:

# 1、使用字典设置的哈希函数,计算键 key 的哈希值
hash = dict->type->hashFunction(key);
# 2、使用哈希表的sizemask属性和第一步得到的哈希值,计算索引值
# 根据情况不同, ht[x] 可以是 ht[0] 或者 ht[1]
index = hash & dict->ht[x].sizemask;

当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis 使用 MurmurHash2 算法来计算键的哈希值。这种算法的优点在于,即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度也非常快。

ref哈希算法

  • 扩容和收缩

    当哈希表保存的键值对太多或者太少时,就要通过 rerehash(重新散列)来对哈希表进行相应的扩展或者收缩。具体步骤如下:

    1)如果执行扩展操作,会基于原哈希表创建一个大小等于 ht[0].used * 2n 的哈希表(也就是每次扩展都是根据原哈希表已使用的空间扩大一倍创建另一个哈希表)。相反如果执行的是收缩操作,每次收缩是根据已使用空间缩小一倍创建一个新的哈希表。

    2)重新利用上面的哈希算法,计算索引值,然后将键值对放到新的哈希表位置上。

    3)所有键值对都迁徙完毕后,释放原哈希表的内存空间。

  • 触发扩容条件

    1)服务器目前没有执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子等于 1。

    2)服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子等于 5。

    (其中 负载因子 = 哈希表已保存节点数量 / 哈希表大小。)

6、什么是渐近式 rehash?

​ 也就是说扩容和收缩操作不是一次性、集中式完成的,而是分多次、渐进式完成的。 如果保存在 Redis 中的键值对只有几个几十个,那么 rehash 操作可以瞬间完成,但是如果键值对有几百万,几千万甚至几亿,那么要一次性的进行 rehash,势必会造成 Redis 一段时间内不能进行别的操作。所以 Redis 采用渐进式 rehash。

​ 这样在进行渐进式 rehash 期间,字典的删除查找更新等操作可能会在两个哈希表上进行,第一个哈希表没有找到,就会去第二个哈希表上进行查找。但是进行增加操作,一定是在新的哈希表上进行的。

7、有序集合 Zset 的底层实现?

zset 是 Redis 中一个非常重要的数据结构,其底层是基于跳表(skip list) 实现的。

跳表是一种随机化的数据结构,基于并联的链表,实现简单,插入、删除、查找的复杂度均为 O(logN)。简单说来跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能,正是这个跳跃的功能,使得在查找元素时,跳表能够提供 O(logN) 的时间复杂度。

img

跳表为了避免每次插入或删除带来的额外操作,不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每个节点随机出一个层数(level)。而且新插入一个节点不会影响其它节点的层数。因此,插入操作只需要修改插入节点前后的指针,而不需要对很多节点都进行调整。

img

ref Redis内部数据结构详解(6)——skiplist

8、Zset 为何不使用红黑树等平衡树?

  • 1)跳跃表范围查询比平衡树操作简单。 因为平衡树在查询到最小值的时还需要采用中序遍历去查询最大值。 而跳表只需要在找到最小值后,对第一层的链表遍历即可。

    2)平衡树的删除和插入需要对子树进行相应的调整,而跳表只需要修改相邻的节点即可。

    3)跳表和平衡树的查询操作都是O(logN)的时间复杂度。

    4)从整体上来看,跳表算法实现的难度要低于平衡树。

ref redis中的Zset原理

9、什么是 Redis 的 intset?

整数集合(intset) 是 Redis 用于保存整数值的集合抽象数据类型,它可以保存类型为 int16_t、int32_t 或者 int64_t 的整数值,并且保证集合中不会出现重复元素。

typedef struct intset{
     //编码方式
     uint32_t encoding;
     //集合包含的元素数量
     uint32_t length;
     //保存元素的数组
     int8_t contents[];
}intset;

整数集合的每个元素都是 contents 数组的一个数据项,它们按照从小到大的顺序排列,并且不包含任何重复项。需要注意的是虽然 contents 数组声明为 int8_t 类型,但是实际上 contents 数组并不保存任何 int8_t 类型的值,其真正类型有 encoding 来决定。

  • 集合升级过程

  • 当我们新增的元素类型比原集合元素类型的长度要大时,需要对整数集合进行升级,才能将新元素放入整数集合中。具体步骤:

    1)根据新元素类型,扩展整数集合底层数组的大小,并为新元素分配空间。

    2)将底层数组现有的所有元素都转成与新元素相同类型的元素,并将转换后的元素放到正确的位置,放置过程中,维持整个元素顺序都是有序的。

    3)将新元素添加到整数集合中(保证有序)。

  • 集合是否降级

    整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态。

10、说说什么是压缩列表?

压缩列表(ziplist) 是 Redis 为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。

压缩列表并不是对数据利用某种算法进行压缩,而是将数据按照一定规则编码在一块连续的内存区域,目的是节省内存。

img

压缩列表的每个节点构成如下:

img

  • revious_entry_ength

    记录压缩列表前一个字节的长度。previous_entry_ength 的长度可能是 1 个字节或者是 5 个字节,如果上一个节点的长度小于 254,则该节点只需要一个字节就可以表示前一个节点的长度了,如果前一个节点的长度大于等于 254,则 previous_length 的第一个字节为254,后面用四个字节表示当前节点前一个节点的长度。利用此原理即当前节点位置减去上一个节点的长度即得到上一个节点的起始位置,压缩列表可以从尾部向头部遍历。这么做很有效地减少了内存的浪费。

  • encoding

    节点的 encoding 保存的是节点的 content 的内容类型以及长度,encoding 类型一共有两种,一种字节数组一种是整数,encoding 区域长度为 1 字节、2 字节或 5 字节。

  • content

    content 区域用于保存节点的内容,节点内容类型和长度由 encoding 决定。

11、什么是 RedisObject?

​ 我们知道,Redis 底层实现了很多高级数据结构,如简单动态字符串、双端链表、字典、压缩列表、跳跃表、整数集合等。然而 Redis 并没有直接使用这些数据结构来实现键值对的数据库,而是在这些数据结构之上又包装了一层 RedisObject(对象),也就是我们常说的五种数据结构:字符串对象、列表对象、哈希对象、集合对象和有序集合对象。

typedef struct redisObject {
    // 类型
    unsigned type:4;
    // 编码,即对象的底层实现数据结构
    unsigned encoding:4;
    // 对象最后一次被访问的时间
    unsigned lru:REDIS_LRU_BITS;
    // 引用计数
    int refcount;
    // 指向实际值的指针
    void *ptr;
} robj;
  • 这样做有两个好处:

    1)通过不同类型的对象,Redis 可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行该的命令。

    2)可以针对不同的使用场景,为对象设置不同的实现,从而优化内存或查询速度。

12、五种类型分别都有哪些实现?

  • 字符串(String)

  • 字符串对象的 encoding 有三种,分别是:int、raw、embstr。

    1)如果一个字符串对象保存的是整数值,并且这个整数值可以用 long 类型标识,那么字符串对象会讲整数值保存在 ptr 属性中,并将 encoding 设置为 int。比如 set number 10086 命令。

    2)如果字符串对象保存的是一个字符串值,并且这个字符串的长度大于 44 字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为 raw。

    3)如果字符串对象保存的是一个字符串值,并且这个字符串的长度小于等于 44 字节,那么字符串对象将使用 embstr 编码的方式来保存这个字符串。

ref为何是 44 个字节

embstr 的编码方式的一些优点:

  • embstr 存储形式是这样一种存储形式,它将 RedisObject 对象头和 SDS 对象连续存在一起,使用 malloc 方法一次分配。embstr 最小占用空间为 19(16+3),而 64-19-1(结尾的\0)=44,所以 embstr 只能容纳 44 字节。

    1)embstr 编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次。

    2)释放 embstr 编码的字符串对象只需要调用一次内存释放函数,而释放 raw 编码的字符串对象需要调用两次内存释放函数。

    3)因为 embstr 编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起 raw ,编码的字符串对象能够更好地利用缓存带来的优势。

  • 哈希对象(hash)

    哈希对象的编码有 ziplist 和 hashtable 两种。当哈希对象保存的键值对数量小于 512,并且所有键值对的长度都小于 64 字节时,使用压缩列表存储;否则使用 hashtable 存储。

  • 列表对象(list)

    列表对象的编码有 ziplist 和 linkedlist 两种。当列表的长度小于 512,并且所有元素的长度都小于 64 字节时,使用压缩列表存储,否则使用 linkedlist 存储。

  • 集合对象(set)

    列表对象的编码有 intset 和 hashtable 两种。当集合的长度小于 512,并且所有元素都是整数时,使用整数集合存储;否则使用 hashtable 存储。

  • 有序集合对象(sort set)

    有序集合对象的编码有 ziplist 和 skiplist 两种。当有序集合的长度小于 128,并且所有元素的长度都小于 64 字节时,使用压缩列表存储;否则使用 skiplist 存储。

    intset(整数集合)和 ziplist(压缩列表)主要是为节省内存而设计的内存结构,它的优点就是节省内存,但缺点就是比其他结构要消耗更多的时间,所以 Redis 在数据量小的时候使用整数集合存储。 refRedis 学习笔记(篇五):对象(RedisObject)

1.4 数据存储

1、Redis 的数据过期策略是什么?

  • 在回答词问题之前,首先需要回答另一个问题,就是如何设置 Redis 中数据的过期时间?

    1)expire key time (以秒为单位)–这是最常用的方式 2)setex(String key, int seconds, String value) --字符串独有的方式

除了字符串自己独有设置过期时间的方法外,其他方法都需要依靠 expire 方法来设置时间,如果没有设置时间,那缓存就是永不过期。 如果设置了过期时间,使用 persist key 让缓存永不过期。

  • 常见的过期策略

    1)定时删除

    在设置 key 的过期时间的同时,为该 key 创建一个定时器,让定时器在 key 的过期时间来临时,对 key 进行删除。

    2)惰性删除

    3)定期删除

    每隔一段时间执行一次删除(在 redis.conf 配置文件设置,1s 刷新的频率)过期 key 操作。

  • Redis采用的过期策略

    Redis 采用了惰性删除+定期删除的方式处理过期数据。

  • 惰性删除的流程

    1)在进行get或setnx等操作时,先检查key是否过期;

    2)若过期,删除key,然后执行相应操作;

    3)若没过期,直接执行相应操作。

  • 定期删除的流程

    其核心是对指定个数个库的每一个库随机删除小于等于指定个数个过期 key:

    1)历每个数据库(就是 redis.conf 中配置的 “database” 数量,默认为16);

    2)检查当前库中的指定个数个 key (默认是每个库检查 20 个,相当于该循环执行 20 次):

    2.1)如果当前库中没有一个 key 设置了过期时间,直接执行下一个库的遍历;

    2.2)随机获取一个设置了过期时间的 key,检查是否过期,如果过期则删除;

    2.3)判断定期删除操作是否已经达到指定时长,若已经达到,直接退出定期删除。

ref Redis学习笔记–Redis数据过期策略详解

2、持久化文件对过期策略的处理?

过期 key 是不会写入 RDB 和 AOF 文件,同时数据恢复时也会做过期验证。

3、Redis 有哪些内存淘汰机制?

Redis 作为一个内存数据库,在内存空间不足的时候,为了保证命中率,就会和我们操作系统中的页面置换算法类似,选择一定的数据淘汰策略。

  • volatile(设置过期时间的数据集)

    1)volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。

    2)volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。

    3)volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。

    4)volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。

  • allkeys(所以数据集)

    5)allkeys-lru:从数据集中挑选最近最少使用的数据淘汰

    6)allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰。

    7)allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰

  • no-enviction

    8)no-enviction(驱逐):禁止驱逐数据,这也是默认策略。

    意思是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用 no-enviction 策略可以保证数据不被丢失。

PS:在 redis.config 文件中,我们可以设置 maxmemory 的值来配置 Redis 的最大占用内存字节数。

ref Redis5.0数据淘汰策略详解

4、Redis 有哪些持久化机制?

RDB 和 AOF。

5、说说 Redis 的 RDB?

​ RDB 持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式。也就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为 dump.rdb。

​ RDB 支持 同步(save 命令)、后台异步(bgsave)以及自动配置三种方式触发。

img

优点

  • RDB 文件紧凑,全量备份,非常适合用于进行备份和灾难恢复
  • 生成 RDB 文件时支持异步处理,主进程不需要进行任何磁盘IO操作
  • RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快

缺点

  • RDB 快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。且在快照持久化期间修改的数据不会被保存,可能丢失数据。

6、说说 Redis 的 AOF?

​ 全量备份总是耗时的,有时候我们提供一种更加高效的方式 AOF,其工作机制更加简单:会将每一个收到的写命令追加到文件中。

​ img

​ 随着时间推移,AOF 持久化文件也会变的越来越大。为了解决此问题,Redis 提供了 bgrewriteaof 命令,作用是 fork 出一条新进程将内存中的数据以命令的方式保存到临时文件中,完成对AOF 文件的重写。

AOF 也有三种触发方式:1)每修改同步 always 2)每秒同步 everysec 3)不同no:从不同步。

优点

  • AOF 可以更好的保护数据不丢失,一般 AOF 隔 1 秒通过一个后台线程执行一次 fsync 操作
  • AOF 日志文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损
  • AOF 日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写
  • AOF 日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复

缺点

  • 对于同一份数据来说,AOF 日志文件通常比 RDB 数据快照文件更大
  • AOF开启后,支持的写 QPS 会比RDB支持的写 QPS 低,因为 AOF 一般会配置成每秒 fsync 一次日志文件,当然,每秒一次 fsync,性能也还是很高的

ref 详解Redis中两种持久化机制RDB和AOF

7、RDB 和 AOF 该如何选择?

​ 通过前面章节我们已经明白了两种机制的原理,接下来就该结合具体需求进行机制的选择,通常情况我们是二者结合使用的。

命令RDBAOF
启动优先级低高
体积小大
恢复速度快慢
数据安全性丢数据取决于刷盘策略
轻重重轻

1.5 Pipeline

1、什么是 Redis 的 Pipeline?

​ 在出现 Pipeline 之前,我们梳理一下 Redis 客户端执行一条命令需要经过哪些步骤: 发送命令-〉命令排队-〉命令执行-〉返回结果。

​ 这个过程称为 Round trip time(简称RTT, 往返时间),mget 和 mset 有效节约了 RTT,但大部分命令(如 hgetall 并没有 mhgetall)不支持批量操作,需要消耗 N 次 RTT ,这个时候需要 pipeline 来解决这个问题。

img

ref Redis系列十:Pipeline详解

2、原生批命令 (mset, mget) 与 Pipeline 区别?

​ 1)原生批命令是原子性的,而 pipeline 是非原子操作。

​ 2)原生批命令一命令多个 key, 但 pipeline 支持多命令(存在事务),非原子性。

​ 3)原生批命令是服务端实现,而 pipeline 需要服务端与客户端共同完成。

1.6 多线程

1、Redis 6 之前真的是单线程吗?

Redis 在处理客户端的请求时,包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的单线程。但如果严格来讲从 Redis 4 之后并不是单线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 key 的删除等等。

2、Redis 6 之前为什么使用单线程?

​ 使用了单线程后,可维护性高。多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。

​ 同时 Redis 通过 AE 事件模型以及 IO 多路复用等技术,即使单线程处理性能也非常高,因此没有必要使用多线程。单线程机制使得 Redis 内部实现的复杂度大大降低,Hash 的惰性 Rehash、Lpush 等等 “线程不安全” 的命令都可以无锁进行。

3、Redis 6 为何引入多线程?

  • 随着目前行业内越来越复杂的业务场景,有些公司动不动就上亿的交易量,因此需要更大的 QPS。常见的解决方案是在分布式架构中对数据进行分区并采用多个服务器,但该方案有非常大的缺点,比如:

    1)要管理的 Redis 服务器太多,维护代价大;

    2)某些适用于单个 Redis 服务器的命令不适用于数据分区;

    3)数据分区无法解决热点读/写问题;

    4)数据偏斜,重新分配和放大/缩小变得更加复杂等等。

  • 从 Redis 自身角度来说,因为读写网络的 read/write 系统调用占用了 Redis 执行期间大部分 CPU 时间,瓶颈主要在于网络的 IO 消耗, 优化主要有两个方向:

    1)提高网络 IO 性能,典型的实现比如使用 DPDK 来替代内核网络栈的方式;

    2)使用多线程充分利用多核,典型的实现比如 Memcached。

协议栈优化的这种方式跟 Redis 关系不大,支持多线程是一种最有效最便捷的操作方式。所以总结起来,Redis 支持多线程主要就是两个原因:

  • 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核;
  • 多线程任务可以分摊 Redis 同步 IO 读写负荷。

4、多线程如何开启以及配置?

Redis 6 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis.conf 配置文件中的 io-threads-do-reads yes。

开启多线程后,还需要设置线程数,否则是不生效的。同样修改 redis.conf 文件中的 io-threads [n] 配置。

关于线程数的设置,官方有一个建议:4 核的机器建议设置为 2 或 3 个线程,8 核的建议设置为 6 个线程,线程数一定要小于机器核数。还需要注意的是,线程数并不是越大越好,官方认为超过了 8 个基本就没什么意义了。

5、Redis 多线程的实现机制?

img

  • 大致流程如下:

    1)主线程负责接收建立连接请求,获取 socket 放入全局等待读处理队列;

    2)主线程处理完读事件之后,通过 RR(Round Robin) 将这些连接分配给这些 IO 线程;

    3)主线程阻塞等待 IO 线程读取 socket 完毕;

    4)主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行;

    5)主线程阻塞等待 IO 线程将数据回写 socket 完毕;

    6)解除绑定,清空等待队列。

  • 该设计的特点:

    1)IO 线程要么同时在读 socket,要么同时在写,不会同时读或写。

    2)IO 线程只负责读写 socket 解析命令,不负责命令处理。

6、多线程是否会导致线程安全问题?

​ 从上面的实现机制可以看出,Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。所以我们不需要去考虑控制 key、lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。

7、Redis 和 Memcached 多线程区别?

​ 相同点:都采用了 master 线程 - worker 线程的模型。

​ 不同点:Memcached 执行主逻辑也是在 worker 线程里,模型更加简单,实现了真正的线程隔离,符合我们对线程隔离的常规理解。而 Redis 把处理逻辑交还给 master 线程,虽然一定程度上增加了模型复杂度,但也解决了线程并发安全等问题。

ref Redis 6.0 新特性-多线程连环13问!

2 集群

2.1 集群模式

1、Redis 集群搭建有几种模式?

  • 主从模式

    和 MySQL 需要主从复制的原因一样,Redis 虽然读写速度非常快,但是也会产生性能瓶颈,特别是在读压力上,为了分担压力,Redis 支持主从复制。Redis 的主从结构一主一从,一主多从或级联结构,复制类型可以根据是否是全量而分为全量同步和增量同步。 img

  • 哨兵模式

    在主从复制实现之后,如果想对 master 进行监控,Redis 提供了一种哨兵机制,哨兵的含义就是监控 Redis 系统的运行状态,通过投票机制,从 slave 中选举出新的 master 以保证集群正常运行。

    还可以启用多个哨兵进行监控以保证集群足够稳健,这种情况下,哨兵不仅监控主从服务,哨兵之间也会相互监控。

  • Cluster 集群模式

img

ref Redis集群搭建的三种方式

2、Redis 主从复制的实现?

主从复制可以根据需要分为全量同步的增量同步两种方式。

  • 全量同步

    Redis 全量复制一般发生在 slave 的初始阶段,这时 slave 需要将 master 上的数据都复制一份,具体步骤如下:

img

1)slave 连接 master,发送 SYNC 命令;

2)master 接到 SYNC 命令后执行 BGSAVE 命令生产 RDB 文件,并使用缓冲区记录此后执行的所有写命令;

3)master 执行完 BGSAVE 后,向所有的 slave 发送快照文件,并在发送过程中继续记录执行的写命令;

4)slave 收到快照后,丢弃所有的旧数据,载入收到的数据;

5)master 快照发送完成后就会开始向 slave 发送缓冲区的写命令;

6)slave 完成对快照的载入,并开始接受命令请求,执行来自 master 缓冲区的写命令;

7)slave 完成上面的数据初始化后就可以开始接受用户的读请求了。

  • 增量同步

    增量复制实际上就是在 slave 初始化完成后开始正常工作时 master 发生写操作同步到 slave 的过程。增量复制的过程主要是 master 每执行一个写命令就会向 slave 发送相同的写命令,slave 接受并执行写命令,从而保持主从一致。

    ref Redis主从复制原理总结

3、Redis 的主从同步策略?

​ 主从同步刚连接的时候进行全量同步,全量同步结束后开始增量同步。

​ 如果有需要,slave 在任何时候都可以发起全量同步,其主要策略就是无论如何首先会尝试进行增量同步,如果失败则会要求 slave 进行全量同步,之后再进行增量同步。

​ 注意:如果多个 slave 同时断线需要重启的时候,因为只要 slave 启动,就会和 master 建立连接发送SYNC请求和主机全量同步,如果多个同时发送 SYNC 请求,可能导致 master IO 突增而发送宕机。所以我们要避免多个 slave 同时恢复重启的情况。

4、哨兵模式的原理?

​ 哨兵主要用于管理多个 Redis 服务器,主要有以下三个任务:监控、提醒以及故障转移。

​ 每个哨兵会向其它哨兵、master、slave 定时发送消息,以确认对方是否还存活。如果发现对方在配置的指定时间内未回应,则暂时认为对方已挂。若“哨兵群”中的多数 sentinel 都报告某一 master 没响应,系统才认为该 master “彻底死亡”,通过一定的 vote 算法从剩下的 slave 节点中选一台提升为 master,然后自动修改相关配置。

5、哨兵模式故障迁移流程?

1)首先是从主服务器的从服务器中选出一个从服务器作为新的主服务器。

选点的依据依次是:

  • 网络连接正常 -> 5 秒内回复过 INFO 命令 -> 10*down-after-milliseconds 内与主连接过的 -> 从服务器优先级 -> 复制偏移量 -> 运行id较小的。

    2)选出之后通过 slaveif no ont 将该从服务器升为新主服务器;

    3)然后再通过 slaveof ip port 命令让其他从服务器复制该信主服务器。

缺点

  • 主从服务器的数据要经常进行主从复制,这样会造成性能下降
  • 当主服务器宕机后,从服务器切换成主服务器的那段时间,服务是不可用的

2.2 Cluster 集群

1、什么是一致性 Hash 以及解决什么问题?

​ 一致性 hash 其实是普通 hash 算法的改良版,其 hash 计算方法没有变化,但是 hash 空间发生了变化,由原来的线性的变成了环。

​ 缓存 key 通过 hash 计算之后得到在 hash 环中的位置,然后顺时针方向找到第一个节点,这个节点就是存放 key 的节点。

img

​ 由此可见,一致性 hash 主要是为了解决普通 hash 中扩容和宕机的问题。

​ 同时还可以通过虚拟节点来解决数据倾斜的问题:就是在节点稀疏的 hash 环上对物理节点虚拟出一部分虚拟节点,key 会打到虚拟节点上面,而虚拟节点上的 key 实际也是映射到物理节点上的,这样就避免了数据倾斜导致单节点压力过大导致节点雪崩的问题。

img

  • ref 什么是一致性hash?*

2、Cluster 模式的原理?

​ 其实现原理就是哈希槽 Hash。Redis Cluster 中有一个 16384 长度的槽的概念,他们的编号为 0、1、2、3 …… 16382、16383。这个槽是一个虚拟的槽,并不是真正存在的。正常工作的时候,Redis Cluster 中的每个 Master 节点都会负责一部分的槽,当有某个 key 被映射到某个 Master 负责的槽,那么这个 Master 负责为这个 key 提供服务。

​ 至于哪个 Master 节点负责哪个槽,这是可以由用户指定的,也可以在初始化的时候自动生成(redis-trib.rb脚本)。这里值得一提的是,在 Redis Cluster 中,只有 Master 才拥有槽的所有权,如果是某个 Master 的 slave,这个slave只负责槽的使用,但是没有所有权。

3、Cluster 的分片机制?

​ 为了使得集群能够水平扩展,首要解决的问题就是如何将整个数据集按照一定的规则分配到多个节点上。对于客户端请求的 key,根据公式 HASH_SLOT=CRC16(key) mod 16384,计算出映射到哪个分片上。而对于 CRC16 算法产生的 hash 值会有 16bit,可以产生 2^16-=65536 个值。8

​ Redis 集群提供了灵活的节点扩容和收缩方案。在不影响集群对外服务的情况下,可以为集群添加节点进行扩容也可以下线部分节点进行缩容。可以说,槽是 Redis 集群管理数据的基本单位,集群伸缩就是槽和数据在节点之间的移动。

4、Cluster 集群的扩容流程?

  • 当一个 Redis 新节点运行并加入现有集群后,我们需要为其迁移槽和数据。首先要为新节点指定槽的迁移计划,确保迁移后每个节点负责相似数量的槽,从而保证这些节点的数据均匀。

    1)首先启动一个 Redis 节点,记为 M4。

    2)使用 cluster meet 命令,让新 Redis 节点加入到集群中。新节点刚开始都是主节点状态,由于没有负责的槽,所以不能接受任何读写操作,后续给他迁移槽和填充数据。

    3)对 M4 节点发送 cluster setslot { slot } importing { sourceNodeId } 命令,让目标节点准备导入槽的数据。

    4)对源节点,也就是 M1,M2,M3 节点发送 cluster setslot { slot } migrating { targetNodeId } 命令,让源节点准备迁出槽的数据。

    5)源节点执行 cluster getkeysinslot { slot } { count } 命令,获取 count 个属于槽 { slot } 的键,然后执行步骤 6)的操作进行迁移键值数据。

    6)在源节点上执行 migrate { targetNodeIp} " " 0 { timeout } keys { key... } 命令,把获取的键通过 pipeline 机制批量迁移到目标节点,批量迁移版本的 migrate 命令在 Redis 3.0.6 以上版本提供。

    7)重复执行步骤 5)和步骤 6)直到槽下所有的键值数据迁移到目标节点。

    8)向集群内所有主节点发送 cluster setslot { slot } node { targetNodeId } 命令,通知槽分配给目标节点。为了保证槽节点映射变更及时传播,需要遍历发送给所有主节点更新被迁移的槽执行新节点。

5、Cluster 集群收缩流程?

  • 收缩节点就是将 Redis 节点下线,整个流程需要如下操作流程。

    1)首先需要确认下线节点是否有负责的槽,如果是,需要把槽迁移到其他节点,保证节点下线后整个集群槽节点映射的完整性。

    2)当下线节点不再负责槽或者本身是从节点时,就可以通知集群内其他节点忘记下线节点,当所有的节点忘记改节点后可以正常关闭。

    ref Redis Cluster数据分片机制

6、客户端如何路由?

​ 既然 Redis 集群中的数据是分片存储的,那我们该如何知道某个 key 存在哪个节点上呢?即我们需要一个查询路由,该路由根据给定的 key,返回存储该键值的机器地址。

​ 常规的实现方式便是采用如下图所示的代理方案,即采用一个中央节点(比如HDFS中的NameNode)来管理所有的元数据,但是这样的方案带来的最大问题就是代理节点很容易成为访问的瓶颈,当读写并发量高的时候,代理节点会严重的拖慢整个系统的性能。

img

​ Redis 并没有选择使用代理,而是客户端直接连接每个节点。Redis 的每个节点中都存储着整个集群的状态,集群状态中一个重要的信息就是每个桶的负责节点。在具体的实现中,Redis 用一个大小固定为 CLUSTER_SLOTS 的 clusterNode 数组 slots 来保存每个桶的负责节点。

typedef struct clusterNode {
    ...
    unsigned char slots[CLUSTER_SLOTS/8];
    ...
} clusterNode;

typedef struct clusterState {
    // slots记录每个桶被哪个节点存储
    clusterNode *slots[CLUSTER_SLOTS];
    ...
} clusterState;

​ 在集群模式下,Redis 接收任何键相关命令时首先计算键对应的桶编号,再根据桶找出所对应的节点,如果节点是自身,则处理键命令;否则回复 MOVED 重定向错误,通知客户端请求正确的节点,这个过程称为 MOVED 重定向。重定向信息包含了键所对应的桶以及负责该桶的节点地址,根据这些信息客户端就可以向正确的节点发起请求。

ref Redis集群详解(上)

7、为什么是 16384 个槽位?

从前面的 Cluster 集群原理我们已经了解到集群中的所有节点在握手成功后悔定期发送 ping/pong 消息,交换数据信息。先来了解一下消息体传递了哪些数据:

typedef struct {
    //消息的长度(包括这个消息头的长度和消息正文的长度)
    uint32_t totlen;
    //消息的类型
    uint16_t type;
    //消息正文包含的节点信息数量
    //只在发送MEET 、PING 、PONG 这三种Gossip 协议消息时使用
    uint16_t count;
    //发送者所处的配置纪元
    uint64_t currentEpoch;
    //如果发送者是一个主节点,那么这里记录的是发送者的配置纪元
    //如果发送者是一个从节点,那么这里记录的是发送者正在复制的主节点的配置纪元
    uint64_t configEpoch;
    //发送者的名字(ID )
    char sender[REDIS_CLUSTER_NAMELEN];
    //发送者目前的槽指派信息
    unsigned char myslots[REDIS_CLUSTER_SLOTS/8];
    //如果发送者是一个从节点,那么这里记录的是发送者正在复制的主节点的名字
    //如果发送者是一个主节点,那么这里记录的是REDIS_NODE_NULL_NAME
    //(一个40 字节长,值全为0 的字节数组)
    char slaveof[REDIS_CLUSTER_NAMELEN];
    //发送者的端口号
    uint16_t port;
    //发送者的标识值
    uint16_t flags;
    //发送者所处集群的状态
    unsigned char state;
    //消息的正文(或者说,内容)
    union clusterMsgData data; 
} clusterMsg; 

上图展示的消息体结构无外乎是一些节点标识,IP,端口号,发送时间等,但需要注意一下标红的 myslots 的 char 数组,长度为 16383/8,这其实是一个 bitmap,每一个位代表一个槽,如果该位为1,表示这个槽是属于这个节点的。

  • 消息体大小上的考量

    至于这个消息体有多大?显然最占空间的就是 myslots 数组:**16384÷8÷1024=2kb。**如果槽位达到 65536,则所占空间提升到 65536÷8÷1024=8kb,极大浪费带宽。

  • Redis 集群主节点数量基本不可能超过 1000 个

    集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。

  • 槽位越小,节点少的情况下,压缩比高

    ref 为什么Redis集群有16384个槽ref 集群之间的消息

8、集群的故障发现与迁移?

故障发现

​ 当集群内某个节点出现问题时,需要通过一种健壮的方式保证识别出节点是否发生了故障。Redis 集群内节点通过 ping/pong 消息实现节点通信,消息不但可以传播节点槽信息,还可以传播其他状态如:主从状态、节点故障等。因此故障发现也是通过消息传播机制实现的。 主要环节包括:

  • 主观下线(PFAIL-Possibly Fail)

    集群中每个节点都会定期向其他节点发送ping消息,接收节点回复pong消息作为响应。如果在cluster-node-timeout时间内通信一直失败,则发送节点会认为接收节点存在故障,把接收节点标记为主观下线(PFail)状态。

  • 客观下线(Fail)

    Redis 集群对于节点最终是否故障判断非常严谨,只有一个节点认为主观下线并不能准确判断是否故障。当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播,通过Gossip消息传播,集群内节点不断收集到故障节点的下线报告。当半数以上持有槽的主节点都标记某个节点是主观下线时。触发客观下线流程。

故障恢复

​ 故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它,从而保证集群的高可用。下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节点进入客观下线时,将会触发故障恢复流程。

  • ref 018.Redis Cluster故障转移原理*

9、Codis、Twemproxy 以及 Cluster 方案对比?

https://cloud.tencent.com/developer/article/1701574

3 业务场景

1、如何设置 Key 当天失效?

  • expireAt
  • key 值带上时间戳

2、如何实现分布式锁?

  • 「互斥性」: 任意时刻,只有一个客户端能持有锁
  • 「锁超时释放」:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁
  • 「可重入性」:一个线程如果获取了锁之后,可以再次对其请求加锁
  • 「高性能和高可用」:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效
  • 「安全性」:锁只能被持有的客户端删除,不能被其他客户端删除

Redison 底层原理:

img

ref Redis实现分布式锁的7种方案,及正确使用姿势!

  • RedLock 加锁步骤:

    1)按顺序向集群中所有 master 节点请求加锁;

    2)根据设置的超时时间来判断,是不是要跳过该 master 节点;

    3)如果大于等于半数节点( N/2+1 )加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦;

    4)如果获取锁失败,解锁!

4、如何实现排行榜功能?

​ 活动运营中经常会有这样的需求:1)Value 值排序 2)Value 相同按时间排序。

  • 使用 MySQL:
EXPLAIN SELECT
    *
FROM
    (
        SELECT
            @rank := @rank + 1 AS rank,
            s.uid AS uid,
            s.coin AS coin
        FROM
            `user` s,
            (SELECT @rank := 0) r
        ORDER BY
            coin DESC,
            create_time
    ) q
WHERE
    q.uid = 'xxx';
  • 使用 Redis:
# 插入或者更新数据
Long zadd(final String key, final double score, final String member)
key : 排行榜的名字
memeber : 用户
score : 用户的分数
# 获取用户分数
Double zscore(String key, final String member)
# 获取用户的排名
Long zrevrank(final String key, final String member):(score从大到小,从0开始,所以需要加1)
Long zrank(final String key, final String member):(score从小到大,从0开始,所以需要加1)
# 获取某个范围内的用户排名
Set<Tuple> zrevrangeWithScoresBytes(String key, final long start, final long end) (从大到小)
Set<Tuple> zrangeWithScoresBytes(String key, final long start, final long end) (从小到大)
start : 开始排名
end : 结束排名
Tuple :

同时为了实现先到先得,zset 中的 score 并不能仅仅是排序的 Value 值,还需加入时间戳因子:score = (value * 10000000000(十次方)) + (100000000000(十一次方) - ts)。

由于时间戳相同一个月的时间内,头三位是相等的,即我们还可以进一步压缩时间位数以提高排序值的精度。

ref redis zset做排行榜

4 优化

1、为什么要优化大 key

img

ref redis大key优化

2、如何优化热点 key?

热 key 带来的问题:请求到的分片过于集中,超过单台 Server 的性能极限。

解决方案:

​ 1)服务端缓存:即将热点数据缓存至服务端的内存中;

​ 2)备份热点Key:即将热点Key+随机数,随机分配至 Redis 其它节点中。

最近更新:: 2025/8/22 15:05
Contributors: yanpeng_
Prev
Mybatis面试
Next
java集合面试题