Redis 最常见的问题
Redis 常用的五种数据结构
字符串 String
- 概念:字符串主要用于管理 Redis 字符串值。
- 容量:最大为数据长度是 512M
列表 List
- 概念:列表是简单的字符串列表,按照插入顺序排序,可以从列表的头部或尾部插入一个元素。
- 容量:一个列表最多可以包含 2^32 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。
集合 Set
- 概念:集合是 String 的无序集合,集合元素是唯一的。
- 容量:集合中最大可存储的元素数量为 2^32 - 1 (40 多亿)。
哈希 Hash
- 概念:哈希是一个 String 类型的 field 和 value 的映射表,适合存储对象。
- 容量:每个 Hash 可以存储 2^32 - 1 键值对 (40 多亿)。
有序集合 Sorted Set
- 概念:有序集合也是 String 的集合,但每个元素会关联一个 double 类型的分数 (score) 且集合中的元素是从小到大排序的。
- 容量:有序集合中最大可存储的元素数量为 2^32 - 1 (40 多亿)。
Redis 常见问题
说一说 Redis 哈希槽的概念 ?
Redis
集群没有使用一致性 hash
,而是引入了哈希槽的概念。
Redis
集群中有 16384
个哈希槽,每个 ke
y 通过 CRC16
校验后对 16384
取模来决定存储在那个槽。
集群的每个节点负责一部分 hash
槽。
Redis 为什么选择单线程?
- 多线程会频繁切换上下文消耗大量的 CPU。
- 单线程支持原子操作,不会存在锁竞争问题。
为什么不使用 Redis 的事务?
- 不支持回滚操作,不能满足原子性。
Redis 集群的利弊
好处
- 容量增加,处理能力增强。
- 可按需扩容与缩容。
问题
1、存储数据时如何选择节点、查询数据时如何选择节点 ?
2、新增节点时如何拉取数据、剔除节点时如何转移数据 ?
解决方式
为了解决数据与节点直接的映射关系,Redis 引入了槽。
引入槽之后,节点上放置的是槽,槽当中存储的是数据。
一个集群只能有 16384 个槽,编号为 0 - 16383。这些槽会分配给集群中的所有主节点,分配策略无要求。
可以指定哪些编号的槽分配到哪一个主节点。集群会记录节点和槽的对应关系。
如何计算 Key 在那个槽?
对 Key 求哈希值,然后对 16384 取余,余数的值就是 Key 对应的槽的编号。
即:CRC16 (Key) % 16384
Redis 为什么需要管道技术?
Redis 提供了一种管道技术,可以让客户端一次发送多条命令。
期间不需要等待服务器端的响应,等所有的命令发送完成后,再依次响应。
这样节省了时间,提升了效率。
Redis 分布式锁是什么 ?
- 先拿
setnx
来争抢锁,抢到之后,再用expire
给锁加一个过期时间防止锁忘记了释放。 - 如果在
setnx
之后,执行expire
之前进程意外crash
或重启维护, 那么就需要把setnx
和expire
合成一条指令来用。
假如 Redis 里面有一亿个 key,其中 10 万个 key 是以某个固定的已知前缀开头的,如何将它们全部找出来?
- 使用keys指令可以扫出指定模式的key列表。
- 如果这个redis正在给线上的业务提供服务,那么使用key指令会导致线程阻塞。(redis是单线程的,执行key指令期间,线上服务会卡顿,直到指令执行完成,服务才会恢复)。在这种场景下,就可以使用scan指令,该指令可以无阻塞的提取出指定模式的key列表,但是会有一定重复的概率,可以在客户端做一次去重就好了, 但是整体花费的时间会比直接使用keys指令长。
如何实现 Redis 异步队列 ?
使用 list
结构作为队列,rpush
生产消息,lpop
消费消息。
当 lpop
没有消息的时候,要适当 sleep
一会再重试。
如果不用 sleep
, 还有个指令 blpop
, 在没有消息的时候, 他会阻塞住直到有消息。
如果要生产一次消费多次,则需要使用 pub/sub
主题订阅者模式,可以实现 1:N
的消息队列。
在消费者下线的情况下,生产的消息会丢失。
在这种情况下,就得使用更专业的消息队列,例如 RabbitMQ。
如何实现 Redis 延时队列 ?
使用 SortedSet
,拿时间戳作为 Score
, 消息内容作为 key
调用 zadd
来生产消息,
消费者用 zrangebysocre
指令获取N秒之前的数据轮询进行处理。
如果有大量的 key 需要设置统一时间过期,需要注意什么 ?
如果有大量的 key
过期时间设置过于集中,到过期的那个时间点。
redis
可能会出现短暂的卡顿现象。一般需要在时间生加上一个随机值, 使得过期时间分散一些。
Redis 如何做持久化 ?
bgsave
做镜像全量持久化, aof
做增量持久化。
因为 bgsave
会耗费较长时间, 不够实时, 在停机的时候会导致大量丢失数据, 所以aof来配合使用。
在 redis
实例重启时, 会使用 bgsave
持久化文件重新构建内存, 在使用 aof
重放近期的操作指令来实现完整恢复重启之前的状态。
如果不要求性能, 在每条写指令是都 sync
一下磁盘, 就不会丢失数据。
但是在高性能的要求下每次都 sync
是不现实的, 一般都使用定时 sync
, 比如1s1次, 这个时候最大就会丢失1s的数据。
bgsave
的原理是, fork
和 cow
。fork
是指 redis
通过创建子进程来进行 bgsave
操作。
cow
指的是 copy on write
, 子进程创建后, 父进程通过共享数据段。
父进程继续提供读写服务, 写脏的页面数据会逐渐和子进程分离开来。
Pipeline 有什么好处?
可以将多次 IO
往返的时间缩减为一次, 前提是 pipeline
执行的指令质检没有因果相关性。
使用 redis-benchmark
进行压测的时候可以发现影响 redis
的 QPS
峰值的一个重要因素是 piepline
批次指令的数目。
Redis 的同步机制是如何操作的?
redis
可以使用主从同步, 从从同步。
第一次同步时, 主节点做一次 bgsave
, 并同时将后续修改操作记录到内存buffer。
待完成后将 rdb
文件全量同步到复制节点, 复制节点接受完成后, 将rdb镜像加载到内存。
加载完成后, 再通知主节点将修改期间的操作 记录同步到复制节点进行重放就完成了同步过程。
Redis 集群的原理?
redis sentinal
着眼于高可用, 在 master
宕机时会自动将 slave
提升为 master
, 继续提供服务。
redis cluster
着眼于扩展性, 在单个 redis
内存不足时, 使用 cluster
进行分片存储。
缓存穿透
现象
查询不存在的数据,缓存中没有数据,数据库也没有数据。
因此所有的请求都访问到了数据库,给数据库造成了压力。
解决方法
1、采用布隆过滤器,将所有可能存在的数据,哈希到一个很大的 bitmap 中,
一个一定不存在的数据会被 bitmap 拦截调,从而避免了对数据库的查询压力。
2、如果查询的数据为空,那么直接将空数据也缓存起来并设置较短的过期时间。
这样下次访问的时候,就直接返回空值。
缓存预热
系统上线之后,将需要缓存的数据直接加载到内存。
实现思路:
1、直接写个缓存刷新命令,上线时手动操作。
2、项目启动时自动加载缓存到内存。
3、定时刷新缓存。
Redis 的三种删除策略
定时删除
在设置键的过期时间的同时,创建一个定时任务。当键达到过期时间时,立即执行对键的删除操作。
- 优点
对内存友好,定时删除策略可以保证过期键会尽可能的快被删除,并且释放过期键所占用的内存。
- 缺点
对 CPU 时间片不友好,在过期键比较多的情况下。删除任务会占用很大一部分 CPU 时间片,在内存不紧张但是 CPU 时间紧张时.将 CPU 事件用在删除和当前任务无关的过期键上,会影响服务器的响应时间和吞吐量。
惰性删除
- 优点
对 cpu 时间友好,在每次从键空间获取键时进行过期键检查并是否删除,删除目标也仅限当前处理的键,这个策略不会在其他无关的删除任务上花费任何 cpu 时间。
- 缺点
对内存不友好,过期键过期也可能不会被删除,导致所占的内存也不会释放。甚至可能会出现内存泄露的现象,当存在很多过期键,而这些过期键又没有被访问到,这会可能导致它们会一直保存在内存中,造成内存泄露。
定期删除
由于定时删除会占用太多的 CPU 时间片,影响服务器的响应时间和吞吐量。并且惰性删除浪费太多的内存,有内存泄漏的风险。因此定期删除策略是这两策略的折衷策略。
- 优点
定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。
定时删除策略有效地减少了因为过期键带来的内存浪费。
什么是缓存穿透?
缓存穿透是指缓存和数据库中都没有数据的情况下,客户端不断发起请求,导致数据库压力过大。
什么是缓存击穿?
缓存击穿是指缓存过期之后,瞬时间并发客户端特别多查询同一条数据的情况下,导致数据库压力过大。
什么是缓存雪崩?
缓存雪崩是指缓存中大量的不同数据同时过期,此时查询大量的数据,导致数据库压力过大。