Bootstrap

redis-分布式缓存详解

redis单节点问题
1.数据丢失问题,数据在内存中,重启就会丢失
  解决方案: 实现redis数据持久化
2.并发访问问题,性能不错但是高并发情况下还是会有问题
  解决方案: 搭建主从集群,实现读写分离
3.故障恢复问题,单节点部署,一旦出现问题,就出现整个微服务影响的范围非常大
  解决方案: 利用redis哨兵实现健康检测和自动恢复
4.存储能力问题,单节点内存难以满足海量数据的存储
  解决方案: 搭建分片集群,利用插槽机制实现动态扩容

问题一: redis持久化
    有两种解决方式RDB和AOF
    RDB: redis数据备份文件,也叫做redis数据快照,简单来说就是把内存中的所有数据都记录到磁盘中,当redis实例故障重启后,从磁盘读取快照文件,恢复数据
         快照文件称为RDB文件,默认是保存是保存在当前的运行目录,默认文件名dump.rdb
         执行save命令就会进行一次快照保存(save命令是主进程来执行,会影响效率)
         一般情况下执行bgsave开启子进程执行RDB,避免住进程收到影响
         主动关闭redis会默认自动执行一次RDB。
         除了主动执行和关机执行,RDB还会根据配置文件中的配置来执行。
         save 900 1       //900秒内,如果至少有1个key被修改,则执行bgsave,如果是save "" 则表示禁用RDB。以下两个同理
         save 300 10
         save 60  10000
         RDB的底层执行流程: bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据,完成fork后读取读取内存数据并写入RDB文件中
         RDB缺点: RDB执行间隔比较长,两次RDB之间写入的数据有丢失的风险。 fork主进程,压缩,写出RDB文件都比较耗时
    
    AOF: redis处理的每一个命令都会记录在AOF文件中,可以看做是命令日志文件(默认关闭)
         开启需要修改redis.config文件,将appendonly yes(默认为no),appendfilename "appendonly.aof"(修改aof文件名称)
         aof记录日志的频率有三种,通过redis.config文件进行修改
         第一种表示每执行一次写的命令,都记录在aof文件中   appendfsync always
         第二种表示写命令执行完先放入aof缓存区,然后表示每隔一秒将缓存区数据写到aof文件,是默认方案  appendfsync everysec
         第三种表示写命令执行完先放入aof缓存区,由操作系统决定何时将缓存区数据写会磁盘   appendfsync no
         
         因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。
         通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。
         比如执行了三条语句 set name 123,  set name 456, set name 789  执行了bgrewriteaof命令之后会合并为 mset name 123 456 789
         
         Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置:
         auto-aof-rewrite-percentage 100  //AOF文件比上次文件 增长超过多少百分比则触发重写
         auto-aof-rewrite-min-size 64mb   //AOF文件体积最小多大以上才触发重写

         
问题二: 并发访问问题
        单节点redis并发能力是有上限的,要进一步提高redis的并发能力,就需要搭建主从集群,实现读写分离     
        因为redis主要的业务是读的操作,写的操作比较少,一般都是master节点接收读的操作,slave节点接收写的操作,然后master节点将数据同步到各个slave节点上,实现数据一致
        
        集群搭建(一主两从):
        1. 这里我们会在同一台虚拟机中开启3个redis实例,模拟主从集群,信息如下:
           | 192.169.666.888 | 7001 | master |
           | 192.169.666.888 | 7002 | slave  |
           | 192.169.666.888 | 7003 | slave  |
        2. 要在同一台虚拟机开启3个实例,必须准备三份不同的配置文件和目录,配置文件所在目录也就是工作目录。 
           我们创建三个文件夹,名字分别叫7001、7002、7003:
           # 进入/tmp目录
            cd /home
           # 创建目录
            mkdir 7001 7002 7003
           恢复原始配置
           修改redis-6.2.4/redis.conf文件,将其中的持久化模式改为默认的RDB模式,AOF保持关闭状态。
           # 开启RDB
           # save ""
           save 3600 1
           save 300 100
           save 60 10000

           # 关闭AOF
           appendonly no
           
           拷贝配置文件到每个实例目录
           然后将redis-6.2.4/redis.conf文件拷贝到三个目录中
           cp redis-6.2.4/redis.conf 7001
           cp redis-6.2.4/redis.conf 7002
           cp redis-6.2.4/redis.conf 7003
           
           修改每个实例的端口、工作目录
           修改每个文件夹内的配置文件,将端口分别修改为7001、7002、7003,将rdb文件保存位置都修改为自己所在目录
           sed -i -e 's/6379/7001/g' -e 's/dir .\//dir \/home\/7001\//g' 7001/redis.conf
           sed -i -e 's/6379/7002/g' -e 's/dir .\//dir \/home\/7002\//g' 7002/redis.conf
           sed -i -e 's/6379/7003/g' -e 's/dir .\//dir \/home\/7003\//g' 7003/redis.conf
           
           虚拟机本身有多个IP,为了避免将来混乱,我们需要在redis.conf文件中指定每一个实例的绑定ip信息,格式如下:
           # redis实例的声明 IP
           replica-announce-ip 123.57.87.143
           
           每个目录都要改,我们一键完成修改
           sed -i '1a replica-announce-ip 192.169.666.888' 7001/redis.conf
           sed -i '1a replica-announce-ip 192.169.666.888' 7002/redis.conf
           sed -i '1a replica-announce-ip 192.169.666.888' 7003/redis.conf
         
           为了方便查看日志,我们打开3个ssh窗口,分别启动3个redis实例,启动命令:
           # 第1个
           redis-server 7001/redis.conf
           # 第2个
           redis-server 7002/redis.conf
           # 第3个
           redis-server 7003/redis.conf
           
           开启主从关系
           现在三个实例还没有任何关系,要配置主从可以使用replicaof 或者slaveof(5.0以前)命令。
           有临时和永久两种模式:
           修改配置文件(永久生效)
                在redis.conf中添加一行配置: slaveof <masterip> <masterport>
           使用redis-cli客户端连接到redis服务,执行slaveof命令(重启后失效):
                slaveof <masterip> <masterport>
           注意: 在5.0以后新增命令replicaof,与salveof效果一致。
           
           这里我们为了演示方便,使用方式二。

           通过redis-cli命令连接7002,执行下面命令:
            # 连接 7002
            redis-cli -p 7002
            # 执行slaveof
            slaveof 192.169.666.888 7001
            
           通过redis-cli命令连接7003,执行下面命令:
            # 连接 7003
            redis-cli -p 7003
            # 执行slaveof
            slaveof 192.169.666.888 7001
 
           然后连接 7001节点,查看集群状态:
            # 连接 7001
            redis-cli -p 7001
            # 查看状态
            info replication

           测试:
            - 利用redis-cli连接7001,执行set num 123

            - 利用redis-cli连接7002,执行get num,再执行set num 666

            - 利用redis-cli连接7003,执行get num,再执行set num 888

        可以发现,只有在7001这个master节点上可以执行写操作,7002和7003这两个slave节点只能执行读操作(完成^_^)

        redis主从数据同步原理(全量同步):
        1. slave节点请求增量同步
        2. master节点判断replid,发现不一致,拒绝增量同步
        3. master将完整内存数据生成RDB,发送RDB到slave
        4. slave清空本地数据,加载master的RDB
        5. master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave
        6. slave执行接收到的命令,保持与master之间的同步
        
        redis主从数据同步原理(增量同步):
        slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave
        什么时候执行全量同步?
        slave节点第一次连接master节点时
        slave节点断开时间太久,repl_baklog中的offset已经被覆盖时
        什么时候执行增量同步?
        slave节点断开又恢复,并且在repl_baklog中能找到offset时
        
        
问题三:故障恢复问题
    slave节点宕机后重启可以找到master节点同步数据,那master节点宕机了怎么办?
    解决: redis提供了哨兵sentinel机制来实现主从集群的自动故障恢复。
    监控: sentinel会不断检查master和slave是否按照预期工作
    自动故障恢复: 如果master故障,sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
    通知: sentinel充当redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给redis的客户端    
        
    Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
    主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
    客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。
    
    一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:
    1. 首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点 (与master断开时间太长了,丢失的数据越多)
    2. 然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举 
    3. 如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高 (offset代表当前节点与master数据同步进度)
    4. 最后是判断slave节点的运行id大小,越小优先级越高。

    选取master结束后,故障转义的步骤
    1. sentinel给备选的slave1节点发送slaveof no one命令,让该节点成为master
    2. sentinel给所有其它slave发送slaveof 192.169.666.888 7002 命令,让这些slave成为新master的从节点,开始从新的master上同步数据。
    3. 最后,sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点
    
    搭建哨兵集群
    集群状态(哨兵集群3个实例,redis集群3个实例)    
    三个sentinel实例信息如下:
    | s1   | 192.169.666.888 | 27001 |
    | s2   | 192.169.666.888 | 27002 |
    | s3   | 192.169.666.888 | 27003 |    
    1. 要在同一台虚拟机开启3个实例,必须准备三份不同的配置文件和目录,配置文件所在目录也就是工作目录。
       我们创建三个文件夹,名字分别叫s1、s2、s3: 
        # 进入/home目录
        cd /home
        # 创建目录
        mkdir s1 s2 s3
    2. 然后我们在s1目录创建一个sentinel.conf文件,添加下面的内容:
       port 27001
       sentinel announce-ip 192.169.666.888
       sentinel monitor mymaster 192.169.666.888 7001 2
       sentinel down-after-milliseconds mymaster 5000
       sentinel failover-timeout mymaster 60000
       dir "/home/s1"
       
       配置解读:
       port 27001: 是当前sentinel实例的端口
       sentinel monitor mymaster 192.169.666.888 7001 2: 指定主节点信息
       mymaster: 主节点名称,自定义,任意写
       192.169.666.888 7001: 主节点的ip和端口
       2: 选举master时的quorum值
       
       然后将s1/sentinel.conf文件拷贝到s2、s3两个目录中(在/tmp目录执行下列命令):
       cp s1/sentinel.conf s2
       cp s1/sentinel.conf s3
       
       修改s2、s3两个文件夹内的配置文件,将端口分别修改为27002、27003:
       sed -i -e 's/27001/27002/g' -e 's/s1/s2/g' s2/sentinel.conf
       sed -i -e 's/27001/27003/g' -e 's/s1/s3/g' s3/sentinel.conf
     
    3. 启动 
       为了方便查看日志,我们打开3个ssh窗口,分别启动3个redis实例,启动命令:
       # 第1个
       redis-sentinel s1/sentinel.conf
       # 第2个
       redis-sentinel s2/sentinel.conf
       # 第3个
       redis-sentinel s3/sentinel.conf
    
       启动redis集群
       # 第1个
       redis-server 7001/redis.conf
       # 第2个
       redis-server 7002/redis.conf
       # 第3个
       redis-server 7003/redis.conf

    4. 测试
       将7001节点关机模拟宕机,sentinel会选取一个成为主节点,我这里测试,发现选取了7002为master。
       连接7002 info replication查看集群状态,发现7002为master集群,在7002上执行set操作可以成功
       连接7003 get 7002上set的值可以成功,7003 set数据提示失败。
       以上证明,当master宕机之后,sentinel选取了新的节点成为了master。
       将7001开启,再次连接到7002查看集群状态,发现7002还是master,7001和7003都是集群中的salve


    springboot整合RedisTemplate连接redis集群与整合redis-sentinel集群:
    1. 添加pom
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-data-redis</artifactId>
       </dependency>
    
    2.添加配置(注意: 这里配置的不是redis集群地址,是sentinel集群地址。因为在sentinel模式下主从的地址是有可能变化的,所以不能写死)
      spring:
          redis:
            sentinel:
              master: mymaster #指定集群名称
              nodes:           #指定redis-sentinel集群信息
                - 192.169.666.888:27001
                - 192.169.666.888:27002
                - 192.169.666.888:27003    
    
    3.配置读写分离
      @Bean
      public LettuceClientConfigurationBuilderCustomizer configurationBuilderCustomizer(){
        return configBuilder -> configBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
      }
      这个配置表示优先从slave节点读,slave阶段不可用时再从master节点读


问题四:存储能力问题,和如果写的操作也很多怎么办?
       这里主要用到了redis的分片集群来解决
       这里可以学习到 1.redis分片集群  2.散列插槽的原理  3.集群伸缩  4故障转移  5.RedisTemplate访问分片集群
       
       redis分片集群
       主从和哨兵模式可以解决高可用,高并发问题,但是依然有两个问题没有解决
       第一个问题海量数据存储问题
       第二个问题高并发写的问题
       
       使用分片集群可以解决以上的问题。
       分片集群的特征(不在需要哨兵模式了,但是具备了哨兵模式的功能)
       - 集群中有多个master,每个master保存不同的数据
       - 每个master都可以有多个slave节点
       - master之间通过ping来检测彼此健康状态
       - 客户端请求可以访问集群任意节点,最终都会被转发到正确的节点上
       
       redis分片集群的搭建
       步骤一: 集群结构
               分片集群需要的节点数量较多,这里我们搭建一个最小的分片集群,包含3个master节点,每个master包含一个slave节点,结构如下:
               这里我们会在同一台虚拟机中开启6个redis实例,模拟分片集群,信息如下:
               | 192.169.666.888 | 7001 | master |
               | 192.169.666.888 | 7002 | master |
               | 192.169.666.888 | 7003 | master |
               | 192.169.666.888 | 8001 | slave  |
               | 192.169.666.888 | 8002 | slave  |
               | 192.169.666.888 | 8003 | slave  |
               
        步骤二: 准备实例和配置
                删除之前的7001、7002、7003这几个目录,重新创建出7001、7002、7003、8001、8002、8003目录:
                # 进入/tmp目录
                cd /home
                # 删除旧的,避免配置干扰
                rm -rf 7001 7002 7003
                # 创建目录
                mkdir 7001 7002 7003 8001 8002 8003
                
                在/home下准备一个新的redis.conf文件,内容如下:
                port 6379
                # 开启集群功能
                cluster-enabled yes
                # 集群的配置文件名称,不需要我们创建,由redis自己维护
                cluster-config-file /home/6379/nodes.conf
                # 节点心跳失败的超时时间
                cluster-node-timeout 5000
                # 持久化文件存放目录
                dir /home/6379
                # 绑定地址
                bind 0.0.0.0
                # 让redis后台运行
                daemonize yes
                # 注册的实例ip
                replica-announce-ip 192.169.666.888
                # 保护模式
                protected-mode no
                # 数据库数量
                databases 1
                # 日志
                logfile /home/6379/run.log
                
                将这个文件拷贝到每个目录下:
                # 进入/home目录
                cd /home
                # 执行拷贝
                echo 7001 7002 7003 8001 8002 8003 | xargs -t -n 1 cp redis.conf
                
                修改每个目录下的redis.conf,将其中的6379修改为与所在目录一致:
                # 进入/home目录
                cd /home
                # 修改配置文件
                printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t sed -i 's/6379/{}/g' {}/redis.conf
                
        步骤三: 启动
                因为已经配置了后台启动模式,所以可以直接启动服务:
                # 进入/home目录
                cd /home
                # 一键启动所有服务
                printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-server {}/redis.conf
                
                通过ps查看状态:
                ps aux|grep redis 或 ps -ef | grep redis
                
                如果要关闭所有进程,可以执行命令:
                ps -ef | grep redis | awk '{print $2}' | xargs kill
                或者(推荐这种方式):
                printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-cli -p {} shutdown
                
        步骤四: 创建集群
                虽然服务启动了,但是目前每个服务之间都是独立的,没有任何关联。        
                我们需要执行命令来创建集群,在Redis5.0之前创建集群比较麻烦,5.0之后集群管理命令都集成到了redis-cli中。
                
                Redis5.0之前:
                Redis5.0之前集群命令都是用redis安装包下的src/redis-trib.rb来实现的。因为redis-trib.rb是有ruby语言编写的所以需要安装ruby环境。
                # 安装依赖
                yum -y install zlib ruby rubygems
                gem install redis
                然后通过命令来管理集群:
                # 进入redis的src目录
                cd /tmp/redis-6.2.4/src
                # 创建集群
                ./redis-trib.rb create --replicas 1 192.169.666.888:7001 192.169.666.888:7002 192.169.666.888:7003 192.169.666.888:8001 192.169.666.888:8002 192.169.666.888:8003
                
                Redis5.0以后:
                我们使用的是Redis6.2.4版本,集群管理以及集成到了redis-cli中,格式如下:
                redis-cli --cluster create --cluster-replicas 1 192.169.666.888:7001 192.169.666.888:7002 192.169.666.888:7003 192.169.666.888:8001 192.169.666.888:8002 192.169.666.888:8003
                命令说明:
                    redis-cli --cluster 或者 ./redis-trib.rb: 代表集群操作命令
                    create: 代表是创建集群
                    --replicas 1 或者 --cluster-replicas 1: 指定集群中每个master的副本(就是slave节点)个数为1,此时 节点总数 ÷ (replicas + 1)  得到的就是master的数量。
                    因此节点列表中的前n个就是master,其它节点都是slave节点,随机分配到不同master
                
                注意: 这里如果创建集群的时候遇到Waiting for the cluster to join ....................................... 然后无尽的等待。请参照下面这篇文章修改
                https://www.jianshu.com/p/250f5da36b49/
                
                通过命令可以查看集群状态: 
                redis-cli -p 7001 cluster nodes
                
                
        步骤五: 测试
                尝试连接7001节点,存储一个数据:        
                # 连接
                redis-cli -p 7001
                # 存储数据
                set num 123
                # 读取数据
                get num
                # 再次存储
                set a 1
                结果悲剧了: 会提示失败
                集群操作时,需要给redis-cli 加上 -c 参数才可以: redis-cli -c -p 7001
                
        散列插槽的原理
        Redis会把每一个master节点映射到0~16383共16384个插槽(hash slot)上,查看集群信息时就能看到:
        数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,分两种情况:
        key中包含"{}",且“{}”中至少包含1个字符,“{}”中的部分是有效部分
        key中不包含“{}”,整个key都是有效部分
        例如:key是num,那么就根据num计算,如果是{itcast}num,则根据itcast计算。计算方式是利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值。
        
        问题:
            Redis如何判断某个key应该在哪个实例?
            将16384个插槽分配到不同的实例
            根据key的有效部分计算哈希值,对16384取余
            余数作为插槽,寻找插槽所在实例即可
            如何将同一类数据固定的保存在同一个Redis实例?
            这一类数据使用相同的有效部分,例如key都以{typeId}为前缀
        
                
        集群伸缩问题
        redis-cli --cluster提供了很多操作集群的命令,可以通过下面方式查看:
            redis-cli --cluster help
        比如,添加节点的命令:
            add-node       new_host:new_port existing_host:existing_port
                 --cluster-slave
                 --cluster-master-id <arg>
        
        案例演示: 
        需求:
        启动一个新的redis实例,端口为7004
        添加7004到之前的集群,并作为一个master节点
        给7004节点分配插槽,使得num这个key可以存储到7004实例
        1. 新建一个文件7004
        2. 拷贝redis.conf配置文件到7004中
        3. 修改配置文件中的端口号
        4. redis-server 7004/redis.conf 运行7004
        5. 将7004加入到集群中   redis-cli --cluster add-node 192.169.666.888:7004 192.169.666.888:7001
        6. 分配插槽,不然新增的节点没有任何意义 执行重新分配插槽的命令
           redis-cli --cluster reshard 192.169.666.888:7001
           然后选择移动的插槽数量输入: 3000
           然后选择谁接收这部分插槽: 把7004的id复制过来,就代表7004来接收这部分插槽
           然后选择从哪里作为数据源来拷贝: 把7001的id复制过来
           然后输入done  表示结束
        
        需求:
        删除7004这个实例
        1.将插槽移动到7001中(移动步骤同上)
        2.然后删除7004  redis-cli --cluster del-node 192.169.666.888:7004 2e9ee91b50676fe00bd8471e91ada1d36a337264
        
                
        故障转移(自动,发生意外宕机)
        分片集群虽然没有哨兵,但是它也具备故障转移的功能,我们一起验证一下这个功能
        当集群中有一个master宕机会发生什么呢?
        首先是该实例与其它实例失去连接
        然后是疑似宕机:
        最后是确定下线,自动提升一个slave为新的master:
        验证:
        1. 通过命令监控集群: watch redis-cli -p 7001 cluster nodes
        2. 使用命令redis-cli -p 7002 shutdown
        3. 发现8001变成了master
        4. 重启7002之后,7002会加入到集群中,但是还是slave节点
        
        
        (手动故障转移, 机器升级,替换机器时使用)
        利用cluster failover命令可以手动让集群中的某个master宕机,切换到新的redis机器上,连接redis执行cluster failover命令的这个slave节点,实现无感知的数据迁移。其流程如下:
        手动的Failover命令支持三种不同模式(参数):
            缺省:默认的流程,如图1~6歩
            force:省略了对offset的一致性校验
            takeover:直接执行第5歩,忽略数据一致性、忽略master状态和其它master的意见

        
        使用RedisTemplate访问分片集群
        RedisTemplate底层同样基于lettuce实现了分片集群的支持,而使用的步骤与哨兵模式基本一致:
        步骤:
        1.引入redis的starter依赖
          <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
          </dependency>
        2.配置分片集群地址
        3.配置读写分离
        4.与哨兵模式相比,其中只有分片集群的配置方式略有差异,如下:
          spring:  
            redis:
                 cluster:
                 nodes: # 指定分片集群的每一个节点信息        
                  - 192.168.150.101:7001
                  - 192.168.150.101:7002
                  - 192.168.150.101:7003
                  - 192.168.150.101:8001
                  - 192.168.150.101:8002
                  - 192.168.150.101:8003

        (完结散花)

;