Redis 一些面试题
1. 你在最近的项目中哪些场景使用了 Redis 呢?
具体的项目与图片上的使用场景内容结合回答
2. 谈谈你对 Redis 的理解?
- Redis(Remote Dictionary Server) 是一个使用 C 语言编写的,开源的(BSD 许可)高性能非关系型 (NoSQL)的键值对(Key-Value 存储结构)数据库。
- 它提供了 5 种常用的数据类型,String、Map、Set、ZSet、List。针对不同的结构,可以解决应用开发中不同场景的问题。
- 其次,由于 Redis 是基于内存存储,并且在数据结构上做了大量的优化,所以读写性能比较好。所以在实际开发中,会把它作为应用与数据库之间的一个分布式缓存组件,也经常用来做分布式锁。除此之外,Redis 支持事务 、持久化、LUA 脚本等。
- 还有就是它提供了主从复制 + 哨兵、以及集群方式实现高可用。在 Redis 集群里面,通过 hash 槽的方式实现了数据分片,进一步提升了性能。
3. Redis 优缺点?
优点:
- 基于内存操作,内存读写速度快。
- 支持多种数据类型,包括 String、Hash、List、Set、ZSet 等。
- 支持持久化。Redis 支持 RDB 和 AOF 两种持久化机制,持久化功能可以有效地避免数据丢失问题。
- 支持事务。Redis 的所有操作都是原子性的,同时 Redis 还支持对几个操作合并后的原子性执行。
- 支持主从复制。主节点会自动将数据同步到从节点,可以进行读写分离。
- Redis 命令的处理是单线程的。Redis6.0 引入了多线程,需要注意的是,多线程用于处理网络数据的读写和协议解析,Redis 命令执行还是单线程的。
缺点:
- 对结构化查询的支持比较差。
- 数据库容量受到物理内存的限制,不适合用作海量数据的高性能读写,因此 Redis 适合的场景主要局限在较小数据量的操作。
- Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。
4. 为什么要用 Redis 而不用 map/guava 做缓存?
- 缓存分为本地缓存和分布式缓存。
- 以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 JVM 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
- 使用 Redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 Redis 或 memcached 服务的高可用,整个程序架构上较为复杂。
5. Redis 为什么那么快?
- 完全基于内存的,C 语言编写
- 采用单线程,避免不必要的上下文切换可竞争条件,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
- 使用多路 I/O 复用模型,非阻塞 IO,例如:
bgsave
和bgrewriteaof
都是在后台执行操作,不影响主线程的正常使用,不会产生阻塞
6. Redis 数据类型有哪些?
Redis 支持 5 中基本数据类型:
- String:最常用的一种数据类型,String 类型的值可以是字符串、数字或者二进制,但值最大不能超过 512MB。可以用于存储计数器数据、短信验证码,配置信息、token 等。
- Hash:一个键值对集合。可以用于存储用户、商品信息等对象数据。
- Set:无序去重的集合。Set 提供了交集、并集等方法,对于实现共同好友、共同关注等功能特别方便。
- SortedSet:有序去重的集合,增加了一个 score 参数,自动会根据 score 的值进行排序。适用于排行榜和带权重的消息队列等场景。
- List:有序可重复的集合,底层是依赖双向链表实现的。比较适合存储一些有序且数据相对固定的数据。如省市区表、字典表、根据写入的时间来排序的列表数据等。
除了这 5 种基本数据类型,Redis 还支持一些高级的数据类型,如 HyperLogLog、Bitmap、GEO 等。这些数据类型通常用于特定的应用场景,如计数、布隆过滤器、地理位置等。
7. 能解释一下 I/O 多路复用模型?
I/O 多路复用是指利用单个线程来同时监听多个 Socket ,并在某个 Socket 可读、可写时得到通知,从而避免无效的等待,充分利用 CPU 资源。目前的 I/O 多路复用都是采用的 epoll 模式实现,它会在通知用户进程 Socket 就绪的同时,把已就绪的 Socket 写入用户空间,不需要挨个遍历 Socket 来判断是否就绪,提升了性能
其中 Redis 的网络模型就是使用 I/O 多路复用结合事件的处理器来应对多个 Socket 请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器
在 Redis6.0 之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程
8. 什么是缓存穿透 ? 怎么解决 ?
缓存穿透是指查询一个一定不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB(DataBase 简称,后面都会使用 DB) 去查询,如果请求的数据量过大或者是有人利用不存在的 key 频繁攻击我们的应用时,可能导致 DB 挂掉。
解决方案一:缓存空数据
如果查询返回的数据为空,仍把这个空结果进行缓存,并给它设置一个较短的过期时间。{key:1,value:null}
,优点是简单,缺点是消耗内存,还可能会发生不一致的问题(比如当在 Redis 里面缓存了 key 为 1 值为 null 的情况,后面数据库更新,key 为 1 的有值了,这样 key 为 1 的数据库和缓存的值出现不一致的问题了)
解决方案二:使用布隆过滤器
在应用程序启动进行缓存预热时,预热布隆过滤器,当去查询数据时,先去查询布隆过滤器有没有这个 key,如果布隆过滤中存在,那么去查 Redis,如果不存在,直接返回。优点是内存占用较少,没有多余 key,缺点是实现复杂,存在误判。
9. 能介绍一下布隆过滤器吗?
具体回答:
- 布隆过滤器主要是用于检索一个元素是否在一个集合中。我们当时使用的是 Redisson 实现的布隆过滤器。
- 它的底层主要是先去初始化一个比较大数组,里面存放的二进制 0 或 1。在一开始都是 0,当一个 key 来了之后经过 3 次 hash 计算,模于数组长度找到数据的下标然后把数组中原来的 0 改为 1,这样的话,三个数组的位置就能标明一个 key 的存在。查找的过程也是一样的。
- 当然是有缺点的,布隆过滤器有可能会产生一定的误判,我们一般可以设置这个误判率,大概不会超过 5%,其实这个误判是必然存在的,要不就得增加数组的长度,其实已经算是很划分了,5% 以内的误判率一般的项目也能接受,不至于高并发下压倒数据库。
解析:
1)正常布隆过滤器流程
2)发生误判的情况:当去查询 id3 这个不存在的值时,经过布隆过滤器 hash 计算是存在的,因为数据中查询 id3 那些数据位置存在的值其实是 id1 和 id2 存储数据是放进去的
3)简易代码实现
//是用的这个包下面的类
import org.Redisson.api.RBloomFilter;
/**
* 测试误判率
*
* @param bloomFilter
* @param size
* @return int
*/
private static int getData(RBloomFilter<String> bloomFilter, int size) {
// 记录误判的数据条数
int count = 0;
for (int x = size; x < size * 2; x++) {
if (bloomFilter.contains("add" + x)) {
count++;
}
}
return count;
}
/**
* 初始化数据
*
* @param bloomFilter
* @param size
*/
private static void initData(RBloomFilter<String> bloomFilter, int size) {
//第一个参数: 布隆过滤器存储的元素个数,第二个参数: 误判率
bloomFilter.tryInit(size, 0.05);
//在布隆过滤器初始化数据
for (int x = 0; x < size; x++) {
bloomFilter.add("add" + x);
}
System.out.println("初始化完成...");
}
10. 什么是缓存预热?
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
实现方案
- 自己写个接口,项目上线后自己手动调用一下
- 数据量不大时,可以在项目启动的时候,调用初始化方法加载缓存
- 写一个定时任务,加载数据到缓存中
11. 什么是缓存降级?
在访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,为了保证主服务的可用性,将次要服务的数据进行缓存降级,以此来提升主服务的稳定性。
通常采取的做法是不去访问数据库,而直接返回默认数据给用户。
12. 什么是缓存击穿 ? 怎么解决 ?
具体回答:
缓存击穿的意思是对于设置了过期时间的 key,缓存在某个时间点过期的时候,恰好这时间点对这个 Key 有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。
对于这个的解决方案
- 设置缓存中的 key 不过期
- 可以使用互斥锁。
- 当缓存失效时,不立即去请求数据库,可以使用 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行请求数据库的操作并回设缓存,否则重试 get 缓存的方法。
- 这是数据的强一致性的解决方案,性能上可能没那么高,因为锁需要等待。
- 可以设置当前 key 逻辑过期,大概是思路如下:
- 在设置 key 的时候,设置一个过期时间字段一块存入缓存中,不给当前 key 设置过期时间
- 当查询的时候,从 Redis 取出数据后判断时间是否过期
- 如果过期则启动另外一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新
- 这是优先考虑的高可用性的解决方案,性能比较高,但是数据同步这块做不到强一致。
- 具体使用那个解决方案可以按不同具体的业务场景选择合适的方案。
解析:
13. 什么是缓存雪崩 ? 怎么解决 ?
缓存雪崩意思是设置缓存时采用了相同的过期时间或者是 Redis 服务宕机,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多 key,击穿是某一个 key 缓存。
解决方案
对于缓存时采用了相同的过期时间的问题可以将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
对于 Redis 服务宕机的问题,我们可以
- 利用 Redis 集群提高服务的可用性,比如使用 Redis 的哨兵模式、集群模式;
- 给缓存业务添加降级限流策略(穿透、击穿、雪崩都可以使用的策略),比如使用 nginx 或 spring cloud gateway 网关中设置限流规则
- 给业务添加多级缓存,比如除了使用 Redis 分布式缓存外,还可以使用 Map 或者 Guava 设置一层本地缓存
14. Redis 和 MySQL 的数据如何保持一致性呢?
具体回答:
这个要根据业务场景是否要保证数据库与 Redis 高度保持一致有不同的解决方案
如果说业务不是为了性能和高可用性不是很需要保证数据库与 Redis 高度一致性(比如要把文章的热点数据存入到了缓存中),允许延迟一致性,那我们可以采用
- 延迟双删策略,先删除 Redis,再更新数据库,延迟一段时间后再删除一次 Redis。这个延迟的时间要大于请求将数据库旧数据写入 Redis 的时间。这方法如果删除时间把握不当可能会出现脏数据,所以这个方案用的不多。
- 异步通知保证数据的最终一致性,当我们把数据写入到数据库后,可以发一条消息给 MQ,MQ 消费这条消息,去执行更新 Redis 的缓存的代码。
- 采用阿里的 canal 组件实现两者数据同步,不需要更改业务代码,部署一个 canal 服务。canal 服务把自己伪装成 mysql 的一个从节点,当 mysql 数据更新以后,canal 会读取 binlog 数据,然后在通过 canal 的客户端获取到数据,更新缓存即可。
如果说业务要保证数据库与 Redis 高度一致性,要求时效性比较高(比如把抢券的库存存入到了缓存中,这个需要实时的进行数据同步),我们可以采用读写锁保证的两者强一致性。
- 比如采用 Redisson 实现的读写锁,在读的时候添加共享锁,可以保证读读不互斥,读写互斥。
- 当我们更新数据的时候,添加排他锁,它是读写,读读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。
- 这里面需要注意的是读方法和写方法上需要使用同一把锁才行。
解析:
强一制性读写锁的代码简单实现
/**
* 读锁操作
*
* @param id
* @return
*/
public Item getById(Integer id) {
RReadWriteLock readWriteLock = RedissonClient.getReadWriteLock("READ_WRITE_LOCK");
//读之前加读锁,读锁的作用就是等待该lockkey释放写锁以后再读
RLock readLock = readWriteLock.readLock();
try {
//上锁
readLock.lock();
System.out.println("readLock...");
Item item = (Item) RedisTemplate.opsForValue().get("item:" + id);
if (item != null) {
return item;
}
//查询业务数据
item = new Item(id, "小手机");
//写入缓存
RedisTemplate.opsForValue().set("item:" + id, item);
//返回数据
return item;
} finally {
//释放锁
readLock.unlock();
}
}
/**
* 写锁操作
*
* @param id
*/
public void updateById(Integer id) {
RReadWriteLock readWriteLock = RedissonClient.getReadWriteLock("READ_WRITE_LOCK");
//写之前加写锁,写锁加锁成功,读锁只能等待
RLock writeLock = readWriteLock.writeLock();
try {
//上锁
writeLock.lock();
System.out.print("writeLock...");
// 更新业务数据
Item item = new Item(id, "大手机");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//删除缓存
RedisTemplate.delete("item:" + id);
} finally {
//释放锁
writeLock.unlock();
}
}
15. Redis 的持久化是怎么做的,持久化方式有什么区别?
- 持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。在 Redis 中提供了两种数据持久化的方式:第一个是 RDB,第二个是 AOF。
- RDB 是 Redis 默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,实现持久化,RDB 触发的方式有很多
- 执行 bgsave 命令开启子线程触发异步快照,执行 save 命令触发同步快照,同步快照会阻塞客户端的所有命令
- 也可以根据
redis.conf
文件里面的配置,自动触发 bgsave 命令(例如,想要 900 秒内,如果至少有 1 个 key 被修改,则执行 bgsave,则配置这么写save 900 1
) - 第三个是主从复制的时候触发
- AOF 是通过命令追加的方式实现持久化。当 Redis 操作写命令的时候,都会存储这个文件中,当 Redis 实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据
- AOF 默认是关闭的,需要修改
redis.conf
配置文件来开启 AOF(命令是appendonly yes
) - AOF 的命令记录的频率也可以通过
redis.conf
文件来配置,配置项有三个,Always 同步刷盘、everysec 每秒刷盘、no 由操作系统决定何时刷盘(命令是appendfsync always/everysec/no
) - 为了避免 aof 文件过大的问题,Redis 提供了 aof 重写的机制,当 aof 文件的大小达到某一个阈值(阈值也可以在
redis.conf
中配置,第一个可以配置超过多少百分比进行重写,比如超过 100% 重写,那么命令是auto-aof-rewrite-percentage 100
,第二个可以配置文件体积最小多大以上才触发重写,比如文件大小大于 64MB 重写,那么命令是auto-aof-rewrite-min-size 64mb
)的时候,会执行bgrewriteaof
命令把这个文件里面的相同的指令进行压缩,以最少的命令达到相同的效果
- AOF 默认是关闭的,需要修改
- RDB 是每隔一段时间触发持久化,AOF 可以做到实时持久化,所以数据安全性上 AOF 要比 RDB 更高
- RDB 文件采用的压缩方式持久化,AOF 存储的是执行指令,所以 RDB 文件要比 AOF 文件更小且 RDB 在宕机或重启时数据恢复的速度上要比 AOF 更好
16. RDB 的异步同步执行原理?
bgsave 开始时会 fork 主进程得到子进程,子进程共享主进程的内存数据。完成 fork 后读取内存数据并写入 RDB 文件。
fork 采用的是 copy-on-write
技术:
- 当主进程执行读操作时,访问共享内存;
- 当主进程执行写操作时,则会拷贝一份数据,执行写操作。
17. 如何选择合适的持久化方式?
- 如果只希望数据在服务器运行的时候存在,可以不使用任何持久化方式。
- 如果可以承受数分钟以内的数据丢失,可以只使用 RDB 持久化方式。
- 如果想达到很高的数据安全性,应该同时使用两种持久化方式,使用 RDB 持久化方式用于数据备份,使用 AOF 方式保证当前数据的安全性。在这种情况下,当 Redis 重启的时候会优先载入 AOF 文件来恢复原始的数据。
18. Redis 的过期键的删除策略?
- 在 Redis 中提供了三种数据过期删除策略
- 第一种是定时删除,在设置了 key 的过期时间的同时,Redis 创建了一个定时器,当 key 到达过期时间时,立即执行对该 key 的删除操作。这种方法的优点是可以及时清理掉过期的 key,节省内存;但缺点是当过期 key 比较多时,删除过期 key 会对 CPU 造成较大压力,影响服务器性能。
- 第二种是惰性删除,当 key 被访问时,Redis 才会检查键是否已经过期,如果过期,则删除该 key。这种方式的优点是不会频繁地对服务器 CPU 造成压力;缺点是在 key 访问频次低时,可能会导致内存中的过期 key 堆积,对内存很不友好。
- 第三种是定期删除,Redis 从一定数量的数据库中取出一定数量的随机 key 进行检查,并删除其中的过期 key,定期清理的有两种模式
- SLOW 模式是定时任务,执行频率默认为 10hz,每次不超过 25ms,可以通过修改配置文件
redis.conf
的 hz 选项来调整这个次数 - FAST 模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于 2ms,每次耗时不超过 1ms
- 优点是可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。缺点是难以确定删除操作执行的时长和频率。
- SLOW 模式是定时任务,执行频率默认为 10hz,每次不超过 25ms,可以通过修改配置文件
- Redis 在实际应用中是惰性删除 + 定期删除两种策略进行配合使用的。
19. Redis 的数据淘汰策略有哪些 ?(Redis 的内存用完了会发生什么?)
具体回答:
当 Redis 中的内存不够用时,此时在向 Redis 中添加新的 key,那么 Redis 就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。Redis 支持 8 种不同策略来选择要删除的 key:
- 第一种是
noeviction
,这是 Redis 的默认策略,不淘汰任何 key,但是内存满时不允许写入新数据,并返回客户端错误消息(错误信息是(error)OOM command not allowed when used memory
)。 - 第二种是
allkeys-lru
,对所有的 key 使用最近最少使用算法(LRU)进行淘汰 - 第三种是
allkeys-lfu
,对所有 key 中使用最不常用算法(LFU)进行淘汰 - 第四种是
allkeys-random
,对所有 key 中随机淘汰数据 - 第五种是
volatile-lru
,对设置了过期时间的 key 使用最近最少使用算法(LRU)进行淘汰 - 第六种是
volatile-lfu
,对设置了过期时间的 key 使用最不常用算法(LFU)进行淘汰 - 第七种是
volatile-random
,对设置了过期时间的 key 中随机淘汰数据;
volatile-ttl: 在设置了过期时间的 key 中,淘汰过期时间剩余最短的。 - 第八种是
volatile-ttl
,对在设置了过期时间的 key 中,淘汰过期时间剩余最短的。
当使用 volatile-lru
、volatile-lfu
、volatile-random
、volatile-ttl
这四种淘汰策略时,如果没有 key 可以淘汰,则和 neoviction
一样返回错误。
这些策略可以在 redis.conf
文件(文件中的配置是 maxmemory-policy noeviction
)中手动配置和修改,我们可以根据缓存的类型和缓存使用的场景来选择合适的淘汰策略。
解析:
LRU(Least Recently Used)
最近最少使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。比如,key1 是在 3s 之前访问的, key2 是在 9s 之前访问的,删除的就是 key2。
LFU(Least Frequently Used)
最少频率使用。会统计每个 key 的访问频率,值越小淘汰优先级越高。比如,key1 最近 5s 访问了 4 次, key2 最近 5s 访问了 9 次, 删除的就是 key1。
20. 如何选择使用哪种内存淘汰策略?(Redis 的数据淘汰策略的使用策略)
- 优先使用
allkeys-lru
策略。充分利用最近最少算法的优势,把最近最常访问的数据留在缓存中。如果业务有明显的冷热数据区分,建议使用。(如果被问到数据库有 1000 万数据 ,Redis 只能缓存 20w 数据, 如何保证 Redis 中的数据都是热点数据? 就回答这个) - 如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用
allkeys-random
策略,随机选择淘汰。 - 如果业务中有置顶的需求,可以使用
volatile-lru
策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据。 - 如果业务中有短时高频访问的数据,可以使用
allkeys-lfu
或volatile-lfu
策略。 - 在实际使用中可以使用 Redis 的 info 命令输出,监控缓存命中的次数(keyspace_hits)和没有命中的次数(keyspace_misses)通过计算缓存命中率(
keyspace_hits / (keyspace_hits + keyspace_misses)
) ,来调整内存淘汰策略。
21. Redis 分布式锁如何实现 ?
具体回答:
- 在 Redis 中提供了一个命令
setnx(SET if not exists)
,设置成功返回 1 ,设置失败返回 0 。由于 Redis 的单线程的,用了命令之后,只能有一个客户端对某一个 key 设置值,在没有过期或删除 key 的时候是其他客户端是不能设置这个 key 的 - 但是用 Redis 的 setnx 指令来做分布式锁不好控制这个锁的有效时长
- 所以在实际开发中通常使用 Redis 的一个框架 Redisson 来实现分布式锁的
- 在 Redisson 中需要手动加锁,并且可以控制锁的失效时间和等待时间。
- 当锁住的一个业务还没有执行完成的时候,在 Redisson 中引入了一个 watchDog 看门狗机制,就是说每隔一段时间就检查当前业务是否还持有锁,如果持有就增加加锁的持有时间,默认是每隔 10 秒续期一次,当业务执行完成之后需要使用释放锁就可以了。
- Redisson 里面的加锁、设置过期时间等操作都是基于 lua 脚本完成,保证了执行的原子性。
- 使用 Redisson 还有一个好处就是,在高并发下,一个业务有可能会执行很快,线程 1 持有锁的时候,线程 2 来了以后并不会马上拒绝,它会自旋不断尝试获取锁,如果线程 1 释放之后,线程 2 就可以马上持有锁,性能也得到了提升。
解析:
Redisson 分布式锁执行流程
Redisson 实现分布式锁代码
public void RedisLock() throws InterruptedException {
//获取锁(重入锁),执行锁的名称
RLock lock = RedissonClient.getLock("lock");
try {
//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
//boolean isLock = Lock.trylock(10,30,TimeUnit.SECONDS);
boolean isLock = lock.tryLock(10, TimeUnit.SECONDS);
//判断是否获取成功
if (isLock) {
System.out.print("执行业务...");
}
} finally {
//释放锁
lock.unlock();
}
}
22. Redisson 实现的分布式锁是可重入的吗?
- Redisson 实现的分布式锁是可以重入的。这样做是为了避免死锁的产生。
- 这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计算上减一。
- 在存储数据的时候采用的 hash 结构,大 key 可以按照自己的业务进行定制,其中小 key 是当前线程的唯一标识,value 是当前线程重入的次数
23. Redisson 实现的分布式锁能解决主从一致性的问题吗?
这个是不能的,比如,当线程 1 加锁成功后,Master 节点数据会异步复制到 Slave 节点,此时当前持有 Redis 锁的 Master 节点宕机,Slave 节点被提升为新的 Master 节点,假如现在来了一个线程 2,再次加锁,会在新的 Master 节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问题。
我们可以利用 Redisson 提供的 RedLock 红锁来解决这个问题,它的主要作用是,不能只在一个 Redis 实例上创建锁,应该是在多个 Redis 实例上创建锁(n/2+1
个实例上创建锁),并且要求在大多数 Redis 节点上都成功创建锁,红锁中要求是 Redis 的节点数量要过半。这样就能避免线程 1 加锁成功后 Master 节点宕机导致线程 2 成功加锁到新的 Master 节点上的问题了。
但是如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变的很低了,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁。
Redis 集群整体是 AP 思想,主要是实现高可用,要做到数据的强一致性,就会非常影响性能,如果业务一定要保证数据的强一致性,可以使用 CP 思想的 zookeeper 实现的分布式锁,它是可以保证数据的强一致性。
24. Redis 集群有哪些方案, 知道嘛 ?
在 Redis 中提供的集群方案总共有三种,分别为:主从复制、哨兵模式、Redis Cluster 分片集群,因为在 Redis 3.0 版本之前只支持单实例模式,Redis 3.0 之后才支持了集群方式,所以在 Redis 3.0 之前各大厂为了解决单实例 Redis 的存储瓶颈问题各自推出了自己的集群方案,比如 Twemproxy 代理方案、Codis 等。
主从复制:单节点 Redis 的并发能力是有上限的,要进一步提高 Redis 的并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中。
哨兵模式: 主从复制不能保证 Master 节点的高可用性,哨兵模式可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障恢复、通知;如果 Master 故障,Sentinel(哨兵)会将一个 Slave 提升为 Master。当故障实例恢复后也以新的 Master 为主;同时 Sentinel 也充当 Redis 客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给 Redis 的客户端。(这个可以回答怎么保证 Redis 的高并发高可用? 下面单独列了这个问题)
分片集群: 集群中有多个 Master,每个 Master 保存不同数据,并且还可以给每个 Master 设置多个 Slave 节点,就可以继续增大集群的高并发能力。同时每个 Master 之间通过 ping 监测彼此健康状态,就类似于哨兵模式了。当客户端请求可以访问集群任意节点,最终都会被转发到正确节点。分片集群主要解决的是,海量数据存储和高并发写的问题(回答分片集群有什么用? )
拓展:
Twemproxy:它类似于一个代理方式,使用方法和普通 Redis 无任何区别,设置好它下属的多个 Redis 实例后,使用时在本需要连接 Redis 的地方改为连接 twemproxy,它会以一个代理的身份接收请求并使用一致性 hash 算法,将请求转 接到具体 Redis,将结果再返回 twemproxy。使用方式简便(相对 Redis 只需修改连接端口),对旧项目扩展的首选。 问题:twemproxy 自身单端口实例的压力,使用一致性 hash 后,对 Redis 节点数量改变时候的计算值的改变,数据无法自动移动到新的节点。
Codis:基本和 twemproxy 一致的效果,但它支持在节点数量改变情况下,旧节点数据可恢复到新 hash 节点。
25. Redis 主从同步数据的流程?(Redis 的主从同步有几种?)
- 主从同步分为了两个阶段,一个是全量同步,一个是增量同步
- 全量同步是指从节点第一次与主节点建立连接的时候使用全量同步,流程是这样的:
- 第一,从节点执行
replicaof
命令请求主节点同步数据,其中从节点会携带自己的replication id
和offset
偏移量。 - 第二,主节点判断是否是第一次请求,主要判断的依据就是,主节点与从节点是否是同一个
replication id
,如果不是,就说明是第一次同步,那主节点就会把自己的replication id
和offset
发送给从节点,让从节点与主节点的信息保持一致。 - 第三,在同时主节点会执行 bgsave,生成 rdb 文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的 rdb 文件,这样就保持了一致
- 当然,如果在 rdb 文件生成执行期间,依然有请求到了主节点,而主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件(repl_baklog 日志),最后把这个日志文件发送给从节点,这样就能保证主节点与从节点完全一致了,后期再同步数据的时候,都是依赖于这个日志文件,这个就是全量同步
- 第一,从节点执行
- 增量同步指的是,当从节点服务重启或者是后期数据变化之后,数据就不一致了,所以这个时候,从节点会请求主节点同步数据,主节点还是判断不是第一次请求,不是第一次就获取从节点的
offset
值,然后主节点从命令日志中获取offset
值之后的数据,发送给从节点进行数据同步
解析:
Replication Id
:简称 replid,是数据集的标记,id 一致则说明是同一数据集。每一个 Master 都有唯一的 replid,Slave 则会继承 Master 节点的 replid。
offset
:偏移量,随着记录在 repl_baklog 中的数据增多而逐渐增大。Slave 完成同步时也会记录当前同步的 offset。如果 Slave 的 offset 小于 Master 的 offset,说明 Slave 数据落后于 Master,需要更新。
26. 怎么保证 Redis 的高并发高可用?
首先可以搭建主从集群,再加上使用 Redis 中的哨兵模式,哨兵模式可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障恢复、通知;如果 Master 故障,Sentinel 会将一个 Slave 提升为 Master。当故障实例恢复后也以新的 Master 为主;同时 Sentinel 也充当 Redis 客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给 Redis 的客户端,所以一般项目都会采用哨兵的模式来保证 Redis 的高并发高可用。
27. Redis 哨兵的作用?
- 监控:Sentinel 会不断检查您的 Master 和 Slave 是否按预期工作
- 自动故障恢复:如果 Master 故障,Sentinel 会将一个 Slave 提升为 Master。当故障实例恢复后也以新的 Master 为主
- 通知:Sentinel 充当 Redis 客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给 Redis 的客户端
28. Redis 哨兵检测 Master 节点是否宕机过程?
- Sentinel 基于心跳检测,默认每隔 1 秒向集群的每个实例发送 ping 命令。如果某 Sentinel 节点发现某实例未在规定时间响应,则认为该实例主观下线。
- 当 Sentinel 认为主节点已宕机,它会将此信息广播给其他 Sentinel 并进入主观下线状态。其他 Sentinel 收到主观下线的消息后,会重新检测主节点的状态。
- 若超过指定数量(quorum,可以设置,quorum 值最好超过 Sentinel 实例数量的一半)的 Sentinel 都认为该实例主观下线,则该实例客观下线。
- 此时,Sentinel 将自动开始故障转移,选取出新的 Master 节点。
29. Redis 中的哨兵选举算法
- 当 Redis 集群中的 Master 节点出现故障,哨兵节点检测到以后,会从 Redis 集群中的其他 Slave 节点选举出一个作为新的 Master。具体有两个部分:第一部分是筛选,第二部分是综合评估
- 在筛选阶段,会过滤掉没有回复 Sentinel 哨兵心跳响应的 Slave 节点。同时判断主节点与从节点断开时间长短,如超过指定值就排该从节点。经过筛选后,留下的都是健康的节点
- 接下来就对健康节点进行综合评估,具体有三个维度,按照顺序来判断
- 根据 Slave 优先级来判断,通过
slave-priority
配置项(redis.conf),可以给不同的从节点设置不同优先级,值越小优先级越高,优先级高的优先成为 Master。 - 如果
slave-priority
一样,则判断 Slave 节点的 offset 值,越大优先级越高。 - 最后是判断 Slave 节点的 runID 大小,越小优先级越高。
- 根据 Slave 优先级来判断,通过
- 经过以上步骤,就可以选举出新的 Master 节点了。
30. Redis 集群脑裂,该怎么解决呢?
这个在项目很少见,不过脑裂的问题是这样的,我们现在用的是 Redis 的哨兵模式集群的
有的时候由于网络等原因可能会出现脑裂的情况,就是说,由于 Redis Master 节点和 Redis salve 节点和 Sentinel 处于不同的网络分区,使得 Sentinel 没有能够心跳感知到 Master,所以通过选举的方式提升了一个 salve 为 Master,这样就存在了两个 Master,就像大脑分裂了一样,这样会导致客户端还在 old Master 那里写入数据,新节点无法同步数据,当网络恢复后,Sentinel 会将 old Master 降为 salve,这时再从新 Master 同步数据,这会导致 old Master 中的大量数据丢失。
关于解决的话,我记得在 Redis 的配置中可以设置:第一可以设置最少的 salve 节点个数,比如设置至少要有一个从节点才能同步数据(min-replicas-to-write 1
表示最少的 salve 节点为 1 个),第二个可以设置主从数据复制和同步的延迟时间(min-replicas-max-lag 5
表示数据复制和同步的延迟不能超过 5 秒),达不到要求就拒绝请求,就可以避免大量的数据丢失
31. Redis 分片集群中数据是怎么存储和读取的?
在 Redis 集群中引入了哈希槽的概念,有 16384 个哈希槽,集群中每个主节点绑定了一定范围的哈希槽范围, key 根据有效部分(有效部分,如果 key 前面有大括号,大括号的内容就是有效部分,如果没有,则以 key 本身做为有效部分)通过 CRC16 校验后对 16384 取模来决定放置哪个槽,通过槽找到对应的节点进行存储。取值的逻辑也是一样的
32. 你们使用 Redis 是单点还是集群,哪种集群?
我们当时使用的是主从(1 主 1 从)加哨兵。一般单节点不超过 10G 内存,如果 Redis 内存不足则可以给不同服务分配独立的 Redis 主从节点。尽量不做分片集群。因为集群维护起来比较麻烦,并且集群之间的心跳检测和数据通信会消耗大量的网络带宽,也没有办法使用 lua 脚本和事务
33. 讲一下 Redis 的事务?
- Redis 中的事务是一组命令的集合,这组命令要么都执行,要不都不执行。
- Redis 事务相关命令包括 MULTI、EXEC、DISCARD 和 WATCH。
- MULTI 命令用于开启事务,将后续的所有指定均加入到事务中。
- EXEC 命令用于执行事务中的所有操作命令。
- DISCARD 命令用于取消事务,放弃执行事务块中的所有命令。
- WATCH 命令用于监视一个或多个 key,如果事务在执行前,这个 key(或多个 key)被其他命令修改,则事务被中断,不会执行事务中的任何命令。UNWATCH 用于取消 WATCH 对所有 key 的监视。
- Redis 事务的执行过程可以分为三个步骤:
- 使用MULTI命令开启一个事务,服务器返回 OK 表示事务开始成功。
- 在事务开启后,可以执行多个操作命令,这些命令会被放入一个队列中。每次输入一个命令服务器并不会马上执行,而是返回”QUEUED”
- 使用EXEC 命令提交事务,此时 Redis 会按照队列中的顺序依次执行这些命令。
- 在事务执行过程中,会按照顺序串行的执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中,也就保证了隔离性。
- 因为 Redis 是单线程的,所以 Redis 单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。如果先保证事务的原子性和支持回滚,可以使用 Redis 的 lua 脚本功能来实现
34. Redis 常见性能问题和解决方案?
- Master 最好不要做任何持久化工作,包括内存快照和 AOF 日志文件,特别是不要启用内存快照做持久化。
- 如果数据比较关键,某个 Slave 开启 AOF 备份数据,策略为每秒同步一次。
- 为了主从复制的速度和连接的稳定性,Slave 和 Master 最好在同一个局域网内。
- 尽量避免在压力较大的主库上增加从库。
- Master 调用 BGREWRITEAOF 重写 AOF 文件,AOF 在重写的时候会占大量的 CPU 和内存资源,导致服务 load 过高,出现短暂服务暂停现象。
- 为了 Master 的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关系为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现 Slave 对 Master 的替换,也即,如果 Master 挂了,可以立马启用 Slave1 做 Master,其他不变。
35. Redis 怎么实现消息队列?
- 使用 list 类型保存数据信息,rpush 生产消息,lpop 消费消息,当 lpop 没有消息时,可以 sleep 一段时间,然后再检查有没有信息,如果不想 sleep 的话,可以使用 blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。
- Redis 可以通过 pub/sub 主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。
36. 怎么使用 Redis 实现一个延时队列?
在 Redis 里面可以使用 Zset 这个有序集合来实现延时队列。
具体的实现方式可以分成几个步骤。
1)使用 ZADD 命令把消息添加到 sorted set 中,并将当前时间作为 score(分数)。
ZADD delay-queue <timestamp> <message>
2)启动一个消费者线程,使用 ZRANGEBYSCORE 命令获取定时从 Zset 中获取当前时间之前的所有消息。
ZRANGEBYSCORE delay-queue 0 <current_time> WITHSCORES LIMIT 0 <batch_size>
3)消费者处理完消息后,可以从有序集合中删除这些消息。
ZREMRANGEBYSCORE delay-queue 0 <current_time>
这种方式实现的延迟队列,消费端需要不断的向 Redis 发起轮询,所以它会存在两个问题:
- 轮询存在时间间隔,所以延时消息的实际消费时间会大于设定的时间
- 大量轮询会对 Redis 服务器造成压力
37. 讲讲 Redis 的线程模型?
Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为 4 部分:多个套接字、IO 多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以 Redis 才叫单线程模型。
- 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
- 当被监听的套接字准备好执行连接 accept、read、write、close 等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。