Redis进阶

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

Redis持久化:

前面我们讲到mysql事务有四个比较核心的特性

  1. 原子性:保证多个操作打包成一个。
  2. 一致性:A给B100,A少一百,B必须多一百。
  3. 持久性:针对事务操作必须要持久生效,不管是重启还是什么数据是否还在,mysql把数据存储在硬盘上。
  4. 隔离性:脏读幻读不可重复读等等。

Redis是内存数据库,把数据存储在内存上,要想能够做到持久,就要把数据存在硬盘上面,为了速度快,数据得存在内存上,但是为了持久,数据还得存在硬盘上面。那么我们就将数据存在内存和硬盘上面。但是查询的时候就直接从内存中读取,硬盘的数据是为了重启后放在内存中的。

RDB(Redis Database)持久策略:

RDB是定期将数据写入内存中,都给先写入硬盘中,形成一个快照,也就是redis把内存中的数据,快速“拍个照”,存储在硬盘中,就能在重启的时候把数据回复过来。

  • 手动触发:程序员通过自己写代码来执行特定的命令,来触发快照的生成,save和bgsave(如下介绍)等等。
  • 自动触发:在配置文件中配置多久触发一次自动存储。

save:redis全力执行快照生成的操作,有可能阻塞redis。一般不建议使用。

bgsave:不会影响redis处理其他任务。既可以保证持久化顺利进行,也不会影响redis处理其他请求和命令。创建子进程,子进程完成持久化操作,持久化操作将数据写入RDB文件中,接着用新文件替换旧文件

  • 判定是否存在,如果已经存在其他进程运行了,bgsave就会直接返回,并不会继续执行。
  • 如果不存在的话就会通过fork的方式创建进程,fork创建新的进程简单粗暴,就是复制一份和父亲一模一样的进程,一旦复制完了就是两个独立的(一模一样)进程。也就是安排子进程进行复制操作。
  • 子进程进行写文件,父进程继续接受客户端请求。
  • 子进程完成持久化的过程,通知父进程,父进程更新一些统计信息,子进程销毁。

如果子进程和父进程的数据一样就不会触发拷贝,但是如果有变化就会触发写时拷贝自己原本的数据。 

redis生成的RDB文件是存放在redis的工作目录中的,也就是下面这个文件,redis会在工作的时候把输出的等等中间文件放到这里边。

在该目录下面查看文件,找到文件dump.rdb镜像文件。该文件是二进制的文件,以压缩的方式保存下来(节省空间)。

当执行RDB镜像操作时候,就会把要生成的文件先保存到一个临时文件中,当我们的快照生成完毕的时候,再删除之前的RDB文件,把新生成的文件名字修改成我们的dump.rdb。能够保证至始至终的RDB文件只有一个.

RDB文件并不是马上插入就会触发,是通过手动触发或者自动触发。

在redis的配置文件中阐述了自动触发保存的相关信息,虽然这里都可以随意修改数据,但是我们修改的时候要有一个基本原则,这里的RDB是一个比较高的成本,不能让这个操作比较频繁。        

问题:如果在两次快照(两次存档点)之间有大量的key插入删除和修改,但是在两次快照之间服务器突然挂了,就会出现数据丢失的情况!!!

 自动触发:

自动触发的场景有两种:

  1. 1.超时时间后自动触发保存。
  2. 2.通过shutdown命令关闭redis服务器就会触发自动保存。
  3. 进行主从复制的时候,主节点也会生成RDB快照,把RDB快照传给后面的从节点。 

实际上如果当redis异常关闭的时候,来不及生成RDB,就不会保存key存在RDB中。

使用linux的stat命令来观察var/lib/redis(在redis.conf中)文件下的修改rdb文件的incode编号来观察修改,如果再次执行bgsave的话,inode编号就会发送变化。 

如果是save命令就在当前进程中直接写入dump.RDB文件中写入数据,也就不存在文件替换等过程了。

自动生成RDB快照:

在配置文件中修改,修改自动生成RDB快照。

如果是save加上空字符串,那就是关闭快照 。

RDB文件改坏了:

 在etc/redis目录下打开/var/lib/redis查看vim dump.rdb。但是要把文件修改坏了并且保存的话,首选要知道redis进程的进程id。

再通过kill进程的方式,将进程的直接杀死。在这里虽然改坏了,但是看起来好像没什么问题,但是实际上有无问题得看实际情况。

文件格式检查工具:redis-check.rdb。

RDB文件的特点:

  1. RDB是一个紧凑的文件,适合全量备份的场景,比如每六个小时执行bgsave的场景,并且把RDB文件复制到远程机器或者文件系统中。
  2. Redis加载RDB回复数据的方式远远快于AOF的方式。(使用二进制进行组织数据)。
  3. RDB方式数据没办法做到实时持久化,因为每次bgsave运行都要执行fork创建子进程,属于重量级操作,频繁执行成本过高。
  4. Rdb文件用二进制的格式保存,Redis版本更替有多个版本,兼容性可能有风险。新老版本的RDB文件可能不同。

AOF(Append only file)持久策略:

RDB最大的问题就是不能实时持久化保存数据,两次快照期间,实时的数据可能随着重启而丢失。

原理:

类似于mysql的binlog,并不是存储数据,而是存储了该操作,存储在文件中,将操作内容存储在文件中,当redis重新启动的时候,就会读取AOF中的内容用来恢复数据。启动AOF,RDB就不生效了,AOF一般是关闭状态!!!!!!!

要将该命令改为yes,就能生效,就可以启动AOF了。

此时将AOF的操作保存在"appendonly.aof"文件之下,也存在/var/lib/redis的文件下(redis.conf下)。AOF是一个文本文件,每次进行操作都会被记录到文本文件中。

AOF是否会影响性能:

引入AOF后又要写内存也要写硬盘,并没有因为这样影响到redis处理请求的速度。

  1. AOF机制并非是直接把线程数据写入硬盘,而是在内存缓冲区中,积累一波后,写入硬盘。大大降低了写入硬盘的次数。
  2. 硬盘顺序读写相对随机读写的速度快,AOF每次是把数据写到文件的末尾,属于顺序写入。

如果在缓冲区中的数据,没来得及写入硬盘,就会丢失,就会导致数据不可靠!

刷新频率实际上是可以程序员自己设置的,刷新频率比较高,性能影响大,同时数据可靠性就比较高。

默认的策略是everysec每秒进行刷新,最多也就损失一秒的数据,可以在etc/redis下的redis.conf文件查看刷新策略。

AOF的文件大小会影响到redis下一次的启动时间,reids重新启动的时候要读取AOF文件的内容,而且有一些AOF文件是冗余的。比如分别多次插入数据,但是可以一次性插入成功,却分了很多次,就会造成冗余。

Redis的重写机制:

上述我们讲了redis会因为一些原因,记录冗余的AOF数据,所以我们的redis就有重写机制,可以去除掉这些冗余的数据,保留最后的结果,做到给AOF瘦身的这样的效果。

手动触发重写机制:

在redis中直接调用bgrewriteaof命令,就可以触发重写机制了。

自动触发重写机制:

AUTO-AOF-REWIRITE-MIN-SIZE:触发重写的AOF最小文件大小,默认大小是64MB。

AUTO-AOF-REWRITE-PERCENTAGE:表示需要重写时文件相对上一次增长的比例,比如原本AOF文件是1G,下次触发重写的时候就是1.5G的时候触发重写机制。

重写流程:

重写时候并不关心原本AOF文件中有啥,而是内存中的内存状态,同样的重写的时候,也会创建出子进程,子进程会读取内存中的数据,并且生成新的AOF文件,对他进行重写,也就是观察最新的内存数据,然后重写到AOF文件中。内存中的结果就已经是,用户把AOF重写后的状态。

此处写进程的方式就是很类似于RDB文件的镜像快照,不过RDB是二进制,这里是文本文件。在这里fork之前的的修改是存储在旧的AOF文件中,而fork之后则是存储在新的AOF文件中。子进程这边AOF写完后,会通知父进程,父进程会把aof-write-buf中的数据(fork之后的数据)也写入新的AOF文件中。但是这时候还在写旧的AOF文件。

如果在执行重写操作的时候,又来一个重写命令,此时不会执行命令,就直接返回了。如果在bgrewriteaof的时候正在生,成rdb快照,就会等待rdb快照生产完毕,再进行aof重写。

旧的AOF和新的AOF文件:

旧的AOF文件不能不写,如果考虑到极端情况,主机断电了,就会导致新的AOF文件不完整,旧的已经丢失了,导致文件不完整。旧的AOF文件在更替的过程中仍然会进行写入,防止主机断电导致新的没有及时更替,新的文件通过子进程和auto-rewrite-buf进行写入,子进程是在fork之前进行写入,而aof-rewrite-buf是在fork之后将数据写入新的aof文件中。

这里多次设置了key值,然而key3的值实际上最后只有555,其实最后只要设置555就可以了。

此时通过手动触发bgrewriteaof的方法,就能够重写AOF文件,变成RDB(在/var/lib/redis下边)。

混合持久化:

redis引入了混合持久化的特点,结合了RDB和AOF的特点,按照AOF的方式每个请求/操作,都记录到文件中,但是触发AOF重写的操作,就会将当前内容转化为二进制(也就是RDB文件),后续再进行操作,就追加到文件后面,但是是以文本文件的方式存储的。

默认打开混合持久化。保证了文件的可靠性,也保证了效率。

信号通知父进程:

进程之间的相互作用是信号,进程之间的相互作用,上述父子进程,子进程表达“我做完了”,使用信号之间还是可以的。子进程发送函数,父进程通过函数处理。

AOF和RDB差别:

RDB对于fork之后的数据就置之不理了,aof则对fork之后的数据采用了aof—rewrite-buf的方式进行处理。RDB的设计理念就是定期备份的,而AOF是实时备份,随着现在硬件资源更好了,现在AOF的应用场景也更多了。当redis同时存在AOF和RDB快照,以AOF为主,RDB就直接被忽略了。

AOF的数据比RDB更加全面。

Redis事务:

当我们学的MySQL的时候也学到MySQL的事务,但是Redis的事务就相对比较简单。        

弱化的原子性:

redis的原子性做到了要么全部执行,要么全部不执行,并不会保证全部成功,但是MySQL就一定要么保证全部成功,要么都不执行。如果事务中有失败,就失败,Redis不会管,而MySQL则会全部回滚。谈到原子性更多想到的MySQL。

不具备一致性:

Redis不具备一致性,也没有回滚机制,事务执行过程中如果某个操作失败了,就有可能引起不一致性。

不具备持久性:

Redis本身就是内存数据库,但是数据是存储在内存中的,虽然Redis有持久化机制,但是持久化机制和事务没什么关系。

不具备隔离性:

Redis是一个单线程服务器程序,所有的请求事务都是串行执行的,都不是串行,更谈不上隔离性了。

实际上Redis事务最重要的意义,就是为了打包,避免其他客户端命令,插队插到中间,就类似于先给出的命令的Redis不着急操作,而是等待后边命令过来一起操作。

原理和应用场景:

Redis实现事务是实现了一个队列,每个客户端都有一个队列,开启事务的时候,此时客户端输入命令,就会发送给服务器并且进入队列(不是立即执行),当遇到了执行事务的命令的时候,就会把这些队列按照一定的顺序进行执行,会把当前的任务执行完了,再执行别的队列中的任务(打包)。但是,执行是执行了,但是保证对不对,那就另当别论了。

如果搞的像MySQL那么复杂的话,就会付出很多空间和时间,时间上和空间会产生很多开销。

在多线程中我们常常使用加锁的方式来避免出现插队的情况,在Redis中就直接使用事务,

当我们有两个事务的时候,就会排队等待,获取count的时候并不会真正执行,要进行到执行事务的那一步的时候才会真正进行减减。也就是两个事务之间进行排队。在这个场景中就算没有加锁也能解决超卖的问题。

如果Redis按照集群模式进行部署,就不支持事务。Redis支持lua脚本,lua脚本可以实现上述条件判定,并且和事务一样也能打包执行。

事务操作:

  1. 开启事务:MULTI,当开启这个事务的时候,后续的操作都会进入事务的队列中。
  2. 执行事务:EXEC告诉Redis要把入队列的事务进行统一执行。
  3. 放弃当前事务:DISCARD丢弃当前事务。
开启事务:

发现这几个事务下列的返回结果都是QUEUE,说明将下列操作已经加入到执行的服务器的事务队列中了。

执行事务:

使用EXEC操作的命令,开始执行事务。也相当于上述命令的返回值了。

放弃当前事务:

丢弃当前的事务,就不会执行了,这时候这些前面的对事务的操作也就随之不生效了。                  

极端情况:

假如给服务器发送很多请求,此时服务器重启了,此时这个事务就相当于discard(丢弃)了,此时发送请求,是往Redis服务器中发送数据,实际上也是内存结构,如果服务重启,那么数据也就没有了。

WATCH(监控):

unwatch是取消监控。

监控某个key是否在事务执行之前发生改变,在以下这副图中,由于客户端1通过MULTI打开了事务,只有在通过exec执行事务后才会执行命令。所以下面的key的值是222。

由于开启事务时其他客户端修改的话就很容易产生歧义,就可以使用WATCH的命令来监控key,看看MULTI和EXEC之间是否在外部被其他客户端修改了。如果被修改的话,执行EXEC的话就会返回nil。

watch的实现类似于指的是乐观锁和悲观锁,乐观锁也就是有一个预期,接下来锁冲突的概率比较低,悲观锁有一个预期可能锁冲突的概率比较高。

回到watch,redis的watch就相当于基于版本号这样的机制,实现了“乐观锁”。

watch原理:

当执行watch key'的时候,就会给key安排一个版本号,版本号就可以理解成一个整数,每次修改的时候,版本号都会变大。

如果在其他客户端进行对key的修改,版本号就会变大,比如客户端2让版本号变大,比如版本号由1变成2,在最后执行的EXEC的时候,查看版本号和最初watch时候的版本号是否一致。而且必须在MULTI之前使用。如果不一致就直接丢弃事务操作了。

小结:

  • redis不支持回滚。
  • redis不会保证执行前执行后内容统一。
  • redis通过内存存储数据,所以没有持久性。
  • redis也谈不上隔离性,主要redis是单线程的。

multi开启事务,exec执行,discard丢弃事务,watch和unwatch监控key。

Redis主从复制:

在分布式系统中,希望有多个服务器部署Redis服务器,从而构成一个Redis集群,此时就可以让这个集群中的分布式给其他服务提供更加稳定的数据存储。

实际上服务器部署redis有好多种模式,大体可以分为三类:主从,主从加哨兵,集群。

主从结构:

主从模式。在若干个redis中,有的是主节点,有的是主节点,有的是从节点,也就是给redis节点划分了不同的角色,从节点得听主节点的,从节点的数据得和主节点的数据保持一致。把主节点的数据复制出来给从节点,让他们保持数据一致。(从节点就是主节点的副本)。从节点只能读取,不能修改。

由于从主节点和从节点的数据时刻保持一致,因此客户端从节点这边读取数据,可以随便挑一个读取数据,都是一样的。比如数据访问量比较多,那就可以将数据访问量分发给三个节点。 而且三个节点挂的概率很小。

如果挂掉了从节点,实际上没什么影响,挂了主节点就会影响到写数据,读这块还能用,写不能用了。        

主从模式主要是为了针对读操作的进行并发量的提高,然而写操作的话,无论是可用性和并发量都依赖主节点。但是平时读操作比写操作更加频繁。

 配置Redis主从启动多个redis进程:

我们可以在一个云服务器上面配置多个redis服务器,运行多个redis-server进程,但是我们得保证端口不一样。

  1. 可以在启动程序的时候,通过--port来指定端口号。
  2. 也可以在配置文件中指定端口。

这里我们做一个主节点,两个从节点,通过将配置文件复制的方式复制到redis-conf的文件下。

将这个daemon改为yes,表面redis可以在后台运行。并且修改端口号不为6379,这里我修改成了6380。我们保护模式和修改端口号。

通过redis-server ./slave1.conf

上面的命令,我们就可以启动redis,并且查看是否成功。

当前redis并没有构成主从结构,而是各自为政,想要构成主从结构还要进一步配置相关信息。

构成主从结构配置:

通过如上就可以启动redis服务器(根据不同的配置文件)

这里我们通过修改配置文件来设置,来设置主从结构,配置不同的端口号,来适配不同的客户端连接。

在两个配置文件中都增加一个slaveof来修改主从结构。

接着再kill redis进程,再重启。

slave no one(断开与住节点关系):

断开主从复制关系,而且这时候从节点中的数据也不会被抛弃。

通过该命令断开和6379 的主从关系。

如果想要回复就再通过slaveof 的方法再次连接。

只读:

从节点本来是只能读取,不能写的。如果我们修改了配置项,就可以写,但是数据只能从主节点到从节点,如果修改了主节点就感受不到了。

传输延迟(repl-disable-tcp-nodelay):

主节点和从节点之间通信用tcp通信,开启nagle就会增加延迟,节省带宽,在fps游戏场景画面变化比较快的情况,比较时候适合关闭,减小延迟。

拓扑结构:

一主一从:

若干个节点之间用什么形式进行组织连接,这里读请求的操作已经被分摊了,但是写的请求太多了也会增加压力,所以我们可以关闭主节点的AOF,只开启从节点的AOF,但是主节点挂了不能自动重启,重启没有AOF文件,进一步主从同步,把从节点数据删了。所以只能从从节点这里拉取数据。

一主多从:减少延迟        

实际开发中,读请求远大于,所以会把主节点同步给所有从节点。

缺点:网络带宽有比较大压力。

树形结构:节约带宽

读取可以从任何一个节点读取,但是写只能从主节点写,可以有效避免占用太多网络带宽,主节点就不用太大网络带宽。

主从复制过程:

缺点:一旦数据修改,同步延时更长了,同步的高度更高,更长,可能引起数据不太一致的情况。

  1. 保存主节点的ip和端口。
  2. 和主节点建立TCP连接。
  3. 向主节点发送ping,从节点得到pong命令。在应用层的角度看看能不能正常工作。
  4. 看主节点是否开启密码,密码验证成功了,才能匹配。
  5. 全量同步(全部数据打包)。
  6. 增量同步。

redis提供了psync命令,psync并不需要主动执行,redis会在建立好主从关系的时候,从节点自动执行。

写下info replication命令,查看数据。

replid主节点的id:

如果A和B之间的通信过程中出现了网络抖动,B是从节点就会认为A挂了,B就会自己生成一个replid,从节点晋升成主节点也会生成replid。但是B也会记得旧的id,这个旧的就是通过replid2,维护旧的关系,为了有朝一日能够回到原本的A的怀抱。 

offset(偏移量):

主节点的偏移量就是把修改的命令的字节数进行累加,就是偏移量。从节点的就描述了从节点的数据同步到哪里了。如果主节点和从节点的偏移量一样,这时候主从数据已经一致了。如果一个从节点replid(同步的主服务器)和偏移量相同,说明了两个redis服务器数据一模一样。

psync:

从节点向主节点发送请求,产生获取数据

offset为-1时,就是全量同步,会比较低效,如果是其他数字,就是部分进行同步,主节点会判定是否要全量。

全量复制的流程:

重新生成RDB文件,然后传输给从节点,因为原本的RDB文件可能比较老了,不能做到数据同步的过程。

在主节点生成和发送数据的时候(4和5操作),还会收到很多数据,此时数据仍然要发送给从节点,此时的数据就要放到缓冲区中。最后等RDB文件发完了,缓冲区中的数据也会发给从节点。如果从节点开启AOF,就会进行AOF转化等等。

引入无硬盘操作(diskless):直接把导出的数据进行网络传输,不涉及读写硬盘,直接进行数据加载。

部分复制(之前连接过)的流程:

由于全量复制的开销很大,但是从节点和主节点就只有比较少的数据不一样(网络抖动),就不需要全量复制了,这时候就需要部分复制了。

重新连接,要给从节点发送数据,我们就可以先把数据放在挤压缓冲区(类似于内存中的队列),用于存储还没发过去的数据。如果relpid不一样就要全量复制,如果一样,就看看能不能进行部分复制。offset看少的内容多不多,看看在不在挤压缓冲区范围内,在的话我们就直接部分复制(发送挤压缓冲区的某些数据),否则就进行全量复制。

实时复制的流程:

全量复制是刚连接上主节点的时候,进行数据初始化的工作,部分复制是全量复制的特殊情况,优化手段,目的和全量复制一样,实时复制,是主节点收到新的修改操作要同步给从节点的时候进行的操作。

从节点和主节点会建立一个TCP的长连接,从节点根据主节点的修改数据请求,修改内存中的数据,就能做到数据统一。实时复制得保证连接处于一个可用的状态(心跳包)。主节点每隔十秒发一个ping,从节点收到返回pong。从节点每隔一秒就给主节点发送特定请求,就会上报从节点同步数据进度。        

runid:

通过info server来查询,runid是每个节点都不同的,是用来标识redis的运行,用于支撑哨兵的功能实现。实际上和主从复制没什么关系。

缺点;

从节点多了,就会造成延时,而且主机挂了,从机不会变成主机,只能人工干预的方式。

对于主从复制就是主节点挂了,从节点就很迷茫了,但是从节点不能主动升级主节点,不能替换原本的角色。哨兵模式就能自动替换。

断开连接:

通过slave no one:

这时候从节点会直接晋升成主节点,从节点就直接出来单干了。

主节点挂了:

这时候不会主动晋升成主节点,必须通过人工干预的方式,回复主节点。

哨兵模式(redis-sentinel):

对于一定规模的redis集群,如果有一个服务器挂了,需要程序员手动重启,这样显然不科学,所以就引入哨兵模式,让redis自动重启或者让挂了的主节点的从节点找到别的节点当主节点。哨兵机制是通过独立的进程,和redis-server是独立的进程(只是起到监控的作用)。

通过哨兵监控现有的redis master和slave,这些会建立redis长连接,定期发送心跳包。如果一旦主节点挂了,就会和其他几个哨兵商量一下(防止出现误判)。

  1. 主节点挂了,哨兵就要发挥作用,此时就要多个哨兵共同认同这件事出现误判。
  2. 主节点挂了就会通过哨兵机制推选出一个哨兵老大,老大从现有的从节点中,推选出新的主节点。
  3. 挑选出新的主节点后,哨兵节点就会自动控制被选中的节点,执行slaveof no one。
  4. 哨兵机制就会通知客户端程序,告知新的主节点是谁,并且后续客户端操作就会针对新的主节点进行了。

redis哨兵核心功能:

  1. 监控。
  2. 自动的故障转移,能挑选新的节点,当作主节点。(核心)。
  3. 通知给所有使用的客户端。

一个哨兵节点可能会出现问题,可能自己也挂了,或者误判,分布式系统中应该避免单点。哨兵节点要弄奇数个,方便选举。

搭建redis主从结构和哨兵:

通过docker搭建隔离虚拟机环境:

docker实际上就是一台轻量级的虚拟机,,不用调用太多的硬件资源也能够做到类似于多个主机分离的形式。

下载:

通过在命令行敲下 1.apt install docker-compose,

2.通过以上的操作暂停redis服务器的服务,并且将进程直接杀死。3.接着使用docker获取redis的镜像(类似于可执行程序),docker pull redis:5.0.9 。

此处省略了安装docker的过程。

通过docker image观察运行的镜像,接下来就可以基于docker搭建redis环境了。

搭建环境:

通过docker分别搭建哨兵节点和redis服务器,我们通过两个配置文件(yml),一个用来配置哨兵,一个配置redis服务器,把具体要创建哪些容器,每个容器中的参数,描述清楚,后续通过一个简单的命令就可以批量启动了。

创建redis文件夹,在redis文件下面创建两个文件,分别是redis-data和redis-sentinel,在redis-data中编辑docker-compose,并且写入如下的配置。

version: '3.7'
services:
 master:
  image: 'redis:5.0.9'
  container_name: redis-master
  restart: always 
  command: redis-server --appendonly yes
  ports:
    - 6379:6379
 slave1:
  image: 'redis:5.0.9' 
  container_name: redis-slave1
  restart: always 
  command: redis-server --appendonly yes --slaveof redis-master 6379
  ports:
    - 6380:6379
 slave2:
  image: 'redis:5.0.9'
  container_name: redis-slave2
  restart: always 
  command: redis-server --appendonly yes --slaveof redis-master 6379
  ports:
    - 6381:6379

services表示要启动多个服务,其中有自己设定的slave1和slave2,docker容器里面的端口和外面的是不一样的,容器外面访问容器里面的端口,就要通过端口映射。冒号前面是宿主机的端口,冒号后面的是docker里面的端口。 访问宿主机的端口就等于访问容器里面的端口。

docker-compose up -d

通过该命令在后台启动docker。

通过info replication观察6379节点的使用情况,这里的slave1和2是docker自动分配的ip地址。

docker ps -a

docker ps -a 命令用于列出 Docker 主机上所有容器的信息,不管这些容器是正在运行的,还是已经停止的。与之相对的是单纯的 docker ps 命令,它仅会列出当前正在运行的容器。

同样的在这边我们也会在redis-sentinel中创建一个yml文件compose.yml文件。

这里配置三个哨兵节点防止挂掉,并且也是奇数个,方便投票选举。

version: '3.7'
services:
 sentinel1:
  image: 'redis:5.0.9'
  container_name: redis-sentinel-1
  restart: always
  command: redis-sentinel /etc/redis/sentinel.conf
  volumes:
    - ./sentinel1.conf:/etc/redis/sentinel.conf
  ports:
    - 26379:26379
 sentinel2:
  image: 'redis:5.0.9'
  container_name: redis-sentinel-2
  restart: always command: redis-sentinel /etc/redis/sentinel.conf
  volumes:
    - ./sentinel2.conf:/etc/redis/sentinel.conf
  ports:
   - 26380:26379
 sentinel3:
  image: 'redis:5.0.9'
  container_name: redis-sentinel-3
  restart: always
  command: redis-sentinel /etc/redis/sentinel.conf
  volumes:
    - ./sentinel3.conf:/etc/redis/sentinel.conf
  ports:
    - 26381:26379

这里我们配置不同的三个文件是因为哨兵节点在使用的时候会不停的对配置文件进行修改,所以我们创建了三个文件,但是这三个文件初始可以配置成一样的。

conf的配置文件配置内容:

bind 0.0.0.0
port 26379
sentinel monitor redis-master redis-master 6379 2
sentinel down-after-milliseconds redis-master 1000

此时再通过docker-compose up -d启动docker,就可以了。但是此时会出现一个问题,配置文件无法识别主节点的名字,此时就要将哨兵节点和redis服务器节点放一个局域网中。

如果单纯前面的启动方法就会自动创建局域网,

 networks:
   default:
     external:
        name: redis-data_default
version: '3.7'
services:
 sentinel1:
  image: 'redis:5.0.9'
  container_name: redis-sentinel-1
  restart: always
  command: redis-sentinel /etc/redis/sentinel.conf
  volumes:
    - ./sentinel1.conf:/etc/redis/sentinel.conf
  ports:
    - 26379:26379
 sentinel2:
  image: 'redis:5.0.9'
  container_name: redis-sentinel-2
  restart: always
  command: redis-sentinel /etc/redis/sentinel.conf
  volumes:
    - ./sentinel2.conf:/etc/redis/sentinel.conf
  ports:
   - 26380:26379
 sentinel3:
  image: 'redis:5.0.9'
  container_name: redis-sentinel-3
  restart: always
  command: redis-sentinel /etc/redis/sentinel.conf
  volumes:
    - ./sentinel3.conf:/etc/redis/sentinel.conf
  ports:
    - 26381:26379

networks:
   default:
     external:
        name: redis-data_default

停止容器启动。

docker-compose down

此时再启动就可以生效了。

sdown和odown:

slown是主观下线,就是本哨兵节点,认为该主节点挂了,odown是好几个节点都认为挂了,也就是法定票数,此时主节点挂了就被实锤了。当主节点挂掉之后再回来,就会变成从节点了。

主从切换具体流程(经典面试题):

主观下线

哨兵节点通过心跳包,判定redis是否正常工作,如果心跳包没有如约而至,就说明redis服务器挂了,此时是单方面认为节点挂了。

客观下线

多个哨兵节点都认为主节点挂了,也就是哨兵节点达到法定票数,哨兵们就认为真的挂了。

选出leader节点:

要让多个哨兵节点选出一个leader节点,然后让这个节点负责选一个从节点为新的主节点,哪个哨兵节点先发现主节点挂了,就会给自己投一票,其他哨兵就会赞成先发现的节点当作leader。总的个数超过总票数的一半,就成功选出了(手慢无)。奇数选举有利于选举。

选出从节点作为新的主节点:

从节点的优先级比较高的就胜选,一样的话就比较offset,数值越大就认为和主节点数据很接近,选较大的,都一样比较runid(redis启动时候随机生成的数字,大小全凭缘分)(就随便挑),选runid比较小的。

主节点指定好就会自动slave no one,成为master,再控制其他节点,让这些节点以新的master为主节点。

小结:

  •  哨兵节点不能只有⼀个,否则哨兵节点挂了也会影响系统可⽤性。
  • 哨兵节点最好是奇数个(大部分三个就够了),方便选举leader,得票更容易超过半数。
  • 哨兵节点不负责存储数据,仍然是redis主从节点负责存储。哨兵节点就不用配置很高的主机了。
  • 哨兵+主从复制解决的问题是"提高可用性",不能解决"数据极端情况下写丢失"的问题。
  •  哨兵+主从复制不能提⾼数据的存储容量(用集群可以),当我们需要存的数据接近或者超过机器的物理内存,这样的结构就难以胜任了。

Redis集群:

redis提供了集群模式,主要解决存储空间的问题,将每一份数据分成多个分片。每个分片存储到一个节点。

三种主流分片算法(经典面试题):

哈希求余:

由于redis都是键值对的结构,可以通过key进行hash函数的计算进行映射到数组下标,然后进行存储到对应的数组中,后续查询也是同样的流程。

缺点:看似很好用,但是在redis集群服务器需要扩容的时候就会出现问题。此时对哈希函数计算后,得到16进制数,后续进行除以n,如果数据除以n不同就要进行数据搬运。不仅主节点要进行数据搬运,从节点也要进行搬运。搬运量比较大,开销大。

一致性哈希算法:

对某个值进行哈希计算,计算出来的位置往下找的第一个分片就是该对应的位置。比如计算的A值在0-1号分片之间,最后的结果就是1号分片。管辖区域如图所示。

在哈希求余这种的,key属于哪个分片是交替出现的,就导致了搬运成本比较大。

扩容:将分片大小进行压缩,如图把原本归0号的元素搬到三号。虽然搬运的成本低了,但是这几个数据量不均匀(数据倾斜)。

哈希槽分区算法(redis集群采用的算法):

hash_slot就是哈希槽,把数据放到哈希槽中,然后将这16384个槽位分给不同的分片,此时就可以认为这三个分片的数据比较均匀,这种实际上就是把一致性哈希和哈希求余结合到一起。

此时会用位图来区分是否持有该槽位号,比如如上0号分片的0到5461的位图上都是1,也就是持有的意思。

如果想要扩容就对槽位重新分配。针对某个分片不一定是连续的区间,,可以是离散的,或者多个区间组合一起的。有的槽位可能有多个key,有的槽位可能没有key。                

在上述过程中只有移动的槽位才需要搬运。

节点之间会通过心跳包的方式通知对方自己有哪些槽位,每个周期都发一次心跳包,吃网络带宽 ,16384个槽位(位图)通信大概是2kb,但是如果再增加槽位,通信的开销就更大了。

基于docker搭建redis集群:

/首先需要在redis的yml配置文件的目录下,将docker停掉

docker-compose down

创建docker.yml和generate.sh文件。

这个shell脚本就类似于一编程语言,可以写入逻辑语句等,需要进行批量操作,就写入shell脚本中。

  1. 业务端口:为了实现业务数据通信的,响应redis客户端请求。
  2. 管理端口:用来实现一些管理上的任务。

在generate.sh文件中写入以下数据,进行自动执行和配置。

bind 0.0.0.0
protected-mode no
appendonly yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.30.0.10${port}
cluster-announce-port 6379
cluster-announce-bus-port 16379
EOF
done

# 注意 cluster-announce-ip 的值有变化.
for port in $(seq 10 11); \
do \
mkdir -p redis${port}/
touch redis${port}/redis.conf
cat << EOF > redis${port}/redis.conf
port 6379
bind 0.0.0.0
protected-mode no
appendonly yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.30.0.1${port}
cluster-announce-port 6379
cluster-announce-bus-port 16379
EOF
done
bash generate.sh

再通过该命令来执行该shell文件,就能够创建出很多相关文件和配置了。

通过如上两个命令检查redis是否还存在,如果都准确无误之后,使用

docker-compose up -d

在后台启动。

接着我们构建集群:配置两个从节点,但是这里九个节点按照怎样的模式进行主从配置,是不一样的。在linux中执行。

redis-cli --cluster create 172.30.0.101:6379 172.30.0.102:6379 
172.30.0.103:6379 172.30.0.104:6379 172.30.0.105:6379 172.30.0.106:6379 
172.30.0.107:6379 172.30.0.108:6379 172.30.0.109:6379 --cluster-replicas 2

槽位分配:

输入yes执行配置。

配环境的步骤:

  1. 生成每个redis配置文件。yml和gegerate
  2. 使用docker创建出十一个节点(docker-compose up)。
  3. 使用rediscli构建集群命令。

当连接上其中一个redis服务器就等于连接整个集群,通过cluster nodes查看集群信息。

使用集群存储键值对:

此时数据分片了,经过计算key要在该redis节点下才能插入,所以我们可以转化为该节点,但是这样很麻烦,我们就可以在启动redis 的时候加一个-c,此时就能重定向到对应的分片。即使是从节点也能重定向。客户端可以直接转发给真正的分片了。

集群尽量不要一次插入多个或者查找多个。

如果主节点挂了,就会有从节点代替主节点,

故障判定:

这里的故障转移和哨兵模式还有点不一样。

  1. 故障判定,识别出某个节点是否挂了,节点之间会传输心跳包(ping,pong),心跳包里面包含了很多集群的信息(哈希槽位等信息)。
  2. (A节点)每个节点每秒钟都会给某些节点(B节点)随机发送心跳包,这样做是避免如果每个节点往另外节点发送心跳包的话,就会导致网络开销太大。
  3. 如果不能如期回应的话,就会重置TCP连接,连接失败的话就会把不能连接的节点设置为PFAIL状态(主观下线)。 
  4. 此时就会通过redis内部的协议,和其他节点沟通,看看确认B节点是不是真的挂了。
  5. 如果其他节点也认为B挂了,并且认为B挂了的人数超过一半,A就会把B设置为(FAIL)客观下线,并且告诉其他节点。

故障迁移(Raft算法):

如果是从节点的话,就不用进行故障转移。

  1. 如果是主节点挂了,从节点将判定是否有资格参选资格,如果主节点和从节点之间已经太久没有通信过了,那就认为主从之间数据相差很大,就失去竞选资格。
  2. 有资格的节点,就会休眠一定的时间,offset越大,排名越靠前,此时休眠时间就相对更短,就先苏醒。
  3. 当先苏醒的就会进行拉票操作(只有主节点有投票的资格)。
  4. 如果得到的票数超过主节点的数量的一半,就会晋升成主节点。  
  5. 此时新的主节点还会把自己是主节点的消息告诉其他人。

此时是直接投出新的主节点,但是哨兵模式是投出leader,再投出主节点。实际上就是先唤醒的就变成主节点(手慢无)。

集群宕机的情况:

  1. 某个分片里的所有主节点和从节点都挂了,该分片就无法提供数据服务了。

  2. 某个主节点挂了,但是没有从节点。

  3. 超过半数的主节点挂了,如果一系列master都挂了,就说明集群出现大问题了。

集群扩容

前面我们已经有九个主机,现在我们将另外两个主机也加入到集群中,集群扩容是一件风险高,成本大的操作。

添加集群:将要添加的接待你随机添加到某个节点中add-node后的第⼀组地址是新节点的地址.第⼆组地址是集群中的任意节点地址

redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379

但是此时还没有为新增加的节点分配哈希槽。

接下来就是重新分配slot,把之前的一些slots分配出来,分给新的master。

redis-cli --cluster reshard 172.30.0.101:6379
//重新分配slot

在客户端中上述命令,切分slots。。

输入从哪些来搬运槽位,all就是其他各分一点,done就是手动指定。

此时不仅是slot重新划分,也是把数据重新划分。

redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --cluster-slave --cluster-master-id [172.30.1.110 节点的 nodeId]

增加从节点。


缓存:

速度快的可以作为速度慢的缓存,redis主要作为数据库的缓存。

把一些频繁读取的数据保存到缓存上面,先查询redis,如果redis中没有再查mysql,虽然mysql只能存少数数据,但是大部分热点数据都存在redis中。

缓存更新策略:

定期生成:

会把访问的数据以日志的形式记录下来,在一定时间内统计,把使用过的数据给记录下来,并且做一个使用频率排行,就可以把一些词变成热点词。把热点词的搜索结果,就可以直接放到redis缓存了。

统计热词,进行搜索,把搜索结果缓存到服务器上面,然后控制服务器定期更新重启。但是比如春晚在网上7.8点的时候会有很多人搜索,这时候定时更新就不行了,所以得使用实时更新。

实时生成:

查询redis中有数据直接返回,没有数据后在mysql中查询,查询后将数据放到redis中。但是这样也会让redis内存爆满,所以我们引入了内存淘汰策略。

redis内存淘汰策略(经典面试题):

  1. FIFO(First In First Out):先进先出把缓存中存在时间最久的(也就是先来的数据)淘汰掉。
  2. LRU(LeastRecentlyUsed):淘汰最久未使⽤的记录每个key的最近访问时间.把最近访问时间最⽼的key淘汰掉。
  3. LFU(LeastFrequentlyUsed):淘汰访问次数最少的记录每个key最近⼀段时间的访问次数.把访问次数最少的淘汰掉。
  4. Random随机淘汰:从所有的key中抽取幸运⼉被随机淘汰掉。

redis中有一个配置项,就可以设置redis采用哪种淘汰机制,如下:

缓存预热,缓存穿透,缓存雪崩,缓存击穿(面试):

缓存预热:

定期生成的数据不涉及预热,但是实时生成的数据涉及到预热。

redis服务器首次接入的时候,服务器是没有数据的,如果这时候有很多请求,会直接交给mysql,就比较不友好,缓存预热就是为了解决上述问题。

通过离线的方式,通过统计的方法,先把热点数据先导入一批,这时候就能帮助mysql承担很多压力,随着使用新的热点数据淘汰掉旧的数据。

缓存穿透(penetration):

查询的某个key在redis中没有,mysql中也没有,这样的数据在后续不断查询,也会给mysql带来很多压力,因为查完redis还要查mysql。

原因:(经典)业务设计不合理,没有涉及到校验,运维不小心误删了。

解决方案:

  1. 如果查询不存在的key,在数据库中也查不到,那就把他写入redis,但是value设置成一个非法值比如空字符串。
  2. 引入布隆过滤器,每次查询redis和mysql,看看是否在布隆过滤器上存在,把所有的key都放到布隆过滤器中(以比较小的空间开销,比较快的时间速度,看看key是否存在)。
缓存雪崩:

短时间内,redis上大规模的key失效,导致缓存命中率陡然下降,使得mysql的压力瞬间上升,甚至直接宕机。

原因:redis'挂了,也有可能集群模式下大量集群挂了,也可能同时设置的key给redis,过期时间也一样,同时过期,一下子删除很多key。

解决方案:1.加强监控报警,通过哨兵。2.不给key涉及过期时间,或者设置随机过期时间,不让一下子很多key都过期了。

缓存击穿(breakdown)缓存雪崩的特殊情况:

某个非常热点的key过期了,导致访问数据库的某个key访问量剧增,使得数据库遭受不住。

解决方法:

  1. 基于统计发现热点key,并且设置永不过期。
  2. 对服务器降级,比如服务器提供的功能有十个,适当关闭某些功能,保留核心功能,比如通过分布式锁,限制用户访问的频率。

分布式锁

在⼀个分布式的系统中,也会涉及到多个节点访问同⼀个公共资源的情况。此时就需要通过锁来做互斥控制,避免出现类似于线程安全的问题。⽽java的synchronized,这样的锁都是只能在当前进程中⽣效,在分布式的这种多个进程多个主机的场景下就⽆能为力了。此时就需要使⽤到分布式锁。   

setnx

当我们加锁了之后,当其他线程想要同样竞争相同资源就得阻塞等待。

两个不同的客户端通过服务器上进行操作,两个客户端无法通信,实际上买票操作就是在redis服务器上进行买票操作,设置一个特殊的key-value,如果其他服务器也想买票的话,就也尝试设置key和value,设置失败就代表加锁失败(setnx)。 

实际上其实使用mysql的事务也能串行化执行,实际可能要访问的不仅仅是mysql,可能是其他的存储介质。

使用setnx进行加锁,针对解锁就使用del解锁,但是服务器掉电后,就可能出现无法解锁,但是我们可以设置超时时间,即使服务器挂了,也可以解锁。(必须使用set nx ex,不能用expire),此时可能会出现一个命令成功一个失败,redis多个命令没有mysql原子性,相对来说一条命令更加稳妥。 

校验id

(bug)服务器1执行了加锁,服务器2执行了解锁。

  1. 给服务器编号。
  2. 在加锁的时候key表示对哪个资源进行加锁(车次),value可以存储服务器的编号。表示是哪个服务器加上的

解锁的时候,判断value是不是当前执行加锁的编号,如果是才能解锁,不是的话就不能解锁(解锁失败)。

引入lua脚本:

同一个服务器不同进程删除。

lua实际上也是事务的的一种替代,就可以解决上述的问题。

看门狗:

加锁的时候给key设置过期时间,得设置合适的时间,更好的情况得是动态设置。

动态续约(看门狗)一个专门的服务器:比如刚开始初始过期时间是1s,就提前还剩300ms的时候,如果当前任务没有执行完,就再续上1s,如果服务器中途崩溃了,就没有人负责续约,锁就能在合适时间释放。

redlock算法:

在上面哨兵模式引入加锁(设置key-value),会出现还没把数据同步给从节点,主节点就挂了,即使从节点升级主节点,但是没有这个加锁的key。

针对一定的顺序都对多个redis服务器进行加锁,如果某个节点挂了,加不上锁,就给下一个加就可以,如果加上key'成功的个数超过总数的一半,此时就认为加锁成功。同理解锁,也要把全部都设置一遍解锁。