八股记忆笔记
Redis
1.字符串
-
为什么不采用c语言作为字符串具体实现
-
O(n):因为c语言的字符串结尾是以
\0
结尾的 -
没有很好的扩容机制
-
特殊字符无法处理:还是因为
\0
-
-
使用java语言的优点(虽然实际还是以c语言为基础,加了别的标识属性的结构体)
-
字符串长度获取时间复杂度从 O(n)->O(1)
-
减少字符串扩容引起的数据搬运次数
-
储存更加复杂的二进制数据
-
2.链表
-
底层实现是双向链表
-
其中有三种函数,分别是复制函数, 释放函数, 对比函数
-
常见API函数:
-
lpush:向链表左边增加元素
-
rpush:向链表右边增加元素
-
lpop:弹出左侧第一个元素
-
rpop:弹出右侧第一个元素
-
llen:获得链表长度
-
lrange:按索引范围获取值
-
3.哈希表
-
键值对,一对一的实现,一个key对应一个value
-
通过键key,在O(1)时间复杂度下获得相对应的值
-
因为c语言没有内置哈希表这个数据结构,所以就是Redis是自己实现的Hash表
-
-
Redis采用拉链法
-
直接找到对应的key然后直接插到单链表的末端
-
-
sizemask的作用是拿来防止error,做与运算,防止溢出
4.集合
-
二分查找
-
增加就是扩容,删除就是缩容
-
insert是占用一段连续的内存
-
每次修改数据都要重新申请空间
5.有序集合
-
二分查找
-
Zset应用场景 :
-
就是热搜话题,微博热搜之类的
-
选取TopK作为返回值,时间复杂度维O(nlogn)
-
如果这个map本身就是按照标题热度值组织的有序结构,就是复杂度为O(k)
-
Zset中有一种实现方式就是跳表
-
6.持久化
-
Redis是内存型数据库
-
持久化意思就是:
-
把内存上的数据同步到硬盘中去,断电后不丢失
-
-
缺点,数据易失性,断电后内存数据消失
-
解决方法RDB(Redis Data Base),AOF(Append Only File)
RDB(Redis Data Base)
-
通俗:就是把内存中所有数据直接存储到硬盘中!!!!
-
就是把目前redis内存中的数据,生成一个快照(RDB文件),保存在硬盘中,就是相当于备份,然后文件是以
.rdb
为后缀的,就是如果发生事故的话,redis就可以通过这个文件,进行文件读取,并且将数据重新载入内存中
-
上图中不一样的就是
-
数据里有设置的数据的有效期
-
一个是有过期时间的数据
-
一个是没有过期时间的数据
-
-
-
以下是各个基本类型在RDB中的结构
触发RDB条件
-
手动触发
-
save
:执行save指令,主线程执行rdbSave函数,服务器进程阻塞,就是不能处理别的请求了,直接就是卡住了,这个redis -
bgsave(background save):本质就是这个命令和save差不多,区别就是这个命令会去fork一个子进程,去执行rdbSave函数,所以就是主线程还是可以执行新请求,不过就是bgsave在子进程上用,不会干扰主进程。
-
-
自动触发
-
redis的配置文件写入save m n ,就是当m秒内发生n次此变化,就会自动执行bgsave
-
意思就是相当于在后台一直跑这个bgsave这个函数到达一定条件就是m秒内发生了n次变化就到达条件,直接执行bgsave。
-
AOF(Append Only File)
-
记录之后所有对Redis数据进行修改的操作
-
发生事故,redis可以通过AOF文件,将文件中的数据修改命令全部执行一遍,一次来恢复数据
-
-
重写与恢复
问:
当AOF执行过程中会产生很多的无效命令以及过期命令,造成AOF文件过大,请问怎么样才能缓解这种情况???
答:Redis提供了一个AOF文件重写功能,创建一个新的AOF文件,当有对同一条数据操作或者不同的数据操作而产生新的命令,redis的AOF会有一个文件重写策略,产生一个新的AOF文件,代替先前的AOF文件,但是俩个AOF文件保存的数据库数据是相同的,只是新的不会有冗余的命令
注意:新的AOF文件是不会参考老的AOF文件的,而是由当前Redis的数据状态产生的。
触发AOF条件:
-
手动触发:
-
bgrewriteaof
-
执行这个命令,然后redis就会根据当前数据库的状态生成一个AOF文件,并且把老版的AOF文件替换掉,旧的会被删掉
-
-
自动触发:
-
配置文件中设置
appendonly yes
开启-
appendonly yes
策略-
Always:就是同步写回,意思就是在每个命令执行完毕后直接将命令写入到磁盘文件中,就是AOF文件中(数据基本保证了可靠性,但是redis性能大大减低)
-
Everysec:就是每秒写回,在每个命令执行完成后,直接被写入到文件的内存的缓冲区,然后每过1s,redis就会把这个缓冲区的命令写到磁盘的AOF文件中(发生事故最多丢失1s内的数据,性能影响不大)
-
No:这个NO就是将操作命令全部写到Redis缓冲区,至于什么时候将缓存数据写到磁盘,这得看操作系统决定(出了问题,数据不知道有没有,但是性能影响最小)
-
-
Redis得
appendonly yes
默认策略是Everysec
-
注意:其实No的效率跟Everysec差不多,建议用Everysec
-
-
7.缓存
1.缓存淘汰
因为一般数据比缓存大,就是比如数据有20个g,但是高速缓存只有2个g 这样,所以当在缓存中满了的情况下,redis会采用淘汰策略。
-
先进先出FIFO算法
-
就是队列那样,先进缓存的老元素被淘汰 ,然后最新元素进入。
-
-
最近最少使用LRU
-
实现思路:双向链表+哈希表形式储存
-
通俗:就是按照数据中被使用数据时间排序,然后把末尾的数据(键以及对应的值)删掉,再把最新的加上去
-
就是当某个key被访问到了,然后程序会通过哈希表迅速定位到这个节点(时间复杂度O(1)),并将改节点调整到链表最开始的位置。
-
-
最不经常使用LFU
-
按照访问次数淘汰
-
就是俩个表:键值对映射表+次数到链表的映射表(都是哈希表),以及双向链表管理同次数的key
-
淘汰时找到最小次数的链表的末尾节点。
-
键值对映射表:
-
存每个 Key 对应的 Value(实际数据)、访问次数(count)、最近访问时间(lastTime)(处理 “次数相同” 时的淘汰顺序)。
-
-
次数到链表的映射表:
-
相同访问次数的 Key 放在同一个双向链表中,方便快速找到 “当前次数最小的所有 Key”,以及对 Key 的访问次数变化进行分组管理。
-
-
注意:!!!!!!!!
redis如果需要淘汰老数据了,默认采用的是最近最少使用LRU算法
2.过期删除
就是缓存中的数据超过了预设的有效期,然后系统自动将这些过期数据从缓存中移除的机制
关键概念:
过期字典,就是redis会默认为每个数据库维护一个过期字典,存储了所有的过期时间的键。
Key:就是时间戳对应的键
Value:就是过期时间戳
有三种删除模式:
-
定时删除(主动删除)
-
核心:设置缓存的时候设置一个定时器,时间已到达就立马触发删除操作。
-
实现:每个键值对附带一个时间戳,存入缓存时,就启动一个单独的线程或者定时器,到期后直接删除这个键。
-
优点:过期数据立即删除,不i会使内存占用超标
-
缺点:大量的定时器会消耗很多的CPU资源(特别时缓存量很大的时候),而且定时器的创建和管理成本也很高。
-
-
惰性删除
-
核心:程序取值的时候查看该数据是否过期,如果没过期,就直接返回,过期就删除该键,并返回
null
-
优点:服务器运算资源占用小
-
缺点:造成某些数据长期占用内存,不被删除
-
-
定期删除
-
核心:redis默认每10秒执行一下随机抽取一定数量的键(默认20个),检查并删除过期的键,条件是如果本轮的过期键比例超过25%,就会继续抽取删除,直到比例低于25%或者达到最大的执行时间(25ms)
-
实现:(通过redis.conf配置)
-
hz
:每秒执行定期删除的次数(默认是10,范围是1-500) -
active-expire-effort
:删除力度(默认是1,范围是1-10)
-
-
!!!:每隔一段时间,跑主动删除,不跑主动删除时,就是执行惰性删除策略
-
注意:
Redis底层是采用惰性删除和定期删除结合的方式
3.缓存一致(Cache Aside)
指的是**缓存**中的数据跟**数据库数据**保持**同步**的状态,但现实是缓存数据跟数据库数据不同步一致,具体表现为:当数据库更新时,缓存中的旧数据需要被及时清理或更新,**避免请求读取了脏数据**。
-
核心思想:当缓存数据有更新值,采用删除缓存数据。
-
更新策略(核心点)
-
延迟双删
-
实现:先删除缓存,再更新数据库,然后延迟一段时间,再删除缓存
-
目的:解决数据库更新未完成,缓存被加载旧数据问题
-
注意:延迟时间要大于数据库更新的时间
-
-
-
Read/Write Through 模式
-
这个模式就是,应用把缓存作为主要的数据读取方式,即是避免缓存击穿。
-
-
Write Behid模式
-
实现:将更新步骤放到消息队列中,异步执行。
-
4.缓存击穿
定义:大量请求查询某一个值,该值在缓存中无,在数据库中有。
场景:非常常见,因为有时候会设置数据过期时间,当数据过期就是找不到值了,所以就会发生缓存击穿
-
场景问题:
-
热点商品抢购,如iphone16抢购,缓存存了商品的信息,小程序挤满了想要抢货的人,时间一到开抢,但是这个缓存的信息过期了 ,Redis给删了这个信息,于是大量的请求就没打到缓存上,直接请求到后端数据库上。
-
主要问题:Redis的性能是普通关系型数据库的10-100倍,如果Redis能勉强处理的请求直接打到Mysql上,那么Mysql就处理不过来直接宕机了。
-
-
解决方式
-
Mysql方面:减少击穿后的直接流量
-
实现:直接加分布式锁(互斥锁),保证同时间只有一个线程查询数据库并更新缓存,其他线程等待缓存加载我弄成后再读取。
-
// 伪代码:使用Redis的setnx实现互斥锁 String lockKey = "lock:hot_key"; // 尝试加锁(设置过期时间防止死锁) if (redis.set(lockKey, "1", "NX", "EX", 10)) { try { // 查询数据库,更新缓存 String data = mysql.query("SELECT * FROM table WHERE id=热点Key"); redis.set("hot_key", data, "EX", 3600); // 重新设置缓存 } finally { redis.del(lockKey); // 释放锁 } } else { // 未获取到锁,等待一段时间后重试 Thread.sleep(100); return redis.get("hot_key"); // 再次查询缓存(此时可能已被加载) }
-
-
-
Redis方面:
-
1.设置热点数据永不过期
-
热点数据后台启动一个异步线程,重新把数据回填缓存层
-
-
5.缓存穿透
定义:查询一个数据库和缓存都不存在的数据,如果刚删除,本来就没有,所以叫缓存穿透
-
解决方法:
-
1.拦截非法查询请求
-
2.缓存空对象
-
3.布隆过滤器
-
-
具体实现
-
拦截非法查询请求:
-
直接在控制器中配置,直接说明非法请求
-
-
缓存空对象
-
高访问的数据,在缓存中缓存一个空对象,这样也不会直接打到数据库上。
-
-
布隆过滤器
-
是缓存穿透得前置防线,通过极低的空间成本,拦截大量的无效请求。
-
在请求到达缓存或数据库前,通过布隆过滤器快速判断 Key 是否 “可能存在”:
-
若判断为 “不存在”(一定正确),直接返回请求(无需查缓存 / 数据库);
-
若判断为 “可能存在”(可能误判),继续查缓存→查数据库(此时 Key 可能真实存在)
-
-
-
-
6.缓存雪崩
定义:一大批被缓存的数据,同时失效,此时对于这一批的数据请求就全打到数据库上,导致数据库宕机。缓存击穿是单点的数据,缓存雪崩是多条数据
-
Mysql:装互斥锁,减少并发量,让请求在合理范围内
-
Redis:
-
设置热点数据不过期
-
分析失效时间,尽量让失效时间点分散
-
缓存预热,在上线前,根据当天情况分析,将热数据直接加载到缓存系统
-
8.集群
以上8.集群之前的都是单机运行Redis,这节讲的是,在好几台机器上运行Redis是怎么样的。
1.主从复制
-
①首先就是三次握手通信(采用TCP协议),主库和从库要想连接就得知道双方的IP和端口号,当主库和从库上线后就进行握手,然后信息验证。
-
②从库发送
PSYNC
命令:即是同步命令,开启数据同步过程,发送主库的运行ID和复制进度的偏移量Offset。 -
③握完手开始进入数据同步阶段,有俩种模式:
-
一是全量复制(全量同步)
-
why:从库第一次连接这个主库(ID不匹配或offset=-1),就会触发全量同步
-
step1:主库生成RDB快照文件(二进制格式),生成RDB快照的同时会将所有新的写命令记录到复制积压缓冲区(用于后面的增量同步),RDB生成完毕通过网络传输到从库
-
step2:从库接收到RDB文件后,直接清除自身当前所有数据,加载RDB文件,将数据写入内存,恢复主库的全量数据状态。
-
step3:RDB加载完毕后,主库将复制积压缓冲区中的写命令(RDB生成以及从库运行RDB文件期间产生的)发送给从库,从库就执行这些命令,确保和主节点数据完全一致,至此全量同步完成。
-
-
-
二是增量同步
-
why:从库和主库曾经连接过(ID匹配而且offset有效),且主库的复制积压缓冲区保留着从库的偏移量,才会触发增量同步。
-
step1:从库发送
PSYNC <master_run_id> <offset>
命令自报偏移量。-
这里注意!!!:
-
主节点检查复制积压缓冲区中是否包含该偏移量之后的命令:
-
若包含(偏移量在缓冲区范围内)→ 进入增量步;
-
若不包含(偏移量已被覆盖)→ 触发全量同步(因缓冲区是环形结构,旧命令会被新命令覆盖)。
-
-
-
step2:主库从复制积压缓冲区中提取从库偏移量(
offset
)之后的所有写命令(如offset=1000
到offset=2000
的命令),将这些命令发送给从库,从库执行后即可追上主库的最新状态。
-
-
-
主从复制完毕!!:
无论是全量还是增量同步完成后,主从节点会进入持续增量同步阶段,确保后续数据一致!!!
复制积压缓冲区是一个先进先出的队列,存储的是最近主库的数据修改命令
-
下图是redis断线后的重连机制
2.哨兵模式
主从模式保证不了redis的高可用,因为谁也不知道什么时候主库崩了或者从库崩了或者大家一起崩了,这样前面主从模式配置的一切都用不上了。
-
前提:Redis中提供了哨兵(Sentinel)机制来监控主从集群以及实现主从集群的自动故障恢复。
-
哨兵(Sentinel):三个作用加一种情况如下
-
监控:Sentinel基于心跳机制监测状态,默认每隔1s去发送一个
PING
命令去给每一个节点。-
没回应
PONG
:就是在Sentinel规定的时间内没有得到回应,则发命令的Sentinel主观认为这个节点下线,为主观下线。 -
都没回应:当一个Sentinel发现某个节点主观下线后,就发通知给其他的Sentinel,然后其他的Sentinel也发
PING
命令给该节点,如果超过指定数量quorum
(默认quorum为Sentinel实例的一半),就会被认为客观下线。
-
-
选举和恢复故障,通知:如果master故障,Sentinel会将一个slave提升为master,故障恢复还是以新的为主。
-
选择:先是比较优先级(slave-priority)谁高,再是比较谁的offset大,最后比较ID谁靠前,最重要的就是比较
offset
偏移量。 -
选择后:哨兵向新节点发送
SLAVEOF NO ONE
命令,让此节点成为主节点,然后通知其他的从节点SLAVEOF
命令,从新的主节点开始数据同步,同时也会更新配置文件和内存的数据结构,通知客户端让客户端连接新的主节点。
-
-
脑裂:当主节点有三个从节点,而有俩个从节点因为网络原因跟主节点和另一个从节点分离,误判主节点下线,触发不必要的故障转移,原主节点的写操作未同步到新主节点,导致数据丢失。
-
防止此情况配置:
-
min-replicas-to-write 1
:表示最少salve节点为1个 -
min-replicas-max-lag 5
:表示数据复制和同步的延迟不能超过五秒
-
-
-
-
Sentinel
监控
3.分片集群
主从复制和哨兵模式可以解决高可用,高并发读的问题,但是还有俩个问题:海量数据存储问题,高并发写的问题。
-
特征作用
-
多个master,每个master保存不同数据
-
每个master有多个slave节点
-
master通过
ping
监测彼此健康状态 -
客户端请求可以访问集群任意节点,最后都会转发到正确的节点
-
-
存储和读取
-
Redis分片集群中引入了哈希槽的概念,一共有16384个哈希槽。
-
redis集群将16384个插槽分配到不同的实例
-
读写数据:根据key的有效部分计算哈希值,对16384取余(有效部分,就是如果key前面有大括号,那么大括号内的即是有效部分,如果没有,则是key本身为有效部分)余数作为插槽,寻找插槽所在的实例。
-
9.为什么redis快
-
why:redis是单线程的,为什么还这么快
-
Redis是纯内存操作,执行速度快,0磁盘IO延迟,访问速度比磁盘快10万倍。
-
采用单线程,避免不必要的上下文切换可竞争条件,所有操作都是原子性。
-
使用I/O多路复用模型,非阻塞IO
-
I/O多路复用模型
实现高效的网络请求
-
用户空间和内核空间
-
常见的IO模型
-
阻塞IO(Blocking IO)
-
非阻塞IO(Nonblocking IO)
-
IO多路复用(IO Multiplexing)
-
-
Redis网络模型