面试题
为什么要用 Redis,业务中哪块用到的?
Redis因为是基于缓存的,它的处理能力远高于数据库。我们将程序中用到的一些热点数据存放到redis中,来减轻数据库的压力。Redis还可以用来保存访问量、实时排行榜等信息,也可以用来做session共享。
为什么选择Redis
- redis数据都存在内存中,速度快
- redis有持久化机制,断电重启数据可以恢复
- redis中的数据结构比较多,可以满足更多需求
- 支持事务
为什么单线程的Redis这么快
- Redis是基于内存实现的,不需要去读写磁盘,速度自然快
- Redis单线程减少了线程上下文切换时带来的开销(因为redis中每个操作耗时都比较短,使用多线程可能效率更低,而java中采用多线程是因为有些线程需要执行时间比较长的任务)并且单线程不用额外的同步机制
- Redis采用了IO多路复用
IO多路复用机制
redis IO多路复用线程模型 - 简书 (jianshu.com)
单线程中如果一个网络请求进来后阻塞在某个地方时,会导致其他操作被阻塞。Redis使用Linux的IO多路复用机制实现。该机制允许内核中存在多个套接字(网络请求),内核一直监听是否由数据或请求到达。数据到达后统一进入处理队列
Redis同时监听多个socket,请求到来时将socket放入队列中排队等待处理
Redis的缓存一致性解决方案
主要使用的方式有两种:先删缓存再改数据库->延时双删解决脏数据(仍然存在脏数据的可能)
先改数据库再删除缓存->消息队列异步删除、canel基于binlog同步
Redis如何实现统计在线人数
- 每个用户登录后,在redis中设置一个key(username,这里最好可以设置一个前缀+username便于统计总在线人数)并设置过期时间,例如设置30分钟
- 当用户每次进行操作时,重新更新过期时间,可以结合AOP实现
- 获取总人数时只需要获取固定前缀的key个数即可
Redis实现分布式锁
基于setnx实现分布式锁
- 客户端发送setnx+expire
RedLock
https://blog.csdn.net/lisheng19870305/article/details/122464924
Redis持久化
redis持久化主要有两种方式:AOF和RDB
AOF是将操作指令一条一条记录下来,AOF是写后日志。每一操作结束后进行记录确保了指令是正确的
AOF的日志写入方式有三种:每条指令进行记录、每秒记录一次、以及不进行主动记录(操作系统触发)
AOF日志持续记录可能会导致日志文件过大的问题,redis会主动对日志进行重写例如对某一个list进行了多次操作,重写之后只保存最终态的指令
RDB是将某一刻redis当中的数据以快照的方式保存
redis在生成RDB快照时默认会开辟一个子线程执行,redis采用了写时复制的方式来避免主线程阻塞
RDB的执行策略为t秒执行一次
一、概述
Redis是一个使用C语言开发的一个基于内存的键值对数据库,并且可以通过日志和快照实现持久化。
Redis中使用哈希表来保存所有键值对,其结构类似于Java中的HashMap。因为Redis中值的数据类型不同,哈希表中保存的是所有值的指针。这个表称为全局哈希表。使用哈希表来保存键值对的好处就是,不论哈希表中元素个数,每次访问的时间复杂度都是O(1)。
但是于HashMap相同,当表中元素过多时,写入数据时可能会变慢。其原因就是过多元素带来的hash冲突和rehash带来的操作阻塞。
- Hash冲突带来的问题,Redis中的哈希表也使用了链式哈希来解决哈希冲突。但每个桶中的节点在查询时只能通过遍历来查找。所以当哈希表中元素过多时就会出现变慢的问题
- ==Rehash==带来的问题,当哈希表中的元素逐步增多时Redis会对哈希表进行rehash操作即重分配扩容,以求链表上的元素能分散到桶之间减少单个桶的元素个数。在扩容期间客户端的请求可能就会被阻塞导致查找变慢
Redis和Memcached的区别和共同点
区别
- redis中的数据类型更丰富
- redis支持数据持久化,memcache不支持
- redis支持事务
共同点
- 都是基于内存实现的
二、扩容机制
Redis中的用于存放键值对的哈希表实际上有两个,即哈希表1、哈希表2
一开始插入数据时redis只使用哈希表1,此时的哈希表2没有被分配空间。当哈希表中的元素触发扩容时。
- redis给哈希表2分配更大空间(一般是哈希表1的两倍大小)
- 将哈希表1中的内容拷贝到哈希表2中
- 释放哈希表1,用作下次扩容
**问题:**Redis的哈希表扩容流程大致如此。但这种方式存在一定问题—在迁移元素时元素过多会导致请求阻塞。此时Redis无法快速访问数据
**解决:**Redis采用渐进式rehash方式
Redis不会将客户端请求阻塞来进行拷贝,在第二步拷贝期间,Redis正常处理请求,每次请求结束就按顺序将哈希表1中一个桶上的元素散列到哈希表2中,下一次请求结束后继续将下一个顺序位置上的元素散列到哈希表2中。这样就可以将拷贝元素耗费的时间分摊到每次请求耗时上,避免出现一次请求被长时间阻塞的问题
数据结构
(108条消息) Redis五种基本数据类型底层实现_AmyZheng_的博客-CSDN博客
https://baijiahao.baidu.com/s?id=1731524214636496899&wfr=spider&for=pc
redis中主要有5种数据结构:string hash list set zset
String
简单动态字符串,类似于Java中的ArrayList,字符串的内容是可以修改的。
字符串有多种数据类型:int、embstr、raw
-
能用使用数字编码时采用int
-
字符串小于44个字节时使用embstr
-
字符串大于44字节时使用raw
Hash
hash有两种数据结构:ziplist、hashtable
ziplist:当hash表的元素很少时,会直接使用压缩列表来实现,查找元素时直接通过遍历查找
hashtable:
数组加链表,类似于Java中的Hashtable,区别是redis中的hash在扩容时采用的是延迟扩容,客户端请求时将一个元素转移到新Hashtable上,这样做的好处是将扩容耗时均摊到了每次请求上。如果长时间没有请求,Redis也会执行扩容。
List
list的底层数据结构是(3.2之后改用快表->压缩列表+双向链表)
基于压缩列表+双向链表,即多个压缩列表由前后指针链接成链表。
Set
底层数据结构:intset 和 hashtable
- intset:当元素小于512并且都为整数时使用intset
- hashtable
Zset
底层数据结构:压缩列表和跳表
- ziplist:当元素较少时使用
- skiplist:多层的链表
持久化机制
Redis的持久化主要有两大机制,AOF日志和RDB快照
AOF日志
AOF日志是写后日志(Redis先执行操作,再写日志)
AOF中记录的是Redis收到的每条命令,并以文本形式保存
为什么采用写后日志
-
写后日志可以确保记录的命令正确性,只有正确执行后的命令才会被记录下来
-
写后日志不会阻塞命令的执行
-
因为Redis在记录日志时并不会对语法正确性进行检查,所以如果是写前日志就没有办法保证语法正确性
潜在风险
- 服务器在操作完成后,日志记录前就宕机了,这部分操作就无法记录到日志中
- 写后日志虽然不会阻塞当前命令的执行,但因为Redis采用主线程来记录日志,这可能会导致后面的命令被阻塞
三种写回策略
AOF机制提供了三种可配置的写回策略
-
Always:同步写回,每个写命令执行完,立马同步将日志写回磁盘
-
**Everysec:**每秒写回,每个写命令执行完后,将AOF日志放入内存缓冲,每隔一秒将日志存入磁盘
-
**No:**操作系统控制写回,每个命令执行完后,将AOF日志放入内存缓冲,由操作系统决定合适写回磁盘
AOF日志过大问题
因为AOF是以文件形式记录,随着接收的命令越来越多,AOF文件会变得越来越大,这时又会对性能产生一定影响。
解决办法:AOF重写机制,多条指令变一条指令
日志重写是Redis开新的线程来执行的,并不会影响操作
RDB快照
使用AOF日志的方法存在一些问题,比如每次重启时都需要将操作执行一次,如果日志非常多就会造成Redis恢复缓慢的问题。
Redis还有另外一种持久化方法:内存快照RDB
所谓内存快照就是在某一时刻将Redis内存中的数据记录下来并保存成文件。这种文件就被称为RDB文件。
RDB与AOF的区别
- RDB每次保存时只需要保存当前时刻Redis中数据的状态
- RDB文件避免了日志文件过大问题
给哪些内存数据做快照
Redis为了保证数据的可靠性,生成快照时使用的全量快照即每次都会将当前内存中所有的数据全部放入快照中。
Redis中有两种快照生成方式
- save:主线程执行,会导致阻塞
- bgsave:创建子线程执行,避免和主线程阻塞(Redis默认配置)
生成快照时Redis的正常操作会被阻塞吗
Redis使用基于操作系统提供的写时复制技术(Copy-On-Write,COW),实现在生成快照时也能继续写操作。其原理是:
由于bgsave子进程是由主线程fork生成的,可以共享主线程的所有内存数据,子进程在写入快照时,如果主线程对某个键值对需要修改,则将该键值对进行复制,并提供给用户修改。这样就能保证生成快照的同时能进行修改。
生成快照的时机
假设每隔t秒生成开始执行一次快照生成,t的大小不能过于小,如果太小可能会造成资源消耗过大,甚至出现上一个子进程没有执行结束下一个子进程开始执行的情况。如果间隔太大间隔时间内修改的数据就没有办法被保存。
总结
Redis使用AOF日志和RDB快照混合使用的方法。即主要RDB方式生成快照,但两次快照间隔使用AOF日志方式保存操作记录。
缓存穿透、击穿、雪崩
缓存穿透
大量请求的key在redis以及数据库中都不存在,导致后端频繁查询数据库。
这种情况一般是黑客恶意攻击造成的。
解决办法:
- 对请求进行必要的参数校验
- 将无效的数据缓存到redis中(如果是大量不同且不存在的请求的话,redis的压力会增大)
- 使用布隆过滤器,判断请求的数据是否存在。
布隆过滤器
布隆过滤器可以判断要请求的数据是否存在与数据库和缓存中,判断key是否合法。请求到达后布隆过滤器判断是否合法,key值不合法时直接返回。
布隆过滤器是一个bit数组,元素加入过滤器是会计算出一个hash值,然后将对应下标上的值置为1。
判断时只需要计算出要查询的key的哈希值,确认对应下标是否为1,如果为0说明数据不存在,为1时说明数据可能存在。因为计算哈希值时可能出现哈希冲突。所以布隆过滤器可能出现误判,但不影响它的正确性。
缓存击穿
redis中缓存的一个热点数据失效后,导致大量请求打到数据库上,给数据库造成很大压力。
解决办法:
- 缓存失效后,线程查询数据库之前要求先获取锁。
- 这样就保证了大量请求需要查询数据库时,只有一个线程获取到锁去查询数据库,其他线程等待数据查询出来后,直接从redis中获取数据即可。
雪崩
redis中缓存的数据在短时间内同时失效,造成大量请求打到数据库上。
解决办法
- 设置数据永不过期(数据量大了之后redis压力大)
- 随机设置数据过期时间,使过期时间分散。
三者区别
缓存击穿和雪崩类似,但击穿主要指的是一个值过期后大量请求打到数据库上造成的数据库压力。
雪崩是指缓存中大量的值在短时间内同时过期造成的数据库压力。
键值对过期机制
redis中有一个过期字典,一个哈希表。其中的键对应数据的键,值对应的是该键值对的过期时间
redis中过期删除策略主要有两种 惰性删除、定期删除
**惰性删除:**redis在使用这个键值对时才会判断该键值对是否过期。这种删除策略可能会出现有键值对已经过期了但可能有大量数据过期但没有被删除的情况。惰性删除对CPU比较友好
**定期删除:**redis定期的判断哪些键值对过期需要删除。定期删除对内存比较友好
redis采用两种混合的方式,但还是可能会造成内存溢出的情况,这时候就需要redis中的内存淘汰机制了。
Redis内存淘汰机制
redis的内存如果不足时会触发redis的内存淘汰机制。
redis中一共有6种淘汰机制
- 选择设置了过期时间且==最近最少使用(LRU)==的键值对
- 选择设置了过期时间且即将过期的键值对
- 在设置了过期时间的键值对中随机选择键值对淘汰
- 在所有键值对中选择最近最少使用(LRU)的键值对
- 在所有键值对中随机选择键值对
- 不淘汰键值对(新增键值对将失败)
- 淘汰设置了过期时间的键值对中使用频率最少的
- 淘汰所有键值对中使用频率最少的
Redis内存满了之后会发生什么
redis的读会正常执行,但写操作会返回错误信息。
缓存和数据库数据的一致性
为什么说延时双删很扯淡 - idea偶买噶 - 博客园 (cnblogs.com)
保证缓存和数据数据一致有几种方法,这里只讨论主要使用的两种
先删缓存,再改数据库
存在的问题
这种策略存在一定问题,例如:A再删除缓存后,改数据库之前,B同时也进来读取数据,B会发现缓存中没有数据于是去查数据库,由于此时A并没有修改数据库中的数据所以B会读取到旧的数据并且重新设置到缓存中,此时缓存中的数据即位脏数据。
如何解决
所以可以在A修改完数据库后延时等待一定时间再次删除缓存,防止修改数据库期间有其他线程将脏数据写到缓存中。但是等待的时间根据具体的业务进行修改所以仍然可能会出现脏数据。
使用场景
这种方式适合使用在能容忍脏数据的业务场景,并且最好是对缓存数据设置了过期值的场景
下面是延时双删的流程图
延时双删就是在数据更新后,等待一段时间等其他拿到旧数据的事务更新到数据库中后再删除缓存。
但整体来说延时双删在第二次等待期间,其他线程拿到的都是旧数据。(解决办法:等待期间不让过多的线程请求进来,并且尽量缩短第一次删除缓存和更新数据库的时间,就可以减少获取到旧数据的线程数量)
先改数据库,再删除缓存
存在问题
请求A查询获取到旧值,请求B修改数据库并删除缓存,请求A将旧值同步到缓存中导致出现脏数据
如何解决
可以考虑使用消息队列异步删除,或者binlog同步删除(canel这种基于binlog日志的同步中间件)
Redis三种模式
主从模式
哨兵模式
Redis在主从复制模式下,master结点如果出现故障就需要人工来选举新的master点。Redis通过哨兵模式来实现结点动态选举新的master结点。
-
哨兵进程监控集群中master结点是否正常工作
-
当哨兵进程检测到master结点故障时,会自动将slave结点切换为master结点,并且通知其他slave结点修改自己的配置文件来切换master结点
-
Redis中的哨兵不止一个,为了防止单个哨兵出现故障而无法进行动态选举。一般采用多哨兵模式。
-
当一个哨兵检测到master不可用时,这一个哨兵会主观认为它不可用(称为主观下线)只有当多个哨兵都认为master不可用时才会开始进行新结点的选举。这时原来的master结点被称为(客观下线)
哨兵进程工作流程
- 每个哨兵进程以每秒一次的频率向master结点、slave结点以及其他哨兵发送信号
- 如果redis实例(master、slave)超过时间还没有响应,哨兵进程就将该结点标记为主观下线。并且哨兵还会继续发送信号确认该结点是否真的进入主观下线状态
- 当一定数量的哨兵标记master结点主观下线后(大于一个配置的值),该结点被标记为客观下线
- 若没有足够数量的哨兵标记master结点主观下线,标记将被清除