8.1 Redis基础

8.1.1 Redis与Memorycache的区别?

  • Redis使用单线程,而Memcached是多线程
  • Redis使用现场申请内存的方式来存储数据,并且可以配置虚拟内存;Memcached使用预分配的内存池的方式。
  • Redis实现了持久化和主从同步,容灾性会更强。而Memcached只是存放在内存中,服务器故障关机后数据就会消失
  • Redis支持九种数据类型,而Memcached只是简单的key与value
  • Redis的优点:对数据高并发读写、对海量数据的高效率存储和访问、对数据的可扩展性和高可用性
  • Redis的应用场景:取最新N个数据的操作、排行榜应用取TOP N操作、需要精准设定过期时间的应用、计数器应用、获取某段时间所有数据排重值的唯一性操作、实时消息系统、构建队列系统、作缓存。
  • Redis在项目中的使用:表/接口缓存、分布式锁、id生成器

8.1.2 Redis的基本数据结构?

  • Redis常见的五种数据结构
数据结构 编码类型(64字节/512个) 常用命令 使用场景 限制
string int、embstr(39)、raw set/setnx name value 记录粉丝数,自增自减操作的原子计数器 String类型的value值最大支持512M,建议小于1M
list ziplist(64、512)、list lpush、rpop、llen 粉丝列表、评论列表,lrange实现高性能分页 hash、list、set、zset 元素个数不要超过 5000
hash ziplist(64、512)、hashtable hset、hlen、hget 保存用户信息 -
set intset(512)、hashtable sadd、scard、smembers、srem 交集、并集、差集的操作(获取共同好友) -
zset ziplist(64、128)、skiplist zadd、zcard、zrangebyscore 排行榜 -
  • ZAdd/ZRem 时间复杂度是O(log(N)),ZRangeByScore/ZRemRangeByScore 时间复杂度是O(log(N)+M),N是Set大小,M是结果/操作元素的个数。

  • 四种高级数据结构

    • bitmap:(BloomFilter)本质上是String数据结构,只不过操作的粒度变成了位。因为String类型最大长度为512MB,所以bitmap最多可以存储2^32个bit。
    • GEO:用来存储地理坐标,但坐标有限制。Geo本身不是一种数据结构,它本质上还是借助于ZSET,并且使用GeoHash技术进行填充。
    • HyperLogLog:解决大数据应用中的非精确计数操作,它可以接受多个元素作为输入,并给出输入元素的基数估算值,基数指的是集合中不同元素的数量。优点:占用空间小
    • Streams:以更抽象的方式建模日志的数据结构,底层的数据结构是radix tree。Consumer Groups:允许一组客户端协调消费相同的信息流
  • redisObject包含type、encoding、refcount、lru(最后一次被访问时间) 、*ptr(指向实际值的指针)

  • embstr编码创建字符串对象只需内存分配一次,调用一次内存释放函数,而raw都需要两次。

  • 可使用INCR和INCRBY生成分布式系统唯一序列号ID

  • ziplist:是一个逻辑上的双向链表,可以快速找到头节点和尾节点,然后每个节点(entry)中也包含指向前/后节点的"指针",设计是为了节省内存。保存字符串,数值两种类型,列表内部实现主要是对一块连续内存进行管理,列表支持列表头尾的插入或弹出结点操作。

  • 双向链表便于在表的两端操作,但是它的内存地址不连续,容易产生内存碎片。ziplist是一整块连续内存,存储效率很高。但它每次数据变动都会引发一次内存的realloc。所以quicklist结合了双向链表和ziplist的优点,是一个双向无环链表,它的每一个节点都是一个ziplist。

  • 简单动态字符串(SDS)的特点:O(1)获取字符串长度、杜绝缓冲区溢出(边界检查与自动扩容)、减少修改字符串时内存重分配的次数(空间预分配与惰性空间释放)、二进制安全(buf数组中是否包含“\0”不影响判断结束,也可存储任意二进制数据)

  • SDS的扩容机制:SDS的struct(len、free、char buf[]),先根据已使用长度和添加字符串长度计算出newlength,若newlength<SDS_MAX_PREALLOC则扩容2倍,否则扩容为newlength+SDS_MAX_PREALLOC(1M)。空间预分配策略:若len<1M,则free=len,否则free=1M。

  • skipList:一种有序的数据结构,通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。由双向链表+多级索引实现,查找、插入、删除的时间复杂度都是O(log n),空间复杂度是O(n),相对于红黑树来说,跳表能更快找到区间内的元素,红黑树实现更为复杂,给定level值占用内存比树少。插入节点时,level随机生成(maxlevel默认是32),p=0.25(经过递推计算O(log n)),即level变大的概率是25%,做到了层数越高,节点越少。

8.1.3 渐进式rehash过程?

  • rehash的步骤

  • 为字典的ht[1]哈希表分配空间

    • 若是扩展操作,那么ht[1]的大小为>=ht[0].used*2的2^n
    • 若是收缩操作,那么ht[1]的大小为>=ht[0].used的2^n
  • 将保存在ht[0]中的所有键值对rehash到ht[1]中,rehash指重新计算键的哈希值和索引值,新hash通过hashFunction(key)函数获取,新的索引值:index=hash&sizemask,其中sizemask=size-1.然后将键值对放置到ht[1]哈希表的指定位置上。

  • 当ht[0]的所有键值对都迁移到了ht[1]之后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],新建空白的哈希表ht[1],以备下次rehash使用。

  • 扩展与收缩的条件(负载因子=ht[0].used/ht[0].size)

    • 服务器目前没有执行bgsave或bgrewriteaof命令,并且哈希表的负载因子>=1或正在执行负载因子>=5就会进行rehash操作
    • 当负载因子的值小于0.1时,程序就会对哈希表进行收缩操作
  • 渐进式rehash:若哈希表中保存着数量巨大的键值对,一次进行rehash,很有可能会导致服务器宕机。所以要分多次、渐进式的完成。采取分为而治的方式,将rehash键值对的计算均摊到每个字典增删改查操作,避免了集中式rehash的庞大计算量。

  • 主要是维持索引计数器变量rehashidx,每次对字典执行增删改查时将rehashidx值+1,当ht[0]的所有键值对都被rehash到ht[1]中,程序将rehashidx的值设置为-1,表示rehash操作完成。

  • Redis的哈希表使用链地址法解决哈希冲突。

8.1.4 rehash源码?

  • Redis为了兼顾性能的考虑,分为lazy rehashing:在每次对dict进行操作的时候执行一个slot的rehash。active rehashing:每100ms里面使用1ms时间进行rehash。(serverCron函数),而字典有安全迭代器的情况下不能进行 rehash
  • 字典hash(lazy rehashing)函数调用:_dictRehashStep–> dictRehash
    • 在_dictRehashStep函数中,会调用dictRehash方法,而_dictRehashStep每次仅会rehash一个值从ht[0]到 ht[1],但由于_dictRehashStep是被dictGetRandomKey、dictFind、 dictGenericDelete、dictAdd调用的,因此在每次dict增删查改时都会被调用,这无疑就加快了rehash过程。
    • 在dictRehash函数中每次增量rehash n个元素,由于在自动调整大小时已设置好了ht[1]的大小,因此rehash的主要过程就是遍历ht[0],取得key,然后将该key按ht[1]的 桶的大小重新rehash,并在rehash完后将ht[0]指向ht[1],然后将ht[1]清空。在这个过程中rehashidx非常重要,它表示上次rehash时在ht[0]的下标位置。
  • 一般情况服务器在对数据库执行读取/写入命令时会对数据库进行渐进式 rehash ,但如果服务器长期没有执行命令的话,数据库字典的 rehash 就可能一直没办法完成,为了防止出现这种情况,我们需要对数据库执行主动 rehash 。
  • active rehashing函数调用的过程如下:
    serverCron->databasesCron–>incrementallyRehash->dictRehashMilliseconds->dictRehash,其中incrementallyRehash的时间较长,rehash的个数也比较多。这里每次执行 1 millisecond rehash 操作;如果未完成 rehash,会在下一个 loop 里面继续执行。

8.1.5 事务与事件

  • 事务的错误处理:语法错误(入队错误)不会执行,而运行错误(执行错误)其它命令仍然执行。multi会开启事务,exec命令会取消对所有键的监控,还可以用unwatch命令来取消监控,而取消事务的命令为 Discard

  • watch命令可以监控一个或者多个键,一旦有一个键被修改或删除,之后的事务就不会执行。其中exec / discard / unwatch命令会清除连接中的所有监视。通过watched_keys字典,可以知道哪些数据库键在被监视,若被监视的键被修改,则REDIS_DIRTY_CAS标识被打开,Redis中使用watch实现CAS算法。

  • Redis的线程模型:基于Reactor模式的文件事件处理器(单线程),它由多个socket(套接字)、I/O多路复用程序epoll、文件事件分派器、事件处理器(命令请求处理器、命令回复处理器、连接应答处理器等)组成。它采用IO多路复用机制同时监听多个socket,会将socket放入一个队列中排队,每次从队列中取出一个socket给文件事件分派器,文件事件分派器把socket给对应的事件处理器。

  • 时间事件分为定时事件和周期事件,serverCron函数,定期对自身的资源和状态进行检查。

  • 客户端的关闭,硬性限制:若输出缓冲区的大小超过了硬性限制所设置的大小,就立即关闭客户端,软性限制:若输出缓冲区的大小超过了软性缓冲区的大小,但没超过硬性限制,则会记录客户端到达软性限制的起始时间,若持续时间大于服务器设置的时长,则关闭客户端。

  • 伪客户端:创建Lua脚本的伪客户端:在服务器初始化时创建,一直持续到服务器关闭。载入AOF文件时使用的伪客户端:在载入时创建,载入完成后关闭。

  • 命令请求从发送到完成的步骤:客户端将命令请求发送给服务器、服务器读取命令请求,并分析命令参数、命令执行器根据参数查找命令的实现函数setCommand,执行实现函数得到回复

  • serverCron函数默认每隔100毫秒执行一次,它的功能如下:更新服务器时间缓存、更新LRU时钟、更新服务器每秒执行命令次数、更新服务器内存峰值记录、处理客户端资源、管理数据库资源、执行被延迟的bgrewriteaof、检查持久化操作的运行状态、将AOF缓冲区中的内容写入AOF文件、关闭异步客户端、增加cronloops计数器的值

8.1.6 启动过程

  • 初始化服务器状态结构,由initServerConfig函数完成,设置服务器的运行ID、默认运行频率、默认文件配置路径、运行架构、默认端口号、默认RDB持久化条件和AOF持久化条件、初始化服务器的LRU时钟、创建命令表
  • 载入配置选项:指定配置参数或文件
  • 初始化服务器数据结构: 在第一步时initServerConfig函数只是创建了命令表,而服务器状态还包含其他数据结构,如server.clients链表,server.db数组,server.pubsub_channels字典,server.lua环境,server.log慢查询日志。该函数会为以上数据结构分配内存,之后,initServer函数负责初始化数据结构,为服务器设置进程信号处理器、创建共享对象、打开服务器的监听端口、为serverCron函数创建时间事件、若存在AOF文件,打开AOF文件,若没有,就创建一个新的AOF文件、初始化服务器的后台I/O模块(bio),到这就出现了“面包”图
  • 还原数据库状态:若服务器启用了AOF持久化功能,那么就用AOF文件还原数据库状态,否则使用RDB文件来还原数据库状态
  • 执行服务器的事件循环

8.2 Redis持久化

8.2.1 RDB

  • RBD是默认方式,将内存中数据以快照的方式写入到二进制文件中(过期的key被忽略),默认文件名为dump.rdb
  • 触发rdbSave过程的方式
    • save命令:阻塞Redis服务器进程,直到RDB文件创建完毕为止。
    • bgsave命令:fork出一个子进程,会先将数据写入到一个临时文件中,借助了OS的Copy-On-Write(kernel把触发异常的页复制一份),在执行快照的同时,正常处理写操作,主线程修改数据,这块数据就被复制一份,生成副本,然后,子进程把副本写进RDB文件。等持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。RDB持久化过程由子 进程负责,完成后自动结束,而父进程则继续处理命令请求。阻塞只发生在fork阶段,一般时间很短
    • master接收到slave发来的sync命令。写时拷贝只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
    • 定时save(配置文件:900 1或300 10 或 60 10000)
  • 命令bgsave与bgrewriteaof不能同时执行,若bgsave正在执行,则bgrewriteaof延迟到bgsave执行完再执行。若bgrewriteaof正在执行,则服务器拒绝执行bgsave命令。dirty计数器:记录距离上一次save/bgsave命令之后,服务器对数据库状态修改了多少次
  • RDB文件结构:REDIS占5个字节,检查文件是否是RDB文件;db_version长度为4个字节,一个字符串表示的整数,记录了RDB文件的版本号;database包含零个或任意多个数据库;EOF:1个字节,标志着RDB文件正文内容的结束;check_sum:8字节,保存校验和,由前面四部分计算得出的。
  • database结构:SELECTDB:1字节,表示要读一个数据库号码;db_number保存着一个数据库号码;key_value_pairs 保存了数据库中的所有键值对数据,由TYPE、key、value组成
  • RDB的优缺点,优点:一个紧凑的文件,适合大规模的数据恢复,对数据完整性和一致性要求不高,恢复速度快。缺点:若Redis出现宕机,就会丢失最后一次快照后的所有修改。fork时,在数据大时较耗时,不能响应毫秒级请求。

8.2.2 AOF

  • AOF:通过保存服务器所执行的写命令来记录数据库状态。key已过期,但没有被惰性删除或定期删除,AOF不受影响,当key被删除后向AOF追加一条DEL命令来显示。
  • AOF的优缺点,优点默认策略为每秒钟 fsync 一次,最多丢失一秒的数据,缺点:aof文件要远大于rdb文件,恢复速度慢于rdb,运行效率低。AOF提供更新的数据,而RDB提供更快的恢复速度
  • AOF的修复:如果aof文件被破坏, 程序redis-check-aof–fix会进行修复(在flushAppendOnlyFile函数中),若出现断电等意外情况,就将写出错的情况记录到日志里,之后会处理错误。重启redis然后重新加载,AOF优先,它保存的数据集要比RDB完整。
  • AOF写入的步骤:命令追加(将命令追加到AOF缓冲区)文件写入,文件同步
  • AOF重写触发机制: 默认配置是当AOF文件大小增长超过上次rewrite后大小的100%且文件大于64M时并且没有子进程运行时触发。可通过auto-aof-rewrite-percentage和auto-aof-rewrite-min-size参数配置。
  • AOF重写的实现原理 :fork出一个新进程来将文件重写(先写入临时文件最后再rename),遍历新进程数据库的内存数据,将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,根据键的类型,使用适当的写入命令来重现键的当前值。例如:rpush、sadd、zadd、hmset、xadd等命令
  • AOF后台重写:AOF重写程序放到子进程中执行,当Redis执行完一个命令后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区,最后再写入临时文件中。
  • 虚拟内存:暂时把不经常访问的数据从内存交换到磁盘中,从而提高数据库容量,但代码复杂,重启慢,复制慢等等,目前已被放弃
  • 持久化的优化:抛弃AOF重写机制 ,保存RDB+AOF(业务低峰时用定时任务替换重写命令);Pika:适合数据大于50G且重要,多线程,持久化SSD

8.2.3 reaof源码?

  • reaof过程

  • 对于缓存块的大小,因为程序需要不断对这个缓存执行 append 操作,而分配一个非常大的空间并不总是可能的,也可能产生大量的复制工作, 所以这里使用多个大小为 AOF_RW_BUF_BLOCK_SIZE 的空间来保存命令。默认每个缓存块的大小是10MB

  • 如果客户端有命令执行,然后feedAppendOnlyFile函数判断是否开启了AOF标识,若开启,则将命令放入aof_buf_blocks中,继续判断是否有子进程在运行,若有,则说明正在进行reaof,就将命令放入aof_rewrite_buf_blocks中。

  • 服务器开启就是循环文件事件和时间事件的过程,而时间事件是通过ServerCron()函数执行的。该函数会100ms执行一次查看是否有reaof或需要刷新事件。

  • 若有刷新事件(默认每秒),调用flushAppendOnlyFile函数将aof_buf_blocks写入磁盘中,若有reaof事件,调用rewriteAppendOnlyFileBackground()函数,它执行 fork() ,调用rewriteAppendOnlyFile函数子进程,在tmpfile中对 AOF 文件进行重写,完成后子进程结束,通知父进程。

  • 父进程会捕捉子进程的退出信号,如果子进程的退出状态是 OK ,那么父进程调用backgroundRewriteDoneHandler函数将aof_rewrite_buf_blocks追加到临时文件,然后使用 rename(2) 对临时文件改名,用它代替旧的 AOF 文件,但它调用的 write 操作会阻塞主进程。

  • 到现在,后台 AOF 重写已经全部完成了。

  • 重写AOF存在的问题

    • 内存开销:重写期间主进程会将fork之后的数据变化写进aof_rewrite_buf_blocks中,aof_buf_blocks和aof_rewrite_buf_blocks大部分内容是重复的,带来额外的内存开销,可能导致最大内存限制。
    • CPU开销:aof_rewrite_buf_blocks写入数据并使用eventloop向子进程发送数据;子进程重写的后期会循环读取主进程发来的增量数据追加写入tmpfile中;子进程完成重写操作后主进程会进行收尾工作。这3点都可能会造成RT抖动。
    • 磁盘IO开销:写入临时aof和aof_buf持久化都会产生磁盘IO
    • 代码复杂度:6个pipe进行主/子进程的数据传输和交互,使得AOFRW逻辑复杂难以理解。
  • Redis 7.0 Multi Part AOF:将原来的单个AOF文件拆分成多个AOF文件,将AOF分为了3种类型。manifest文件来跟踪管理这些AOF,新的AOF重写流程依然会fork一个子进程进行重写操作(完全独立),但在主进程中会同时打开一个新的Incr文件,重写期间的变化数据都写进去,最终重写会产生一个Base文件,此时Base+Incr=全部数据,AOFRW结束时,主进程会更新manifest文件,将新生成的Base和Incr信息加进去,之前的Base和Incr标记为History,manifest更新完成标志整个AOFRW结束。

    • Base:基础AOF,由子进程通过重写产生,最多只有一个。
    • Incr:增量AOF,在AOFRW开始执行时被创建,可能有多个。
    • History:历史AOF,由Base和Incr变化而来,每次AOFRW完成时,它们就会变为History,会被Redis自动删除。

8.3 Redis深入

8.3.1 主从复制

  • 通过执行slaveof命令或设置slaveof选项,让一个服务器去复制另一个服务器。允许多个slave server拥有和mater server相同的数据库副本
  • 旧版复制功能(2.8之前):同步(SYNC)和命令传播。缺点:断线后复制的效率低。
  • 新版复制功能:完整重同步:用于初次复制,和SYNC一样,让主服务器创建并发送RDB文件,向从服务器发送保存在缓冲区中的写命令。部分重同步(PSYNC):用于断线后重复制,重连后,主服务器将断开期间执行的写命令发送给从服务器。
  • 部分重同步功能由主从服务器的复制偏移量、主服务器的积压缓冲区和服务器的运行ID组成
    • 复制偏移量:主从服务器每次传播N个字节的数据,就把自己的复制偏移量offset+N
    • 复制积压缓冲区是由主服务器维护的一个固定长度的先进先出队列,默认大小为1MB,当从服务器断开重连时,从服务器通过PSYNC命令将自己的复制偏移量offset发送给主服务器,若offset之后的数据在积压缓冲区中,就进行部分重同步的操作,否则进行完整重同步。
    • 服务器运行ID:在启动时由40个随机的十六进制字符生成,断开重连时,从服务器将保存的master运行ID发送给主服务器,若两个运行ID相同,则进行部分重同步操作,否则进行完整重同步操作。
  • 复制的实现:设置主服务器的地址和端口、建立套接字连接、发送ping命令、身份验证(可选)、发送端口信息、同步、命令传播。
  • 心跳检测:在命令传播阶段,从服务器会默认以每秒一次的频率向主服务器发送replconf ack < replication_offset>命令,作用是检测主从服务器的网络连接状态;辅助实现min-slaves-to-write和min-slaves-max-log选项;检测命令丢失,通过对比主从服务器的复制偏移量知道命令是否丢失。
  • 主从复制的特点:一个master 可以拥有多个slave;多个slave可以连接到同一个master,还可以连接到其它slave;主从复制不会阻塞master,在同步数据时,master还可以继续处理client请求;提高系统的伸缩性。
  • 哨兵是Redis高可用性的解决方案,由一个或多个sentinel实例组成的哨兵系统可以监视任意多个主服务器。
  • 启动哨兵后,会创建连向主服务器的网络连接,命令连接指专门用于向主服务器发送命令,并接受命令回复,而订阅连接指专门用于订阅主服务器的sentinel:hello频道
  • 获取服务器信息:和主数据库建立连接后,哨兵会定时执行操作:每10秒哨兵会向主数据库和从数据库发送info命令、每2秒哨兵会向主数据库和从数据库的sentinel:hello 频道发送自己的信息、每秒哨兵会向主数据库和从数据库和其他哨兵节点发送ping命令
  • 当redis集群的主节点故障时,Sentinel集群将从剩余的从节点中选举一个新的主节点的步骤:①故障节点主观下线:心跳检测未回应;②故障节点客观下线:超过quorum数量的Sentinel节点认为该redis节点主观下线,则该redis客观下线;③Sentinel集群选举Leader:如果一个Sentinel节点获得的选举票数达到Leader最低票数(quorum和Sentinel节点数/2+1的最大值),则该Sentinel节点选举为Leader;否则重新进行选举;③Sentinel Leader决定新主节点:选举出Sentinel Leader后,由Sentinel Leader从redis从节点中选择一个redis节点作为主节点。
  • 选举领头Sentinel:如果主数据库断开连接,则会选举领头的哨兵节点对主从系统发起故障恢复。选举过程使用raft 算法。选举规则:删除下线或中断的服务器,尽量选举从服务器优先级高的、复制偏移量大的、运行id小的。
  • 脑裂问题:指某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着,此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master,这时,集群里就会有两个master。可能client还没来得及切换到新的master,还继续写向旧master的数据可能会丢失。配置参数:min-slaves-to-write 1:要求至少有1个slave 和 min-slaves-max-lag 10:数据复制和同步的延迟不能超过10秒,否则master就不会再接收任何请求了,这样在脑裂场景下,最多就丢失10秒的数据

8.3.2 集群(cluster)

  • Redis集群是redis提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移功能。由多个节点组成,通过握手(cluster meet命令)添加节点。

  • Redis集群通过分片的方式保存数据库中的键值对,集群中的整个数据库被分为16384个槽(slot),每个节点可以处理0-16384个槽,当每个槽都有节点在处理时,集群就处于上线状态,命令cluster addslots < slot>可以将一个或多个槽指派给节点负责,slot属性是一个二进制数组,若slots[i]=1,表示节点负责处理槽i,若slots[i]=0,则表示节点不负责处理槽i

  • RedisCluster会设计成16384个槽?①CRC16算法产生的hash值有16位,如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。②redis的集群主节点数量基本不可能超过1000个,③槽位越小,节点少的情况下,压缩率高。redis的node配置信息通过位图存储传输的,传输前有一个压缩过程,填充率=槽数量/节点数, 槽过多,填充率高,对应的压缩比会变小(即文件压缩前后的大小比)

  • 节点保存键值的步骤 :先计算key属于哪个槽(计算key的CRC16值,然后对16384取模,可以获取key对应的hash slot)、判断槽是否由当前节点负责处理、若不是则返回moved错误(该错误被隐藏,但在单机模式下会打印错误),根据错误信息转向正确的节点,而节点数据库的实现只能使用0号库。每个机器会维护一个私有位图和公有位图,私有位图可查找slot是否自己持有,否则回去公有位图上查找slot所在位置,并查找到对应节点返回节点信息。

  • 故障检测:通过集群总线中的ping/pong响应信息(节点id、哈希槽位图、节点标志、tcp端口等)来判断节点是否正常运行。故障检测的标志:①PFAIL:当节点不可访问超过NODE_TIMEOUT时间时会标记pfail,可能失败,未确认,②FAIL:大多数master节点发出PFAIL/FAIL 条件 NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT,即节点故障,大多数节点确认。选举新节点:根据currentEpoch选举,master有lastVoteEpoch,若slave的currentEpoch<=lastVoteEpoch,则拒绝投票,master投票比configEpoch大。

  • Redis集群选举过程:

    • 判断节点宕机,主观宕机:如果某个节点在超时时间内没有返回pong,即pfail;客观宕机:在gossip ping消息中ping其他节点,如果超过半数的节点都认为pfail了,那么就会变成fail
    • 对宕机的master node,在其所有的slave node中,选择一个切换成master node
    • 从节点选举,选举从服务器优先级高的、复制偏移量大的、运行id小的。master node开始slave选举投票,给要进行选举的slave进行投票,如果大部分master node(N/2 + 1)都投票给了某个从节点,那么选举通过,那个从节点可以切换成master。从节点执行主备切换,成为主节点。
  • 重新分片:将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点被移动到目标节点。可在线操作。ASK错误是在节点迁移的过程中,被迁移槽的一部分键值对保存在源节点,而另一部分保存在目的节点。当动态添加或减少node节点时,需要将16384个槽做再分配,槽中的键值也要迁移。Redis Cluster重新分片其实是将哈希槽(密钥)从一个node移动到另一个node

  • Hash Tag原理:当一个key包含 {} 的时候,不对整个key做hash,而仅对 {} 包括的字符串做hash。

  • 请求重定向:集群中的key不在当前节点会返回MOVED重定向,客户端再去找到目标节点。公有位图(集群所有节点槽信息):clusterState的slots数组。私有位图(某个节点的槽信息):clusterNode的slots数组,通过位图方式保存节省空间,16384/8恰好是2048字节,JedisCluster会维护一个hashslot -> node本地映射表(ask重定向不更新)

  • 集群扩容过程(3->4节点):(1)启动节点后 节点握手,手动:cluster meet,cluster replicate命令为结点指定slave角色(默认Master),16384个槽均分给4个节点;自动:redis-trib.rb脚本;(2)槽迁移:cluster addslots,①客户端向源节点发送key命令,检查Key所在Slot是否属于当前节点(计算crc16(key) % 16384得到Slot、通过位图查询节点位置)②若不属于,则响应MOVED错误重定向客户端,③若属于当前节点且Key存在,则直接操作,返回结果给客户端;④若Key不存在,检查该Slot是否迁出中?(clusterState.migrating_slots_to),若Slot迁出中,返回ASK错误(包含目标节点地址信息),客户端向目标节点重新发送请求(临时重定向)⑤若Slot未迁出,检查Slot是否导入中?(clusterState.importing_slots_from),若Slot导入中且有ASKING标记,则直接操作,否则响应MOVED错误重定向客户端

  • 集群采取gossip协议进行通信,gossip协议包含多种消息,消息由消息头和消息正文组成。

    • meet消息:表示接收到服务器发送的cluster meet命令
    • ping消息:每秒对五个节点中最长时间没有发送过ping消息的节点发送ping消息,以检测是否在线
    • pong消息:接收到meet或ping消息
    • fail消息:当A节点判断B节点进入fail状态,A节点就会向集群广播一条关于节点B的fail消息
    • publish消息:当节点接收到publish命令时,向集群广播一条publish消息
  • 集群的优点,容错性:解决在单服redis的单点问题。扩展性:集群能够很好的实现缓存的性能升级,如多节点的热部署。性能提升 :在扩展过程中体现。

  • 发布与订阅:所有频道的订阅关系保存在服务器状态的pubsub_channel字典里,该字典的键是某个被订阅的频道,值是一个链表,记录了所有订阅这个链表的客户端。

  • 将消息发送给频道订阅者 :若客户端执行publish命令,那将在字典中查找该频道,并通过遍历链表将消息发送给该频道的所有订阅者。命令pubsub channels/numsub/numpat

  • 数据分布的算法

    • hash算法:大量缓存重建
    • 一致性hash算法:自动缓存迁移,但当节点较少时会存在数据倾斜。
    • 一致性hash+虚拟节点:自动负载均衡

8.3.3 Redis的过期策略

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

  • 定期删除:Redis默认每隔100ms随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。

  • 惰性删除:获取 key 的时候,redis 会进行检查 ,如果这个 key 设置了过期时间,并且已经过期了,那么就直接删除,返回空。
    (Redis采用的过期策略:惰性删除+定期删除)

  • Redis的8种数据淘汰策略(Redis4.0加入了LFU淘汰策略)

    • volatile-lru:使用LRU算法移除设置过过期时间的key。
    • volatile-random:随机移除设置过过期时间的key。
    • volatile-ttl:移除即将过期的key,根据最近过期时间来删除(辅以TTL)
    • allkeys-lru:使用LRU算法移除任何key。
    • allkeys-random:随机移除任何key。
    • noeviction:不移除任何key,只是返回一个写错误。
    • volatile-lfu:使用LFU算法移除设置过过期时间的key。
    • allkeys-lfu:使用LFU算法移除任何key。
  • 自定义实现LRU算法:HashMap+双链表实现(或使用LinkedHashMap),其中双链表保存了前驱和后继指针,方便在O(1)的时间复杂度移除元素

  • 监视器:执行monitor命令,客户端就可以将自己变成一个监视器,实时的接收并打印出服务器当前处理的命令请求的相关信息。服务器将所有的监视器都记录在monitors链表中。每次处理命令请求时,服务器都会遍历monitors链表,将相关信息发送给监视器。

  • 慢查询日志功能用于记录执行时间超过给定时长的命令请求。以先进先出的形式保存在slowlog链表,参数:slowlog-log-slower-than和slowlog-max-len,

8.3.4 redis的并发竞争?

  • 客户端:进行连接池化、读写加锁;服务端:使用setnx命令实现分布式锁。

  • 缓存穿透:访问不存在的对象,解决方法:缓存空对象或布隆过滤器(由二进制向量或bitmap和多个hash函数组成,特点:可能存在/一定不存在,hash函数存在hash碰撞,所以存在误判,增加hash次数来减少误判,实现删除功能可通过计数器来实现,但会占用更大的空间,更新布隆过滤器可通过重建的方式实现)

  • 缓存雪崩:缓存层不可用,导致存储层调用量暴增,甚至挂掉。解决方法:保证缓存层的高可用性、依赖隔离组件为后端限流并降级、

  • 缓存击穿:出现热点事件,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库。解决方法:缓存读写分离,master作高可用,slave可动态扩容。

  • 缓存热点key的重建优化:若有一个热点key,并发量巨大,不能在短时间重建缓存,在缓存失效的瞬间,大量线程重建缓存,造成后端负载过大,甚至让应用崩溃

  • 解决目标:减少重建缓存的次数、数据尽可能一致、较少的潜在危险、

  • 解决方法:

    • 互斥锁:只允许一个线程重建缓存,其他线程等待(setnx)或查询失败默认值快速返回;
    • 永不过期:对key没有设置过期时间,为每个value设置一个逻辑过期时间,若超时,则使用单独的线程去构建缓存。
    • 开启本地缓存,会有短暂的数据不一致问题
    • 热点散列。数据聚合:同一个key存储多份,随机读取,分散读的压力
  • 缓存与数据库双写不一致:先删除缓存,再写数据库。若写数据库的过程中有请求就放入队列中。

  • 大key的危害:内存空间不均匀,执行命令超时阻塞,网络拥塞。是由于Redis单线程处理请求的架构决定。优化方案:在应用层对key进行压缩;对key重新设计;把key放在不同的redis实例中存储;redis 分片

8.3.5 Redis的优缺点?

  • Redis效率高的原因?

    • 单线程,避免了多线程的频繁上下文切换问题
    • 基于内存操作,速度快
    • 非阻塞的IO多路复用机制
  • Redis的缺点?
    • 单线程不能充分利用多核CPU资源
    • 存储的数据量受机器内存限制
    • 重写AOF文件会有延迟。
  • Redis的边界?
    • 单实例最多能存2.5亿个key
    • key和value的大小限制都是512M
  • Redis为什么要支持5种数据结构?
    • 如果Redis只支持简单的key和value,当做简单的缓存,若有复杂的数据结构就要交给客户端处理,如Java中的List和Map等,那么客户端就必须要处理并发的问题,这会带来性能上的损耗。
    • 由于Redis支持5种数据结构,可以将原本放在客户端的处理流程放在服务端来处理,因为Redis支持高并发,更快。
  • Redis6.0特点:①多线程:使用多线程处理用户请求、网络数据的读写和协议解析,提高CPU利用率,但命令执行还是单线程。②重新设计客户端缓存:使用广播模式,客户订阅 key 的前缀:每次修改匹配前缀的 key 时,这些订阅的客户端都会收到通知。服务端无需记住每个客户端请求的 key。③提升了RDB日志加载速度

注:多线程上下文切换会慢的原因:每个任务(线程)通过时间片轮转的方式获取CPU,而任务从保存到再加载的过程就是一次上下文切换。因为CPU寄存器要保存和加载,系统调度器的代码需要执行,所以会变慢。

8.4 分布式锁的实现

8.4.1 redis实现分布式锁

  • 为什么不能使用JVM锁:因为传统的锁(Synchronized 和 lock)是解决在一个JVM下的多线程竞争问题,而分布式是具有多个JVM的。传统的锁跨不了JVM。
  • 分布式锁的实现方式:Redis的分布式锁、MySQL数据库乐观锁、ZooKeeper的分布式锁、自研分布式锁(如谷歌的 Chubby)
  • Redis分布式锁实现的关键:①原子命令加锁:SET key random_value NX PX 30000 ②设置值random_value是随机的,为了更安全的释放锁。例如:使用uuid ③释放锁的时候需要检查 key 是否存在,且 key 对应的值是否和指定的值相等,相等才能释放锁。为了保障原子性,需要用 lua 脚本。
  • 使用多参数的set方法实现单机的分布式锁示例
public class RedisTool {private static final String LOCK_SUCCESS = "OK";private static final String SET_IF_NOT_EXIST = "NX";private static final String SET_WITH_EXPIRE_TIME = "PX";private static final Long RELEASE_SUCCESS = 1L;/*** 尝试获取分布式锁* @param jedis Redis客户端* @param lockKey 锁* @param requestId 请求标识* @param expireTime 超期时间* @return 是否获取成功*/public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);return LOCK_SUCCESS.equals(result);}/*** 释放分布式锁* @param jedis Redis客户端* @param lockKey 锁* @param requestId 请求标识* @return 是否释放成功*/public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));return RELEASE_SUCCESS.equals(result);}}
  • 上面redis分布式锁存在的问题:①单机部署的情况,宕机后不可用。使用Redlock解决。②客户端持有锁超时,使用 Redssion 解决。
  • 实现可重入的分布式锁
    • 可重入:指可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,如果没有可重入锁的支持,在第二次尝试获得锁时将会进入死锁状态。
    • 实现方案:使用ThreadLocal实现,获取锁后将value保存在ThreadLocal中,同一线程再次尝试获取锁的时候就先将 ThreadLocal 中的 值 与 Redis 的 value 比较,如果相同则表示这把锁可以拥有该线程,即实现可重入锁。

8.4.2 Redlock

  • Redlock:假设有 5 个完全独立的 Redis Master 节点,他们分别运行在 5 台服务器中,可以保证他们不会同时宕机。
  • 客户端获取锁的步骤:①获取以毫秒为单位的时间戳;②依次尝试从 N 个实例,使用相同的 key 和随机值获取锁。③当且仅当从大多数 Redis 节点都取到锁,并且锁使用时间小于锁失效时间时,锁才算获取成功(锁使用的时间 = 当前时间 - 开始获取锁时间)④若获到锁,key 的真正有效时间 = 有效时间 - 锁使用的时间;⑤若获取锁失败(没有在至少 N/2+1 个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁。
  • Redlock存在的问题:①对时钟依赖性太强, 若N个节点中的某个节点发生 时间跳跃 ,也可能会引此而引发锁安全性问题。②某个节点故障重启后可能一把锁被多个客户端持有。延迟重启解决方法会导致系统在TTL时间内任何锁都将无法加锁成功。所以不能用

8.4.3 Redssion

  • Redssion:不需要明确指定 value ,框架会生成一个由 UUID 和 加锁操作的线程的 threadId 用冒号拼接起来的字符串。锁名称用的是hash类型,而不是string。因为加锁使用的hincrby 命令,支持可重入锁
  • 加锁的过期时间默认是 30s,当加锁成功或者重入成功后进入看门狗逻辑:定时任务每 internalLockLeaseTime/3 ms 后执行一次。而 internalLockLeaseTime 默认为 30000ms。所以该任务每 10s 执行一次。定时任务(Timeout)基于 netty 的时间轮HashedWheelTimer
  • 释放锁: 有个counter 的判断,如果减一后小于等于 0。就执行 del key 的操作,之后publish发布事件(tryAcquire 加锁失败订阅的事件),解锁完成后通过cancelExpirationRenewal方法取消看门狗定时任务

8.4.4 分布式锁存在的问题

  • 主备切换:主从模式的集群部署方式中,当主节点挂掉时,从节点会取而代之,但客户端无明显感知。当客户端 A 成功加锁,但是数据还没被同步到 Salve,此时主节点挂掉,从节点提升为主节点,新的主节点没有锁的数据,当客户端 B 加锁时就会成功,出现一把锁被拿到了两次的场景。
  • 集群脑裂:因为网络问题,导致 Redis master 节点跟 slave 节点和 sentinel 集群处于不同的网络分区,因为 sentinel 集群无法感知到 master 的存在,所以将 slave 节点提升为 master 节点,此时存在两个不同的 master 节点(集群模式也有类似的问题)。当不同的客户端连接不同的 master 节点时,两个客户端可以同时拥有同一把锁。
  • 分布式锁的一致性要求 CP,但是Redis 集群架构之间的异步通信满足的是 AP ,因为大多场景中能容忍,BASE保持最终一致性行就可以。

参考

  1. 20 道 Redis 常见面试题
  2. 为什么Redis集群有16384个槽

【面试题】8.Redis相关相关推荐

  1. redis相关知识点讲解,redis面试题

    redis相关知识点讲解,redis面试题 1. redis基本知识点 1.1 什么是redis? 1.2 redis的key的设计 1.3 redis的value数据类型有哪些? 1.3.1 str ...

  2. 小李学知识之redis相关(含redis面试题)

    Redis相关学习 1. 简单介绍 2. redis缓存数据的流程 2.1 redis作为缓存的原因 3. redis的基本命令 4. redis支持的五种数据结构以及相关命令 4.1 String ...

  3. 14.Redis相关原理

    案例背景 Redis 属于单线程还是多线程?考察 Redis 的线程模型 案例分析 基本都知道 Redis 是单线程的,并且能说出 Redis 单线程的一些优缺点,比如,实现简单,可以在无锁的情况下完 ...

  4. Redis 相关知识点

    Redis 相关知识点 概述 为什么要用缓存 为什么用redis 用redis缓存了哪些东西 单线程redis为什么这么快 redis的数据类型和使用场景 redis 的过期策略都有哪些?内存淘汰机制 ...

  5. Day5 - 前端高频面试题之计算机网络相关

    传送门>>> 上一篇: Day4 - 前端高频面试题之浏览器相关 1.请介绍一下HTTP和HTTPS的区别? HTTPS是在HTTP的基础上加入了SSL协议,SSL依靠证书来验证服务 ...

  6. 【Redis-09】面试题之Redis数据结构与对象-RedisObject(下篇)

     承接上篇[Redis-08]面试题之Redis数据结构与对象-RedisObject(上篇) 8. type-字符串string 8.1 字符串的三种encoding编码(int + embstr ...

  7. PHP连接redis并执行redis相关命令的方法详解

    PHP连接redis并执行redis相关命令的方法详解 连接redis库的方法 共性的运算归类 redis服务类函数 set 操作增删改查 List栈的结构,注意表头表尾,创建更新分开操作 Set,没 ...

  8. Spring中RedisTemplate方法中,redis相关操作笔记。[redis生成指定长度自增批次号,删除、设置过期时间等]

    Spring中RedisTemplate方法中,redis相关操作笔记. redis获取自增批次号 // opsForValue()生成long UUID = redisTemplate.opsFor ...

  9. 【面试题】Redis篇-常见面试题p1

    [面试题]Redis篇-常见面试题p1 备战实习,会定期的总结常考的面试题,大家一起加油!

最新文章

  1. In Gradle projects, always use http://schemas.andr
  2. Python知识点3——列表操作
  3. 【Xamarin开发 Android 系列 4】 Android 基础知识
  4. Linux .bin安装文件制作
  5. flask高级编程 LocalStack 线程隔离
  6. c++的lambda表达式捕获this_贯穿 C++ 11 与 C++ 17 的 Lambda 到底是个什么?
  7. 程序员是否应该创造面向 IDE 而非人类的编程语言?
  8. ASP.NET中?和??的用法
  9. python编程入门电子书-《Python编程 从入门到实践》高清电子书免费下载
  10. 扩散方程——热传导问题(能量定律+傅里叶热传导定律)+ 拉普拉斯方程 | 偏微分方程(三)
  11. vue鼠标上下滚动放大与缩小
  12. npm 安装vue脚手架报错警告npm WARN deprecated
  13. matlab特征值分解
  14. css 控制图片的横竖比例
  15. picpick尺子像素大小精度不够准确_picpick尺子像素大小精度不够准确_如何准确的按比例打印图纸...
  16. html语言字体如何变大,怎么把网页的字变大_怎么让html字体变大?
  17. 小程序画布合成二维码海报图,并保存到相册
  18. MySQL—关联查询与子查询(从小白到大牛)
  19. Bugzilla使用说明
  20. 穴位保健:自我按摩赶走亚健康

热门文章

  1. 清华老师终于把微服务讲清楚了
  2. 育才计算机应用学校,仪陇县扶轮育才职业学校2020年招生简章
  3. 诗人温古与洛夫的特别情缘:冥冥之中的有意安排
  4. 广州市白云区2021-2022学年九年级第一学期期末考试英语试题
  5. ppt html5转换,PPT还能转H5?这大概是制作招聘H5最快的方法了...
  6. 相声源稿件:对春联国际版
  7. java求两个数相加代码
  8. TextView相关
  9. 1600802010韩璐---天气预报
  10. 第二单元:文字与列表