Redis 面试笔记
目录
Redis的使用场景
聊项目中的。分布式锁啊,流量控制啊。
如何保证redis和mysql数据一致性
Redis 和 MySQL 的数据一致性,实际上很难做到绝对强一致,通常只能做到最终一致。 我们一般采用旁路缓存模式,也就是读的时候先查 Redis,没有再查 MySQL 并回填缓存;写的时候先更新 MySQL,再删除 Redis,而不是更新缓存。 之所以这样做,是因为缓存本质是副本,删除比更新更安全。 但在高并发场景下,仍然可能出现旧数据回填,所以会结合延迟双删、MQ 异步删除缓存、失败重试、过期时间兜底等方案,尽量缩小不一致窗口。 如果是更复杂的系统,也可以通过订阅 MySQL binlog 来统一做缓存失效。 总体来说,核心思路就是:MySQL 作为最终真源,Redis 作为高性能缓存,通过“先写库、再删缓存、失败补偿”来保证最终一致性。
Redis的存储结构
string,hash,list,set,zset
首先最常见的自然是内5个:
- string: 可以用他存储一些验证码之类的小字符串。
- hash: 可以用他存储一些对象,因为对象是属性+属性值,hash结构有field-value
- list: 可以用来实现一些队列、栈结构,同时允许元素重复,并且存取有序。
- set: 可以用来存储一些不允许重复的数据,存取是无序的,同时利用他可以实现一些交集并集差集这种操作。
- zset: 可以用来实现一些时间窗口的数据结构,也可以做一些排行榜之类的内容。
其次是另外集中数据结构。
- hyperLogLog: 海量数据的统计,比如可以做一些日PV,日UV。是用极小的内存统计非常大的体量
- GEO: 存储地址位置,经纬度,帮你实现一些经纬度相关的查询。
- bitmap: 实现布隆过滤器,做一些海量数据的去重。
- Stream: 类似传统的消息队列的玩法,解决了之前Redis没有专门的MQ结构,需要利用list结构或者是他的publish&subscribe去实现,但是没有ack,消费者组之类的改建,而Stream就提供了。
底层实现
string: 就一个动态字符串,没啥聊的。
hash: 压缩表zipList + hash表(就HashMap)
- 数据体量比较少的时候会采用zipList,条数过多或者占用空间比较大,就用hash表
- 默认:条数超过512,或者 单个元素内存超过64字节 ,就会舍弃压缩表,用hash表
list: quickList(zipList变种)本质就是zipList用指针串连到一起
set: 有序的数值数组(只有数值的时候) + hash表(数据比较大或者有字符串)
- 默认:有序的数值数组的条数超过512,就会换成hash表。
- 如果存储的内容不是纯数值,那就换直接采用hash表
zset: 压缩表zipList + 跳表
- 数据体量少,用zipList,多了直接换跳表
- 默认:数据条数超过128,或者内存占用超过64Kb,就会舍弃压缩表,就用跳表。
hyperLogLog: https://www.mashibing.com/study?courseNo=2727§ionNo=109463&systemId=1&courseVersionId=3633&versionsId=215
GEO: 本质就是利用的ZSet
bitmap: 二进制,没啥聊的。
Stream: 底层结构忽略,不需要看,会用即可。
Redis的缓存穿透、击穿、雪崩
缓存穿透:
- 啥问题: 请求一个缓存和数据库都不存在的数据,比如GET /user?id=99999999,导致每次请求都会穿透到DB数据库。
- 导致了啥情况: 这种请求体量还比较大,一般可能是恶意请求,导致缓存无法命中,大量的请求DB还扛不住,DB直接宕机。
- 解决方案:
- 可以直接在缓存中存储上这个key,并且value设置为空值,这样请求就不会打到DB。
- 可以在请求直接找Redis之前,先通过布隆过滤器查询一下数据是否存在,存在再去Redis里查询。(但是可能存在误判问题,延伸到布隆过滤器的细节) 什么是布隆过滤器?
- 可以在网关处查看非人类行为的请求,可以直接封掉IP。
- 可以在网关处,查看请求携带的参数是否符合要求,不符合,直接返回空值。
- 实际解决方案: 四个都上!
缓存击穿:
- 啥问题: 一个高并发访问的热点key,在Redis中缓存失效了,大量的请求直接干到DB。
- 导致了啥情况: 热点key过期,Redis查询不到,导致请求干到DB,DB懵逼。
- 解决方案:
- 加锁:可以JVM锁,也可以分布式锁,只要可以限制别有大量的请求直接干到DB就可以。
- key不过期:针对一些key,无时无刻都在用,那倒不如直接不过期。要考虑好内存问题。
- 实际方式: 方便一些的话,@Cacheable这种Spring-Cache的注解,自带JVM锁,或者根据业务,永不过期也没毛病。
缓存雪崩:
- 啥问题: 一般服务在启动时,会做缓存预热,导致大量的key生存时间都是一样的,在同一时间都集体失效,导致很多请求绕过了缓存,直接打到DB。
- 导致啥问题: 虽然不是热点key,但是组合拳把DB干懵。
- 解决方案:
- 加锁:可以JVM锁,也可以分布式锁,只要可以限制别有大量的请求直接干到DB就可以。
- 过期时间添加随机数,规避掉这种统一时间到期的问题。
- 也可以针对一些查询及其频繁,更改及其少的key,并且内存不大的,不设置过期时间。
- 实际方案: 三管齐下,都上。
布隆过滤器
1、布隆过滤器的结构?
- 本质就是一个基于bit的数组,将数据进行hash运算,来实现判断数据是否存在的一个数据结构。默认情况下,数组里的值都是0,代表数据不存在。 如果为1,就代表数据可能存在。
2、写入过程?
- 将对应的数据进行hash运算,然后跟bit数组长度做好取余的操作(二进制的方式也可以,参考HashMap)。将对应的bit位,设置为1,代表当前数据存在。
3、判断过程?
- 将你要判断重复的数据,做hash运算,跟上述方式一样,定位到一个索引位置,查看是0,还是1。
4、误判问题?
- 因为对数据进行hash运算后,必然可能会出现hash冲突的问题,导致查询到的索引位置是1,但是可能不是当前数据设置的1。 (布隆过滤器说不存在,一定不存在,但是说存在,可能不存在也可能存在)
5、怎么去减少误判的问题?
- 减少误判的方式一般有两种
- 增加bit数组的长度 ,当长度足够长时,可以减少因为hash不同,但是确定索引位置相关的这种情况。因为是bit,1000万的长度,其实内存才占1.2M不到。
- 增加hash次数 ,比如一个数据,基于多种不同的hash算法去做运算,然后对不同的bit位置修改为1,同理,判断重复的时候,也要经过多次运算,如果都是1,可能存在,只要有一个0,就代表不存在。
- 减少误判的代价:
- 增加数组长度的方案,会让内存占用变多。
- 增加hash次数,会导致无论是写入,还是判断重复时的效率变慢。
6、如何使用布隆过滤?
- Java内存玩的话,直接上Guava提供的一个布隆过滤器,你可以指定预期的元素个数,以及误判的几率,他会指定帮你指定好数组长度以及hash次数。
- Redis玩的话,可以直接上Redis官方提供的RedisBloom,就是布隆过滤器的实现,需要单独下载。
Ps:布隆过滤器的误判无法解决,如果要求没有误判的话,在走一次布隆过滤器后,如果数据存在,再单独的查询一次这个数据,比如在Redis里直接查询一次,成本不是很高,可以接受。
Redis单线程?多线程?
Redis你可以说他是单线程,也可以说他是多线程,甚至他还是个多进程的程序。哪个都对,但是你要解释清楚!!!!
1、Redis的单线程
- Redis在执行命令的时候,一直是单线程,无论是5.x还是6.x
2、Redis的多线程
- Redis全局不可能是单线程处理所有逻辑的,他必然是多线程的。比如:
- 主从,集群,定期删除策略,内部还有定时任务记录时间戳…………
- 6.x的版本中,提供了处理 网络IO的时候是多线程的 ,基于Reactor搞得变种。在负责网络的读取,写回数据的过程是多线程,但是执行命令依然单线程。而且默认是关闭的。
3、Redis的多进程
- Redis在持久化的操作里,还会fork一个子进程,子进程负责的内存区域跟Redis主进程的内存区域一致,他会将内存里的数据帮你做持久化。
- 为什么是fork一个子进程,而不是构建一个线程去完成持久化。
- 资源隔离,避免构建的子线程影响到Redis的主进程,搞一个子进程的话,凉也是凉子进程。
- COW(Copy On Write),Fork子进程的时候,拿到的应当是Redis主内存的数据快照,不影响Redis的继续写入。(我猜的)
Redis中的key删除策略
Redis中的key,如果生存时间到了,不会立即删除! Redis没有精力监听所有的key! 而是基于下面的两种方式来
Redis中key的删除策略,要从两个维度聊
1、惰性删除
- 当查询到对应的key之后,他会先查看一下这个key过期了没,如果过期了,就删除这个key,然后返回null。在源码中可以看到,基于key查询后,会先判断一次key的生存时间到了没
checkAlreadyExpired,到了就直接删除。
2、定期删除(从两个维度来聊)
- 5.x版本:
- 定时去执行一个函数,随机拿到一些key,查看生存时间是否到了,到了就删除!
- 定期删除触发的是
activeExpireCycle函数,来执行删除key的操作。 - 基于配置文件中的hz,默认值是10,代表1s执行10次这个函数。100ms一次。
- 在
activeExpireCycle函数中,会随机的拿出20个Redis中的key,判断选中的key中,生存时间是否到期,到期就删除。 - 如果每次随机选取的key,过期的超过了25%,继续执行内部的do-while循环,继续执行上述中的逻辑。再拿20个,再判断,再删。
- 每执行16次上述的操作,他会判断一下,你当前循环的执行时间,超过了25ms,就会主动退出循环,等待下次任务的触发。
- 定期删除触发的是
- 定时去执行一个函数,随机拿到一些key,查看生存时间是否到了,到了就删除!
- 6.x版本:
- 首先5.x中,提供的一些定期删除的策略,几乎是写死的,能调整的地方不多,而6.x版本中,可以指定一个配置,动态的决定删除的频率高或低。
- 有一个配置,
active-expire-effort,默认值是1,可以主动去调整这个数值,他的取值范围是1~10,数值越大,占用CPU资源越多,定期删除执行的就越频繁。 - 而且可以动态调整,Redis运行时,可以主动基于CONFIG SET去调整
active-expire-effort配置
- 有一个配置,
- 首先5.x中,提供的一些定期删除的策略,几乎是写死的,能调整的地方不多,而6.x版本中,可以指定一个配置,动态的决定删除的频率高或低。
Redis的淘汰策略(京东物流)
1、Redis的内存淘汰的触发时机:
- Redis的淘汰是根据配置文件中的 maxmemory 配置决定的,如果没有主动的去配置
maxmemory的值,内存淘汰是不会触发的。 相当于即便Redis把整个物理机的内存都干满了,也不会触发。 极端点就是操作系统不干了,把你Redis的进程kill掉,甚至windows的效果就蓝屏,也就是系统崩了。- 在源码中有一个判断
if (!server.maxmemory) return 0; /* No limit. */如果没有主动设置maxmemory,不会触发淘汰策略 - 触发淘汰的时机就是使用内存大于设置的
maxmemory。 - 如果问主动设置
maxmemory,一定清楚,生产环境,为了稳定,至少要给操作系统预留2G的空间!可以这么理解,2C4G的服务器,就部署一个Redis,一般Redis的内存给2G左右。
- 在源码中有一个判断
2、Redis的淘汰策略有哪几种。
-
一共有八种,在Redis的配置中写的很清晰
# volatile-lru -> Evict using approximated LRU, only keys with an expire set. # allkeys-lru -> Evict any key using approximated LRU. # volatile-lfu -> Evict using approximated LFU, only keys with an expire set. # allkeys-lfu -> Evict any key using approximated LFU. # volatile-random -> Remove a random key having an expire set. # allkeys-random -> Remove a random key, any key. # volatile-ttl -> Remove the key with the nearest expire time (minor TTL) # noeviction -> Don't evict anything, just return an error on write operations.可以理解为5个大方向:
- Lru:最近最少使用
- Lfu:最近最少频次使用
- random:随机
- ttl:生存时间剩余少的
- noeviction:抛异常
-
Lru和Lfu的存储和更新方式:
- 首先无论是Lru,还是Lfu,都是存储在你在数据里的,Redis存储的数据是RedisObject,在内部有一个24bit位的属性,用来存储Lru和Lfu的信息。
- Lru:存储时间戳
- Lfu:高16位存储时间,低8位存储次数
- 在查询数据后,会触发Lru或者Lfu的操作
- 先拿到key对应的value
- 然后判断你的策略是Lfu还是Lru,执行对应的更新操作。
- 首先无论是Lru,还是Lfu,都是存储在你在数据里的,Redis存储的数据是RedisObject,在内部有一个24bit位的属性,用来存储Lru和Lfu的信息。
-
**Lru算法:**传统的Lru的时间,可以是一个双向链表,是要当前数据被操作了,就挪动到活跃位置,当需要空间时,可以将最不活跃位置的数据干掉。但是Redis没用,他就是单独存储一个时间戳。
- Lru获取时间戳有两个方案:
- 直接从一个LruClock中维护的时间戳去获取,这个时间戳默认每100ms(hz)更新一次。Lru操作时,如果精度小于等于1000(常量),就会走这个维护的时间戳,不需要重新计算。
- 如果精度不满足(源码中的体现,不可能不满足!),会重新计算当前系统的时间戳。
- Lru获取时间戳有两个方案:
-
**Lfu算法:**传统的Lfu无非就是存俩信息,可以理解为HashMap的效果一行,比如横向存储次数,纵向基于Lru的套路调整位置。
- 先判断是否需要挪动频次的位置。因为不是每次操作都改频次,频次一共就0~255,很快用没了,越往上,调整越慢。
- 然后修改bit属性,高位设置时间戳,同时查看低位频次是否需要改。
-
回收的大致过程:
- Redis会在Lru,Lfu,TTL这三种淘汰策略时,维护一个驱逐池,pool,驱逐池。
- Redis会定期随机找一些key,放到这个池子中,比如Lru,会比较空闲时间,将空闲时间大的,留在pool中。空闲时间小的,扔出去。
- 回收过程:
- Lru,Lfu,TTL:直接基于池子删。
- random:不需要维护池子,直接随机找一些key,随机删除!
- Redis会在Lru,Lfu,TTL这三种淘汰策略时,维护一个驱逐池,pool,驱逐池。
Redis的持久化机制
- RDB快照
- AOF追加日志