缓存
缓存穿透
概念:大量访问不存在的数据,数据库压力大
解决方法:
- 设置null值
- 布隆过滤
缓存击穿
概念:热点key过期,导致大量访问数据库
解决方法:
- 互斥锁
- 逻辑过期
缓存雪崩
概念:大量key同时失效,导致大量访问数据库
解决方法:
- 给过期时间加上随机值
- 利用redis集群
- 给缓存业务添加降级限流策略
- 添加多级缓存
双写一致
概念:数据库和redis缓存的内容要保持一致
方法:
- 不用但会问:
- 延迟双删:先删除redis缓存,修改数据库后,延迟一段时间,再删除一次redis缓存(延迟时间原因:数据库一般是主从模式,要等主节点把数据同步给从节点)
- 强一致性:
- 共享锁&排他锁(redisson已经提供)
- 非强一致性:
- 异步通知(使用消息队列mq)
持久化
概念:核心是将内存中易丢失的键值对数据,定期 / 实时写入磁盘存储介质的机制;因为 Redis 是内存数据库,默认数据仅存于内存,进程退出或服务器宕机时数据会丢失,持久化就是为了解决这一问题,同时支持数据备份与灾难恢复。
解决方法:
- RDB(Redis Database) :按指定规则生成某一时刻全量数据的二进制快照文件(.rdb) 。通过手动执行
SAVE/BGSAVE命令,或配置文件中设置触发条件触发;适合全量备份、数据迁移,缺点是可能丢失两次快照间的数据。 - AOF(Append Only File) :以日志形式,实时追加记录所有写命令到文件(.aof)。重启时通过重放 AOF 文件中的命令恢复数据;默认每秒 / 每次命令执行后同步,数据丢失风险极低,缺点是日志文件体积可能较大,恢复速度比 RDB 慢。
RDB执行原理:
- bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入RDB文件;
- fork采用的是copy-on-write技术:
- 当主进程执行读操作时,访问共享内存;
- 当主进程执行写操作时,则会拷贝一份数据,执行写操作。
Redis篇-06-redis使用场景-缓存-持久化_哔哩哔哩_bilibili
数据过期策略
key过期了怎么处理:
- 惰性删除:设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key
- 定期删除:每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)。
redis的过期删除策略:两者配合使用
数据淘汰策略
缓存过多,内存满了怎么办?redis支持8种策略:
- noeviction:不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略
- volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰
- allkeys-random:对全体key,随机进行淘汰
- volatile-random:对设置了TTL的key,随机进行淘汰
- allkeys-lru: 对全体key,基于LRU算法(最近最少使用)进行淘汰
- volatile-Iru:对设置了TTL的key,基于LRU算法进行淘汰
- allkeys-lfu:对全体key,基于LFU算法(最少频率使用)进行淘汰
- volatile-Ifu:对设置了TTL的key,基于LFU算法进行淘汰
分布式锁
场景:集群模式下的抢券
实现方式:
- redis的setnx
- redisson:底层也是使用的setnx
如何设置锁的过期时间?
自动续期:redisson有看门狗机制(默认10s为当前线程续期一次)和重试机制(在阈值内反复重试获取锁)
加锁和设置过期时间都是基于Lua脚本实现的:保证原子性
redisson实现的分布式锁可重入:利用hash结构记录线程id和重入次数
redisson锁解决主从数据一致的问题:红锁(获取锁时需要获取一半以上的节点的锁,但是性能太低了,不使用)
redis集群方案
- 主从复制+哨兵模式(高并发读、高可用)
- 分片集群(高并发读写、高可用、海量数据存储)
主从复制
主节点负责写操作,完成后将数据同步给从节点,从节点负责读操作
主从数据同步原理:先全量,后增量
- 全量同步:
- 从节点向主节点发送数据同步的请求
- 主节点根据从节点的replid和自己的replid是否一致来判断这个从节点是否是第一次进行数据同步
- 如果是第一次进行数据同步,则先与从节点同步版本信息,然后执行bgsave生成.rdb文件传给从节点进行同步,在生成.rdb文件的过程中执行的新命令会写入repl_baklog日志中,再传给从节点进行同步
- 如果不是第一次进行数据同步,则直接通过repl_baklog日志来同步(通过偏移量offset来判断从日志的哪里开始同步),不会执行bgsave了
- 增量同步:
- 从节点向主节点请求数据同步
- 主节点判断是否是第一次(这里肯定不是第一次了,因为先进行的全量),不是第一次就获取从节点的offset值
- 主节点从命令日志中获取offset后的值传给从节点进行数据同步
哨兵模式
作用:实现主从集群的自动故障恢复
特点:
- 监控:Sentinel(哨兵)会不断检查master和slave是否按预期工作
- 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
- 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
监控:心跳机制(每隔1s向集群的每个实例发送ping命令)
- 主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
- 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。
哨兵选取主节点的规则:
- 首先判断主与从节点断开时间长短,如超过指定值就排该从节点
- 然后判断从节点的slave-priority值,越小优先级越高
- 如果slave-prority一样,则判断slave节点的offset值,越大优先级越高
- 如果offset值还一样,则判断slave节点的运行id大小,越小优先级越高
redis集群脑裂问题:
- 概念:集群脑裂是由于主节点和从节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到主节点,所以通过选举的方式提升了一个从节点为主,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将老的主节点降为从节点,这时再从新master同步数据,就会导致数据丢失
- 解决:我们可以修改redis的配置,可以设置最少的从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失
分片集群:
类似于多个主从在一块
特征:
- 集群中有多个master,每个master保存不同数据
- 每个master都可以有多个slave节点
- master之间通过ping监测彼此健康状态
- 客户端请求可以访问集群任意节点,最终都会被转发到正确节点
数据的存储与读取:
引入了哈希槽,将插槽分配给不同的实例
根据key的有效部分计算哈希值,对哈希槽大小取余,余数作为插槽,寻找插槽所对应的实例
redis是单线程的,为什么还那么快
- Redis是纯内存操作,执行速度非常快
- 采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题
- 使用I/O多路复用模型,非阻塞IO
I/O多路复用模型,非阻塞IO详解:
Redis篇-14-redis其他面试问题-redis是单线程的,但是为什么还那么快_哔哩哔哩_bilibili
redis突击篇
面试突击 Redis 时使用
什么是redis?
Redis 是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快 ,常用于缓存,消息队列、分布式锁等场景。
Redis 提供了多种数据类型来支持不同的业务场景,比如 String(字符串)、Hash(哈希)、List (列表)、Set(集合)、Zset(有序集合)、Bitmaps(位图)、HyperLogLog(基数统计)、GEO(地理信息)、Stream(流),并且对数据类型的操作都是原子性的,因为执行命令由单线程负责,不存在并发竞争的问题。
除此之外,Redis 还支持事务、持久化、Lua 脚本、多种集群方案(主从复制模式、哨兵模式、切片集群模式)、发布/订阅模式,内存淘汰机制、过期删除机制等等。
为什么使用redis而不用别的?
- 是基于内存的数据库
- 有过期策略
- 性能非常高
- Redis 支持的数据类型丰富
- Redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用
- Redis 原生支持集群模式
- Redis 支持发布订阅模型、Lua 脚本、事务等功能
为什么用 Redis 作为 MySQL 的缓存?(为什么不直接使用MySQL而要使用Redis?)
主要是因为 Redis 具备「高性能」和「高并发」两种特性。
1、Redis 具备高性能
假如用户第一次访问 MySQL 中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据缓存在 Redis 中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了,操作 Redis 缓存就是直接操作内存,所以速度相当快。
如果 MySQL 中的对应数据改变了之后,同步改变 Redis 缓存中相应的数据即可。
2、Redis 具备高并发
单台设备的 Redis 的 QPS(Query Per Second,每秒钟处理完请求的次数)是 MySQL 的 10 倍左右。
所以,直接访问 Redis 能够承受的请求是远远大于直接访问 MySQL 的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
Redis 数据类型以及使用场景分别是什么?
Redis 提供了丰富的数据类型,常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
随着 Redis 版本的更新,后面又支持了四种数据类型: BitMap、HyperLogLog、GEO、Stream。 Redis 五种数据类型的应用场景:
-
String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
-
List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。
-
Hash 类型:缓存对象、购物车等。
-
Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
-
Zset 类型:排序场景,比如排行榜、电话和姓名排序等。
Redis 后续版本又支持四种数据类型,它们的应用场景如下:
-
BitMap:二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
-
HyperLogLog:海量数据基数统计的场景,比如百万级网页 UV 计数等;
-
GEO:存储地理位置信息的场景,比如滴滴叫车;
-
Stream:消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息 ID,支持以消费组形式消费数据。
五种常见的 Redis 数据类型是怎么实现?
String 类型内部实现
String 类型的底层的数据结构实现主要是 SDS(简单动态字符串)。 SDS 和我们认识的 C 字符串不太一样,之所以没有使用 C 语言的字符串表示,因为 SDS 相比于 C 的原生字符串:
-
SDS 不仅可以保存文本数据,还可以保存二进制数据。因为 SDS 使用 len 属性的值而不是空字符来判断字符串是否结束,并且 SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在 buf[] 数组里的数据。所以 SDS 不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据。
-
SDS 获取字符串长度的时间复杂度是 O(1)。因为 C 语言的字符串并不记录自身长度,所以获取长度的复杂度为 O(n);而 SDS 结构里用 len 属性记录了字符串长度,所以复杂度为 O(1)。
-
Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。因为 SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。
List 类型内部实现
List 类型的底层数据结构是由双向链表或压缩列表实现的:
-
如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),列表每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;
-
如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;
但是在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。
Hash 类型内部实现
Hash 类型的底层数据结构是由压缩列表或哈希表实现的:
-
如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;
-
如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的底层数据结构。
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
Set 类型内部实现
Set 类型的底层数据结构是由哈希表或整数集合实现的:
-
如果集合中的元素都是整数且元素个数小于 512(默认值,set-maxintset-entries 配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;
-
如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。
ZSet 类型内部实现
Zset 类型的底层数据结构是由压缩列表或跳表实现的:
-
如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
-
如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。