redis原理

发布于:2025-02-10 ⋅ 阅读:(32) ⋅ 点赞:(0)

redis客户端-认识RESP

为什么我们能编写出一个自定义的redis客户端?

redis官网公开了应用层协议,所以我们可以通过redis提供的自定义协议'resp',编写一个自己的redis客户端程序
优点 : 
1. 简单好实现
2. 快速进行解析
3. 肉眼可读
传输层使用的是TCP

持久化

mysql的事务,四个核心特性

1.原子性
2.一致性
3.持久性
4.隔离性

redis持久性(持久化)

redis是将数据存储到内存中,所以他是不持久的,断电及丢失,所以redis的持久性就是将数据存储到硬盘中,
最终redis决定,内存中也存储,硬盘中也存储,这两份数据理论上是完全相同的
所以当要插入一个新的数据的时候,就要把这个数据同时插入到内存和硬盘,读取时直接从内存读取,
硬盘数据只是在redis重启的时候,用来回复内存中的数据

持久化具体实现

1. RDB(Redis DataBase)

RDB 定期地把我们redis内存中的所有数据,都写入硬盘中,生成一个“快照”,
后续redis一旦启动了,(内存数据就没了)就可以根据刚才的“快照”把内存
中的数据恢复回来

“定期”具体来说,有两种方式:
1. 手动触发
        程序员再redis客户端执行特定的命令,来触发快照的生成
        sava、bgsave
sava :  全力以赴进行“快照生成”操作,就会阻塞redis的其他命令,类似
        keys* 的操作,所以一般我们不建议使用save
bgsave:不会影响redis服务器处理其他客户端的请求和命令
        此处redis是使用多进程的方式完成bgsave命令的

2. 自动触发
        在redis配置文件中,设置让redis每隔多长时间 /没产生多少次的修改,
        就进行触发

2. AOF(Append Only File)

类似于mysql的binlog,会把用户的每个操作,都记录到文件中,
当redis重新启动的时候,就会读取这个aof文件中的内容,用来恢复数据
注意:当开始aof的时候,rdb就不生效了
aof是一个文本文件,每次进行的操作就会被记录到文本文件中,
通过一些特殊的符号作为分隔符,来对命令做出记录 
为什么多次的写入硬盘没有影响到redis的效率?
1. AOF 机制 并非是直接让工作线程把数据写入硬盘,而是先写入一个内存的缓冲区,
积累一波之后,再统一写入硬盘,大大降低了写硬盘的次数
2. 硬盘上读写数据,顺序读写一般是比较快的(还是比内存要慢很多),但是比随机
访问,还是要快很多的,此处的AOF就是顺序写入(每次把新的操作写入到文件的末尾)


redis 给出了一些选项(appendfsync),根据实际情况来决定如何取舍,(缓冲区的刷新策略)
如果刷新频率越高,性能影响就越大,同时数据可靠性就越高
always :刷新频率最高,可靠性高,性能最低
everysec(默认) :刷新频率低一些,可靠性也低一些,性能高一些
no :刷新频率最低,可靠性最低,性能最高
AOF重写机制(rewrite):
redis存在针对aof文件进行整理操作,这个整理操作能够剔除其中冗余的操作,并且合并一些
操作,达到给aof瘦身的效果

当redis中同时存在AOF和RDB时,以AOF为主

事务

MySQL事务

1.原子性:把多个操作打包成一个整体
2.一致性:事务执行之前和之后,数据不能不一致
3.持久性:事务中作出修改都会存入硬盘
4.隔离性:事务并发执行涉及到的问题

Redis事务

原子性:到底有没有原子性redis存在争议,原本的原子性含义时把多个操作打包一起,要么都
        执行成功,要么都不执行。但是redis事务中若干个操作如果存在失败,那就失败了,
        并没有回滚执行完了就是执行完了,所以我们可以称作“弱化的原子性”
一致性:redis没有约束,也没有回滚机制,如果事务执行中出现某个修改操作失败,就可能引
        起不一致的情况,所以不具备一致性
持久性:redis本身是内存数据库,是存在内存中的,虽然redis也有持久化机制,但是这里的
        持久化机制,和事务没什么直接关系,所以不及备持久性
隔离性:redis是一个单线程的模型,所有的请求/事务都是串行化的,所以不涉及隔离性

redis事务主要的意义,就是“打包”,避免其他客户端的命令插入到中间(底层是一个队列)

相关命令

开启事务

MULTI(multi)猫题~

执行事务

EXEC(exec)

放弃当前事务

DISCARD(discard)

监控key

WATCH(watch)

主从复制

在分布式系统中,希望使用多个服务器来部署redis,存在下面几种redis的部署方式
1.主从模式
2.主从+哨兵模式
3.集群模式

主从模式

在若干个redis节点中,有的时“主”节点,有的时“从”节点
从节点上的数据要跟随主节点变化,从节点的数据和主节点保持一致
(从节点就是主节点的副本)
从节点上的数据不能修改,只能读取


更准确来说,主从模式,主要是针对“读操作”进行并发量 & 可用性
的提高的,而写操作的话,无论是可用性还是并发,都是非常依赖主
节点,但是主节点又不能搞多个

拓扑结构

若干个主从之间,按照啥样的方式来进行组织连接


一主一从
一主多从
树形主从结构(从节点下有从节点) 

同步过程

redis提供了psync命令,完成数据同步过程,psync不需要咱们手动执行,redis
服务器会在建立好主从同步关系之后,自动执行psync,
从节点负责执行psync,从节点从主节点这边拉取数据

部分复制

有些时候,从节点本省已经有了主节点的绝大部分数据,这个时候,就不太需要进行
全量复制,比如出现了网络抖动,主节点最近修改的数据就无法及时同步过来,更严重的
从节点已经无法感知到主节点的存在了

实时复制

从节点已经和主节点同步好了数据(从节点这一刻已经和主节点数据一致了)但是
之后,主节点这边会源源不断的收到新的修改数据的请求,主节点的数据也会随之改变

哨兵(Sentinel)

哨兵节点是一个独立的redis-sentinel进程,他不负责存储数据,只对redis-server进程
也就是主节点和从节点负责监控,通常哨兵节点也会形成一个集合,以防单个哨兵节点
挂了的情况
核心功能 : 
1. 监控 
2. 自动故障转移
3. 通知故障转移给应用放

哨兵选取主节点流程

1.主观下线
哨兵节点通过心跳包,判定redis服务器是否能正常工作,如果心跳包没有如约而至,
就说明redis服务器挂了,此时还不能排除网络波动的原因,只是单方面的认为该节点g了
2.客观下线
多个哨兵判断主节点redis服务器g了,因此达成共识(达到法定票数),客观上决定该redis
服务器下线,这才是真正的下线
3.要让多个哨兵节点,选出一个leader节点(哨兵),由这个leader哨兵负责选一个从节点作为
新的主节点
4.leader节点挑选从节点作为主节点
    1)优先级  每个redis数据节点都会在配置文件中,有一个优先级的设置,默认是一样的,
        可以设置,优先级高的从节点先胜出。
    2) offset 最大,offset从节点从主节点这边同步数据的进度,数值越大,说明从节点数
        据和主节点越接近。 
    3) run id 每个redis节点启动时都会随机生成一串数字,大小全看缘分,此时随便选一个
5.把新的主节点指定好之后,leader就会控制这个节点,执行slave no one成为master 再控制
其他节点,执行slaveof 主节点ip 主节点端口 ,让这些其他节点以新的master为主节点

集群

广义上的集群,是只要是多个机器,构成了一个分布式系统,都可以称为是一个“集群”
狭义上的集群,redis提供的集群模式,这个集群模式之下,主要是解决存储空间不足的问题
哨兵模式主要是提高了系统的“可用性”,哨兵模式中本质上还是redis主从节点存储数据,其中
就要求一个主/从节点就需要存储真个数据的“全集”、
此处关键问题,就是引入多台机器,每台机器存储一部分数据

数据分片

分片算法

1. 哈希求余
借鉴了哈希表的基本思想,借助hash函数把一个key映射到一个整数,再针对数组长度求余,就可以
得到一个数组下标 
优点 : 简单高效,数据分配均匀
缺点 : 一旦服务器需要扩容,就需要重新hash,打乱了所有的映射规则,就需要数据搬运,此时需要
搬运的数据还是很多的,所以扩容的开销很大
2. 一致性哈希算法
优点 : 缩减了扩容时候的消耗
缺点 : 数据分配不均匀
3. 哈希槽分区算法(redis真正采用的算法)
把一致哈希 和 哈希求余 两个方式结合,此处的分片会使用“位图”这样的数据结构来表示当前有多少
槽位号,针对某个分片,上面的槽位号,不一定非待是连续的区间

主节点宕机

如果主节点宕机
和哨兵模式一样,集群机制会实行故障转移,在从节点中挑选一个从节点作为主节点,当挂掉的主节点再次启动,则会变成从节点加入到新的主节点下面,但是故障转移具体处理流程和哨兵模式具体有所不同。
主节点故障转移
1. 故障判定  
集群中的所有节点都会定期发送心跳包,这个心跳包不是全部的节点都发一遍,而是随机发送,如果A给B发送了ping命令之后,B没有在规定时间之内回复pong命令则A节点会尝试重置和B的tcp连接,若连接失败则会把B标记成pfail`主观下线`

后续redis会通过内置的协议和其他节点沟通,其他节点向B发送ping命令,每一个节点都会维护一个“下线列表”,每个人的下线列表都不太一样(每个节点的网络环境不太一样),如果其他未收到pong回应数量超过集群总数的一半,就会把B标记成fail已经下线,此处`客观下线`

此时B节点就会当作是故障节点
2. 故障迁移
如果B是故障节点,但是B是从节点,那么就不会发生故障迁移
如果B是故障节点,但是B是主节点,那么由B节点的从节点(比如C和D)触发故障迁移,故障迁移就是把从节点提拔成主节点

选举过程(谁休眠时间短大概率就是谁): 
	1)如果从节点和主节点已经太久没有通信(此时认为从节点和主节点差异太大)就失去了竞选资格。
	2)接着具有资格的节点就会集体进入休眠状态,休眠时间和offset有关,如果offset值越大,休眠时间越短,就能提前结束休眠。
	3)先唤醒的节点先进行拉票,拉票只能发送给主节点,只有主节点才能进行投票,如果票数过半,该节点则晋升成主节点,该节点自己执行(slaveof no one)变成主节点,另外的落选的节点自己执行(slaveof ip port)变成从节点。
上述的投票算法叫做Raft算法

集群扩容

1.新增分片到集群中
2.重新分配slots(槽)
3.给新的主节点添加从节点

缓存

redis最主要的用途:
1.存储数据(内存数据库)
2.缓存[redis最常用的场景]
3.消息队列

我们通常使用redis作为数据库(Mysql)的缓存

为什么关系型数据库性能不高?
1.数据库的存储实在硬盘上的,硬盘IO是很慢的
2.如果查询不能命中索引,就需要进行表的遍历,会大大增加IO的次数
3.关系型数据库会对SQL的执行做出一些解析(sql语句)
4.涉及到一些复杂的查询,比如联合查询,就需要进行笛卡尔积操作
正是因为关系型数据库效率比较低,所以无法承担高并发的需求,一旦并发过高,就会消耗过高的硬件资源(cpu、内存、硬盘、网络等),就容易使数据库宕机!
如何提高MySQL并发量?
1.开源:引入更多的机器,构成数据库集群(主从复制、分库分表等)
2.节流:引入缓存,存放热点数据,后续查询(读操作)如果缓存中已经存在,就不会再访问MySQL

缓存更新

1.定期生成
	访问的数据,以日志的形式记录下来,每隔一定时间统计里面出现的高频单词作为“热点词”,之后把这些热点词放入到rdis中作为缓存数据
	优点 :实现起来较为简单,过程可控(缓存中有什么是比较固定的),方便排查问题
	缺点 :实时性不够!如果有突发事件新的突发事件热词就无法查询得到

2.实时生成
	访问redis如果有直接返回,如果没有查到,就查MySQL之后把MySQL中的数据存放到redis中,方便下次查询,如果redis内存满了(内存是可调的),就会触发淘汰策略。
	内存淘汰策略(面试常考):
		1)FIFO先进先出:把redis中存在时间最久的数据
		2)LRU淘汰最久未使用的:把最长时间没有使用到的数据淘汰
		3)LFU淘汰访问次数最少的:把访问次数最少的数据淘汰
		4)Random随机淘汰:选取随机数据进行淘汰
	在redis中有一个配置项可以设置redis缓存的淘汰策略,当然也可以自己实现

缓存预热、缓存穿透、缓存雪崩、缓存击穿

  • 缓存预热

    缓存中的数据
    1.定期生成(这种情况不涉及“预热”)
    2.实时生成
    	redis服务器首次连接之后,服务器里面是没有数据的,此时所有的请求都会打到MySQL上面,导致MySQL服务器宕机
    缓存预热就是把定期生成和实时生成结合,先通过离线的方式导入一些数据到redis中,就能帮助MySQL承担压力,随着时间的推移,redis上的热点数据就会越来越多,MySQL的压力就小了
    
  • 缓存穿透

    查询的某个数据在redis中没有,MySQL中也没有,这个数据肯定也不会更新到redis中,但是下次还会有人查询,仍然没有,如果这样的数据过多,并且还反复查询,一样会给MySQL带来很大的压力
    
    为何产生?
    1)业务设计不合理,缺少校验环节
    2)不小心删除了数据库上的数据
    3)黑客攻击
    
    如何解决?
    1)改进业务,增加校验环节(亡羊补牢,不太靠谱)
    2)如果发现这个key在redis和MySQL中都不存在,仍然写入redis中,value设置成一个“”即可(可靠)
    3)可以引入 布隆过滤器 每次查询redis和MySQL之前都判定以下key是否在布隆过滤器中存在(可靠)
    
  • 缓存雪崩

    短时间内大量的key在缓存上失效,导致数据库MySQL压力骤增,甚至直接宕机
    
    为何产生?
    1)redis直接挂了
    2)redis上的大量key同时过期
    
    如何解决?
    1)加强监控报警,加强redis集群的可用性
    2)不给key设置过期时间,或者在给key添加过期时间时候,添加随机过期时间,避免同一时间过期
    
  • 缓存击穿

    缓存击穿不太合适,实际上称作缓存瘫痪更合适
    缓存击穿是缓存雪崩的一个特殊情况,指的是热点key过期的情况,如果热点key过期了,但是访问key的频率是很高的,所以会让大量的数据打到MySQL中,导致MySQL宕机!
    
    如何解决?
    1)基于统计的方式发现热点key,并设置永不过期
    2)进行必要的服务降级,例如使用分布式锁
    

分布式锁

处理分布式的线程安全问题就会用到“分布式锁”

所谓的分布式锁,也就是一个/一组单独的服务器程序,给其他的服务器提供“加锁”服务,redis是一种典型的可以用来实现分布式锁的方案,但是不是唯一的一种,业界可能也会使用MySQL/zookeeper这样的组件来实现分布式锁的效果

就拿买票来说,在买票服务器进行买票的时候,就先要进行加锁(往redis上设置一个特殊的key-value,完成上述的买票操作再将其删除,其他的服务器也想要买票的时候,也会去redis上设置key-value,如果发现key-value已经存在,则会认为“加锁失败”具体是放弃还是等待要看具体实现)

过期时间

如果某服务器对其加锁之后,异常掉电或者宕机,该如何处理?

可以通过set ex nx 设置过期时间,就算是真的掉电,但是时间一到就自动解锁,就可以释放锁,注意要使用原子命令,不能分开使用命令设置

但是也会存在,服务器1加锁,但是服务器2执行了解锁,可能是误解锁,但是这种情况还是存在的,因为分布式锁只需要将存在的key-value删除,为了解决上述问题,就需要引入校验机制

校验Id

1.给服务器编号,每个服务器都一个自己的身份标识
2.进行加锁的时候,设置key-value key对应着要针对哪个资源加锁,value就可以存储刚才服务器的编号,标识出当前这个锁是哪个服务器加的。
3.解锁的时候进行校验,查看这个锁对应的服务器编号,然后判断一下编号是否就是当前执行解锁的服务器编号,是的话就进行解锁,不是就不进行

Lua脚本

现在还是存在一个情况,上述在解锁的时候,先判定,在进行del那么就不是原子的,就会出现问题

lua是一个编程语言,作为redis内嵌脚本,原因是lua语言特别轻量,实现一个lua解析器小号的体积很小,可以使用lua编写一些逻辑,把这个脚本上传到redis服务器上,然后就可以让客户端控制redis,执行lua脚本过程中也是原子的,相当于一条命令,就类似于redis中的事务

watch dog(看门狗)

在加锁的时候,给key设置过期时间,如果过期时间太短在业务逻辑还没执行完就直接释放了,如果设置时间太长,就会导致“所释放不及时”问题,所以更好的方式就是“动态续约”

例如: 初始状态下,设置过期时间为10s,同时看门狗每隔3s看一次任务是否已经完成,如果没有完成,就会进行续约帮你续约300ms之类,设置的短一点,如果任务已经完成了,就通过lua脚本直接释放锁,如果中途程序崩溃了,也就没有人来续约了时间到了也会自动释放

Redlock算法

为了防止在给master节点加锁之后,立马宕机了,此时数据还没有同步给slave,slave重新成为master但是并不知道加锁信息,所以引入redlock算法,redlock就是引入的不是单个的redis服务器当锁而是一组甚至多个服务器,就是一个冗余。


网站公告

今日签到

点亮在社区的每一天
去签到