【面试题】8.Redis相关
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保持最终一致性行就可以。
参考
- 20 道 Redis 常见面试题
- 为什么Redis集群有16384个槽
【面试题】8.Redis相关相关推荐
- redis相关知识点讲解,redis面试题
redis相关知识点讲解,redis面试题 1. redis基本知识点 1.1 什么是redis? 1.2 redis的key的设计 1.3 redis的value数据类型有哪些? 1.3.1 str ...
- 小李学知识之redis相关(含redis面试题)
Redis相关学习 1. 简单介绍 2. redis缓存数据的流程 2.1 redis作为缓存的原因 3. redis的基本命令 4. redis支持的五种数据结构以及相关命令 4.1 String ...
- 14.Redis相关原理
案例背景 Redis 属于单线程还是多线程?考察 Redis 的线程模型 案例分析 基本都知道 Redis 是单线程的,并且能说出 Redis 单线程的一些优缺点,比如,实现简单,可以在无锁的情况下完 ...
- Redis 相关知识点
Redis 相关知识点 概述 为什么要用缓存 为什么用redis 用redis缓存了哪些东西 单线程redis为什么这么快 redis的数据类型和使用场景 redis 的过期策略都有哪些?内存淘汰机制 ...
- Day5 - 前端高频面试题之计算机网络相关
传送门>>> 上一篇: Day4 - 前端高频面试题之浏览器相关 1.请介绍一下HTTP和HTTPS的区别? HTTPS是在HTTP的基础上加入了SSL协议,SSL依靠证书来验证服务 ...
- 【Redis-09】面试题之Redis数据结构与对象-RedisObject(下篇)
承接上篇[Redis-08]面试题之Redis数据结构与对象-RedisObject(上篇) 8. type-字符串string 8.1 字符串的三种encoding编码(int + embstr ...
- PHP连接redis并执行redis相关命令的方法详解
PHP连接redis并执行redis相关命令的方法详解 连接redis库的方法 共性的运算归类 redis服务类函数 set 操作增删改查 List栈的结构,注意表头表尾,创建更新分开操作 Set,没 ...
- Spring中RedisTemplate方法中,redis相关操作笔记。[redis生成指定长度自增批次号,删除、设置过期时间等]
Spring中RedisTemplate方法中,redis相关操作笔记. redis获取自增批次号 // opsForValue()生成long UUID = redisTemplate.opsFor ...
- 【面试题】Redis篇-常见面试题p1
[面试题]Redis篇-常见面试题p1 备战实习,会定期的总结常考的面试题,大家一起加油!
最新文章
- In Gradle projects, always use http://schemas.andr
- Python知识点3——列表操作
- 【Xamarin开发 Android 系列 4】 Android 基础知识
- Linux .bin安装文件制作
- flask高级编程 LocalStack 线程隔离
- c++的lambda表达式捕获this_贯穿 C++ 11 与 C++ 17 的 Lambda 到底是个什么?
- 程序员是否应该创造面向 IDE 而非人类的编程语言?
- ASP.NET中?和??的用法
- python编程入门电子书-《Python编程 从入门到实践》高清电子书免费下载
- 扩散方程——热传导问题(能量定律+傅里叶热传导定律)+ 拉普拉斯方程 | 偏微分方程(三)
- vue鼠标上下滚动放大与缩小
- npm 安装vue脚手架报错警告npm WARN deprecated
- matlab特征值分解
- css 控制图片的横竖比例
- picpick尺子像素大小精度不够准确_picpick尺子像素大小精度不够准确_如何准确的按比例打印图纸...
- html语言字体如何变大,怎么把网页的字变大_怎么让html字体变大?
- 小程序画布合成二维码海报图,并保存到相册
- MySQL—关联查询与子查询(从小白到大牛)
- Bugzilla使用说明
- 穴位保健:自我按摩赶走亚健康