Redis的基本数据类型
Redis有哪些常用的数据类型?
- String:字符串(最常用的缓存)
- Hash:哈希(保存对象)
- List:有序列表(消息队列)
- Set:无序集合(抽奖、点赞、求交集、并集、差集)
- Zset:有序集合(热门排行榜)
- BitMap:位图(统计用户在线状态、活跃用户数、签到等)
- HyperLogLog:基数统计(统计网站UV)
- Geospatial:地理空间(地理位置相关查询与计算)
String的底层结构是什么?
- 是一种叫做sds的数据结构(simple dynamic string 简单动态字符串),本质上其实还是字符串数组。
- sds又可以细分为:sdshdr5(2^5 byte)、sdshdr8(2^8 byte)、sdshdr16(2^16 byte)、sdshdr32(2^32 byte)、sdshdr64(2^64 byte),用于存储不同长度的字符串。
为什么Redis要用sds实现字符串?
- c语言本身没有字符串类型,只有字符串数组,定义字符串数组时需要先分配足够内存,否则会发生内存溢出,而sds无需担心内存溢出问题,它支持动态扩容;
- 字符串数组获取长度必须遍历数组,时间复杂度为O(n),而sds中定义了len属性,可以直接获取长度,时间复杂度为O(1);
- 字符串数组在进行修改时会做内存重新分配,而sds可以通过空间预分配和惰性空间释放来防止多次重新分配内存;
- 空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数。
- 惰性空间释放:对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用 free 属性将这些字节的数量记录下来,等待后续使用。
- c语言字符串通过第一个’\0’来标记字符串结束,因此不能保存图片、音频等二进制文件,而sds是通过len属性来确定长度,因此可以保存二进制文件。
String有几种编码类型?
- int:存储8个字节的长整型(最大值 2^63 - 1),超出范围会升级为embstr;
- embstr:存储小于44字节的字符串,embstr中的redisObject与sds存储连续,创建时只需分配一次内存空间,存取效率高,且embstr不可修改,修改后会升级为raw;
- raw:存储大于44字节的字符串,raw中的redisObject与sds分开存储,创建时只需分配两次内存空间,适合存储较大或频繁修改的字符串。
Hash的底层实现是什么?
- ziplist与hashtable
什么是ziplist(压缩列表)?
- ziplist是一个经过特殊编码,由连续内存块组成的双向链表。为了达到压缩内存,节省空间的目的,它不存储上一个和下一个节点的指针,而是存储上一个节点的长度和当前节点的长度,因此可以通过计算长度来实现元素遍历。这样做虽然牺牲了一定的查询效率,但也节省了空间,(以时间换空间)因此适合元素不多的场景。
- 在hash类型中,同时满足以下两个条件时才使用ziplist:
- hash对象保存的键值数 < 512;
- 所有键和值的字符串长度都 < 64 byte。
ziplist与数组的区别?
- ziplist和数组都是连续内存,需要预分配,但是数组元素长度相同,ziplist的元素长度可以不同;
- 数组的元素保存了索引下标,而ziplist的元素只存储了上一节点和当前节点的长度;
- 数组通过下标来遍历,ziplist通过元素记录的长度遍历;
- 数组的查询时间复杂度O(1),ziplist的查询时间复杂度O(n)。
hashtable的特点是什么?
- hashtable与java中的hashtable类似,也是一个数组 + 链表的结构,同样采用拉链法解决hash冲突。
- 在redis的hashtable的底层数据结构中有两个ht:
- ht[0],是存放数据的table,作为非扩容时容器;
- ht[1],只有正在进行扩容时才会使用,它也是存放数据的table,长度为ht[0]的两倍。
- 当总元素个数与hash槽的比值大于dict_force_resize_ratio(1)时,会触发扩容,即触发ht[0] 到 ht[1] 的rehash过程。
hashtable的扩容原理
- hashtable扩容采用渐进式rehash,避免数据量大时,一次性rehash产生阻塞;
- 在rehash期间,会维护一个rehashIndex,用来表示rehash进度,初始值为0,代表rehash进行中;
- 每次迁移完一个槽位,rehashIndex + 1;
- 迁移过程中如果有请求进来,将请求路由到的index与rehashIndex比较,如果大于rehashIndex,代表这部分数据还没有迁移完成,访问ht[0],否则访问ht[1];
- 全部迁移完成后rehashIndex更新为-1。
List的底层实现是什么?
- List的底层实现是quicklist,quicklist是一个与linkedlist类似的双向链表,每个quicklist的节点又包含多个ziplist,这样就结合了两者的优点,既保证一定的查询效率,又节省了一定内存空间。
Set的底层实现是什么?
- set的底层实现为intset或hashtable,如果都是整型数字,则用intset存储,否则用hashtable。
Zset的底层实现是什么?
- 默认使用ziplist存储,如果元素数量大于128或任意元素长度大于等于64byte,则使用skiplist + dict 存储。
什么是skiplist(跳跃表)?
- 每相邻的两个节点都增加了一个指针,让指针指向下下个节点,这样所有新增加的指针连成了一个新的链表,但包含的节点数只有原来的一半。这样当我们查找数据时,先沿着新链表查找,类似于二分查找的思想。
- 例如查找23的位置:
- 23首先和7比较,再和19比较,直到发现比23大的值;
- 当处于19节点时,发现26比23大,则与19的下一节点22比较;
- 22比23要小,下一节点26比23要大,说明要查找的23元素并不在链表内。
- 因为level的值是随机的,skiplist可能不只是跳一层,也可能跳两层、三层等,如下图:
- skiplist查找的时间复杂度为O(Log n);
- 可以在O(1)的时间复杂度下获得skiplist的头节点、尾节点、长度和高度。
持久化
Redis有哪几种持久化方式?
- Redis支持两种方式的持久化,一种是RDB方式、另一种是AOF(append-only-file)方式。前者会根据指定的规则“定时”将内存中的数据存储在硬盘上,而后者在每次执行命令后将命令本身记录下来。两种持久化方式可以单独使用其中一种,也可以将这两种方式结合使用。
AOF文件过大怎么办?
- Redis提供了AOF文件的重写机制,优化掉冗余的命令;
- 重写过程在AOF文件大小达到指定阈值时触发,由子进程执行;
- 子进程并不是基于原有AOF文件进行重新,而是全量遍历当前内存中的数据,写入新的AOF文件,在此期间新的写请求命令会先写到缓冲区aof_rewrite_buf中,最后合并到一起。
内存回收策略
LRU和LFU的区别?
- LRU是最近最少使用页面置换算法(Least Recently Used),也就是首先淘汰最长时间未被使用的页面(按使用时间排序);
- LFU是最近最不常用页面置换算法(Least Frequently Used),也就是淘汰一定时期内被访问次数最少的页(按使用次数排序)。
Redis有哪些种内存回收策略?
- noeviction:默认的策略,如果内存使用达到阈值,所有引起申请内存的命令会报错;
- allkeys-random:添加数据时,如果内存使用达到阈值,就会扫描所有的key,随机淘汰一些key;
- volatile-random:添加数据时,如果内存使用达到阈值,扫描那些设置里过期时间的key,随机淘汰一些key;
- volatile-ttl:添加数据时,如果内存使用达到阈值,扫描那些设置里过期时间的key,淘汰一些即将过期的key(按失效时间排序);
- allkeys-lru:添加数据时,如果内存使用达到阈值,就会扫描所有的key,淘汰一些最近未使用的key(按最近使用时间排序);
- volatile-lru:添加数据时,如果内存使用达到阈值,扫描那些设置里过期时间的key,淘汰一些最近未使用的key(按最近使用时间排序);
- allkeys-lfu:添加数据时,如果内存使用达到阈值,就会扫描所有的key,淘汰一些最近最少使用的key(按最近使用次数排序);
- volatile-lfu:添加数据时,如果内存使用达到阈值,就会淘汰一些设置了过期时间的,并且最近最少使用的key(按最近使用次数排序)。
高可用
Redis有哪些高可用方案?
- 主从、哨兵、集群。
哨兵节点的最少数量是多少?
- 三个,一主两从。
哨兵Leader选举的原理是什么?
- 哨兵的Leader选举是基于共识算法Raft;
- Raft协议动画演示
- Raft
- 参与选举的三种角色:
- Leader:负责事务请求的处理与数据同步;
- Follower:可以处理非事务请求,以及Leader选举;
- Candidate:候选者,在Leader选举期间,Follower会成为候选者;
- 集群总节点数需要满足 2n + 1(n >= 1);
- Leader选举的触发时机:
- 集群初始化;
- Leader宕机;
- 成为Leader的条件:
- 每个Candidate会在一个随机时间后进行广播发起投票,默认会投自己;
- 其它节点接收到投票请求后会判断该节点的term(任期)是否优于自己,如果满足则会同意;
- 当超过半数节点投票同意后该Candidate会成为新的Leader,同时term + 1;
- 如果平票,则在一个超时时间后重新开启投票流程;
- Leader的数据同步:
- Leader 基于2pc实现数据同步,超过半数的Follower 响应成功后数据才算成功写入;
- 如何处理脑裂或网络分区;
- 以节点多的分区数据为准。
哨兵模式与集群模式的区别?
- 哨兵模式是主从模式的升级版,主节点和从节点都保存了全量数据,而集群模式是对整个数据集进行了横向的分片,每个分片节点只保存一部分数据集;
- 集群模式相比哨兵模式而言部署和运维成本更高,适合数据量更大的场景。
Redis集群方案不可用的情况有哪些?
- 集群master半数以上宕机,无论是否有从库存活(无法进行选举);
- 集群某一节点的主从全部宕机,集群进入fail状态。也可以理解成进群的slot映射[0-16383]不完整时进入fail状态。
缓存的经典七大问题
缓存失效
- 在过期时间基础上加上随机时间,避免同时失效。
缓存穿透
- 对于某些DB不存在的值,可以缓存一个特殊值,避免频繁访问DB;
- 构建BloomFilter记录全量数据,这样可以过滤掉不存在的key。
缓存雪崩
- 对DB的访问增加读写开关,当发现 DB 请求变慢、阻塞,慢请求超过阈值时,就会关闭读开关,部分或所有读 DB 的请求进行 failfast 立即返回,待 DB 恢复后再打开读开关。
- 对缓存增加多个副本,缓存异常或请求 miss 后,再读取其他缓存副本,而且多个缓存副本尽量部署在不同机架,从而确保在任何情况下,缓存系统都会正常对外提供服务。
- 对缓存体系进行实时监控,当请求访问的慢速比超过阈值时,及时报警,通过机器替换、服务替换进行及时恢复;也可以通过各种自动故障转移策略,自动关闭异常接口、停止边缘服务、停止部分非核心功能措施,确保在极端场景下,核心功能的正常运行。
缓存一致性问题
- cache 更新失败后,可以进行重试,如果重试失败,则将失败的 key 写入队列机服务,待缓存访问恢复后,将这些 key 从缓存删除。这些 key 在再次被查询时,重新从 DB 加载,从而保证数据的一致性。
- 缓存时间适当调短,让缓存数据及早过期后,然后从 DB 重新加载,确保数据的最终一致性。
- 缓存的更新方案:缓存的三种读写方式_学无止境-CSDN博客_只读缓存 读写缓存
数据并发竞争
- 使用全局锁。即当缓存请求 miss 后,先尝试加全局锁,只有加全局锁成功的线程,才可以到 DB 去加载数据。其他进程/线程在读取缓存数据 miss 时,如果发现这个 key 有全局锁,就进行等待,待之前的线程将数据从 DB 回种到缓存后,再从缓存获取。
- 对缓存数据保持多个备份,即便其中一个备份中的数据过期或被剔除了,还可以访问其他备份,从而减少数据并发竞争的情况。
Hot key
- 对于特殊的业务场景,提前分析收集热门的key;
- 如果热门的key的名称为hotkey,可以将它分散为 hotkey#1、hotkey#2、hotkey#3,……hotkey#n,这 n 个 key 分散存在多个缓存节点,然后 client 端请求时,随机访问其中某个后缀的 hotkey,这样就可以把热 key 的请求打散,避免一个缓存节点过载。
Big key
- 对于value超过阈值的key进行序列化压缩等。
分布式锁
Redisson如何实现可重入锁?
- Redisson是通过lua脚本实现的lock和unlock,底层使用的数据结构是hash;
- hash的key为锁实际的key,hash中会有一个entry,entry的key为传入的唯一threadId,value为重入次数,重入一次会+1,释放一次会-1;
- 每次加锁都会更新失效时间,同时还有定时任务进行失效时间的延长(看门狗);
- lock和unlock是基于Redis的Pub Sub模式实现锁的等待和唤醒的,lock过程如果无法获得锁,当前线程执行subscribe(threadId),订阅该threadId的锁事件,当对应threadId的锁完全释放后,会执行publish发布锁释放事件,这样订阅该事件的所有线程便可以重新竞争锁。
Redis线程模型
Redis 采用单线程为什么还这么快?
- 首先,Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,比如哈希表和跳表;
- 其次,因为是单线程模型避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题;
- 另外,Redis并不是完全的单线程应用,除了核心的读写操作由专门的线程完成之外,还有很多辅助线程在后台执行工作,例如:
- 主从同步;
- 持久化;
- 大key删除:直接通过del删除大key会导致Redis阻塞,例如数量较大的map、list或set等。Redis4.0引入了unlink命令,先标记再异步删除,避免阻塞;
- 除了unlink之外,flushall、flushdb也都是异步操作;
- 最后, Redis 采用了 I/O 多路复用机制,在Linux环境的epoll模式下,可以高效地进行网络通信,因为基于非阻塞的 I/O 模型,就意味着 I/O 的读写流程不再阻塞。