Redis概述与进阶技术点

前述:

一直有用redis,但对redis的了解很片面,这次花了些时间,系统性的将redis了解的一遍,在这里记录,如果疑惑、补充、不对的地方,希望你可以及时留言交流!

参考文档:redis持久化应用场景+解决方案_lingchen336的博客-CSDN博客_redis持久化应用场景

基本概念

      • redis是什么?(Redis的基础介绍与安装使用步骤 - 简书)
        1.基于内存亦可持久化的日志型、Key-Value数据库。
        2.支持存储的value类型(5种数据结构):string(字符串)、list(链表)、set(集合)、zset(sorted set 有序集合)和hash(哈希类型)。
        3.数据都是缓存在内存中。
        4.redis会周期性的把更新的数据写入磁盘(rdb)或者把修改操作写入追加的记录文件(aof),并且在此基础上实现了master-slave(主从)同步。
      • 为什么用redis(Redis的基础介绍与安装使用步骤 - 简书)
        1.速度快
        2.支持丰富数据类型
        3.支持事务,操作都是原子性。原子性就是对数据的更改要么全部执行,要么全部不执行。
        4.丰富的特性。可用于缓存,消息,按key设置过期时间,过期后将会自动删除。
        5.可持久化,容灾能力好。
      • redis适用场景(Redis的基础介绍与安装使用步骤 - 简书)
        1.会话缓存
        2.全页缓存(FPC)
        3.队列:Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得Redis能作为一个很好的消息队列平台来使用。
        4.排行榜/计数器:Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)。
        5.发布/订阅
      • memcacheredis的区别都有哪些?(Redis的基础介绍与安装使用步骤 - 简书)
        1.存储方式
        Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小
        Redis有部份存在硬盘上,这样能保证数据的持久性
        2.支持的数据类型
        Memcache对数据类型支持相对简单。
        Redis有复杂的数据类型
        3.底层模型不同
        Redis直接自己构建了VM 机制(什么是Redis的VM机制?_程序猿DD-CSDN博客) ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求
        4.value大小
        redis最大可以达到1GB,而memcache只有1MB
      • redis的基础数据结构 (Redis的基础数据结构与使用 - 简书)
        • string:字符串 string 是 Redis 最简单的数据结构。在存取时,都会进行序列化与反序列化操作,需要序列化整个对象。
        • list()列表:相当于 Java 语言里面的 LinkedList。⚠️:是链表不是数组。
          故插入和删除操作块,时间复杂度为O(1)
          但索引定位很慢,时间复杂度为O(n),故查询速度较慢
          当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。
          常用做:异步队列使用++
        • hash(字典):当于 Java 语言里面的 HashMap。⚠️:是无序的。
          字符串一次性需要全部序列化整个对象,hash 可以对 用户结构中的每个字段单独存储,故也可以部分获取,节省网络流量。
          但hash 结构的存储消耗要高于单个字符串。
        • Set(集合):相当于 Java 语言里面的 HashSet。⚠️:内部无序的唯一
          集合中最后一个元素移除之后,数据结构自动删除,内存被回收
        • zset(有序集合):似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重

核心原理

      • 为什么快?(Redis核心原理 - 简书)
        • 数据存储在内存,运算都是内存级别(纳秒)
        • 使用单线程,避免了多线程切换(上下文切换)的性能损耗。⚠️:但也正是因为使用了单线程,在使用redis时,要谨慎使用耗时指令,如keys
      • 单线程
        • 首先单线程最明显的优势就是:避免了多线程的切换(上下文切换)。
        • 为什么要选用单线程呢?我认为原因主要有三点:
          • 1.因为redis是完全基于内存的数据处理,速度本身极快,所以cpu不是redis的瓶颈,使用单线程,也不必考虑多线程频繁切换而造成性能损耗,但同样,因为使用单线程,要注意耗时的指令(keys)
          • 2.使用了I/O多路复用(epoll:IO多路复用原理解析(C10k问题,select,poll,epoll)_With_Her的博客-CSDN博客),大大提高了单线程的效率
          • 3.底层模型不同,redis自己构建了VM机制(什么是Redis的VM机制?_程序猿DD-CSDN博客),而一般的系统函数调用会浪费一定的时间
      • 持久化(Redis核心原理 - 简书)
        • RDB快照
          • redis将内存数据快照保存在dump.rdb的二进制文件中,快照生成与保存的触发条件可以设置。
          • 可能会丢失最近的数据,但读取和恢复比较快速
        • AOF(append-only file)
          • 实时将操作指令保存
          • 不会丢失数据,但读取和恢复会比较耗时
        • Redis4.0后,混合持久化
          • aof会定期根据内存的最新数据重写aof文件
          • 重写这一刻之前的内存rdb快照文件的内容和增量的 AOF修改内存数据的命令日志文件存在一起,都写入新的aof文件,重写完新的AOF文件后会重命名之前文件,已完成覆盖
          • 在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升
      • 淘汰策略(Redis核心原理 - 简书)
          • redis内存超过内存限制时,内存数据会和磁盘开始数据交换(swap),但这样会让redis性能大大降低,所以一般在生产环境中,我们一般不允许出现数据交换。故而redis提供了几种淘汰策略
          • noeviction
            • 不会继续写请求,读请求、DEL操作还可以继续。这样数据不会丢失,但是线上业务有影响。这也是redis默认淘汰策略
          • volatile-lru
            • 尝试淘汰设置过期时间的key,优先淘汰使用最少key。
          • volatile-ttl
            • 尝试淘汰设置过期时间的key,key剩余时间越小,优先被淘汰
          • volatile-random
            • 尝试淘汰设置过期时间的key,在过期key集合中随机淘汰
          • Allkeys-lru
            • 从全体key中,淘汰使用最少的key
          • Allkeys-random
            • 随机淘汰全体key集合(未设置过期时间的key,也可能淘汰)
      • redis事物
        • Redis事务:(今日头条)
          提供了一种将多个命令请求打包,然后一次性、按照顺序地执行多个命令的机制,并且在事务执行的期间,不会被其他客户端发送来的命令请求所打断,它会把事务中所有的命令都执行完毕才会去执行其他的命令。
        • Redis中提供了multi、exec、discard与watch、unwatch这几个命令来实现事务的功能
        • multi、exec、discard是redis提供的食物命令。
          multi:标记事务的开始
          exec:执行事务的commands队列
          discard:结束事务,并清除commands队列
        • Watch:(今日头条)
          是redis提供的一个乐观锁。命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,所以在MULTI命令后可以修改WATCH监控的键值)。类似于java的juc包中的cas。
          unwatch命令,则是取消watch。
        • 不支持回滚。(今日头条)
          不同于mysql的事物,redis事物不支持回滚。为什么不支持,据所了解到的资料:redis可能出现错误分为两种:
          在执行exec之前:错误的语法(参数数量错误,参数名错误),甚至内存不足等。此时会放弃该事物,不执行该事物内所有命令
          在执行exec之后:命令处理了错误类型,如将列表命令用在了字符串上等。此时所有命令都会执行,错误的命令执行失败,但其他正确的命令会正常执行。不会回滚
          为什么不回滚?根据错误原因可知,绝大部分错误都是由于开发过程中的实物造成,而这种错误通常不会在生产中出现。不使用回滚,则会让redis更简洁、方便、快捷。所以redis选择了不回滚。
        • redis原子性讨论(今日头条)
          严格来说,redis不具有原子性
          ACID里的原子性定义:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
          而根据上面的讨论可知,redis不支持回滚,且当exec后发生错误,也仍旧全部执行。所以,严格来讲,redis不具有原子性。
        • redis与mysql事物的区别
          redis基于commands队列实现。没有开启事物,command立即执行。开启事物,则排入队列返回排队状态。调用exce才会执行。
          mysql基于undo/redo日志实现。undo日志记录修改前状态,redo记录修改后状态。无论是否开启事物都会立刻执行。只是开启事物后,执行后的状态记录在redo日志,commit之后,数据才会写入磁盘。
      • redis管道(今日头条)
        • 管道:将多个无依赖关系的独立命令打包,一次请求,批量执行,一次返回。
        • redis一般的命令执行分开请求执行,每个命令都要单独的请求,然后阻塞等待执行结果,然后返回。这样的执行效率很低。管道所做的就是,把多个命令打包一次请求到服务端,然后批量执行命令,后一起返回,以较少网络请求的次数,提高执行效率。 
        • redis用于降低网络交互,从而减少网络所需的时间,以达到提高执行效率的目的
      • redis分区(今日头条)
        • 将数据分割,存储到多个Redis实例中。如3个key为:name_1,name_2,name_3,分别存储到redis01,redis02,redis03三个实例中。
        • 分区可以使得内存横向扩展,利用多台机器的内存构建一个更大的redis数据库。
        • 分区方式:
          • 范围分区:就像上面举的例子。可以指定ID 0到10000的用户存储到实例R0,而ID 10001到20000的用户存储到实例R2等等。但这种方式对key的命名要求有一定限制,而且需要维护一张映射范围对象范围与实例关系表。所以范围一般不受欢迎,且方法低效。
          • 哈希分区:利用哈希方法(如:CRC32(key)方法),将key通过计算转换为一组特定数字,然后取模来确定储存的实例,如果是4个实例,那么就是CRC32(key)%4,获得4的余数(为0-3),这样就确定该key应该存储在那个实例。
          • 客户端分区:客户端直接选择正确节点读写指定键
          • 代理辅助分区:客户端通过Redis协议把请求发送给代理,而不是直接发送给真正的Redis实例服务器。这个代理会确保我们的请求根据配置分区策略发送到正确的Redis实例上,并返回给客户端。Redis和Memcached的代理都是用Twemproxy (这是twitter开源的一个代理框架)来实现代理服务分区的,国内的codis(豌豆荚开源)
          • 可以把一个请求发送给一个随机的实例,这时实例会把该查询转发给正确的节点。通过客户端重定向(客户端的请求不用直接从一个实例转发到另一个实例,而是被重定向到正确的节点),Redis集群实现了一种混合查询路由。
        • 分区缺点:
          • 不支持事物
          • 涉及多个key的操作通常是不被支持的,举例来说,当两个set映射到不同的redis实例上时,你就不能对这两个set执行交集操作
          • 数据处理会更复杂,对于实例你必须处理多个RDB/AOF文件,为了备份数据,需要从多个实例和主机聚合持久文件
          • 增减/删除容量比较复杂
          • 分区最小力度是键,因此不能将一个键对应的很大的数据集映射到不同的实例
      • redis cluster(干货:一文详解Redis集群原理核心内容_Java_老男孩的技术博客_51CTO博客)
        • redis cluster采用”分而治之”的思想。
          即:将Key按照某种规则划分成多个分区(哈希槽),将不同分区的数据存放在不同的节点上。其实也是一种分区概念。和上面所说的哈希分区类似:
          首先利用哈希算法(slot = CRC16(Key) mod 16383)将key进行计算,可以得到一个哈希槽(slot)位置,根据该算法一共可有16384个槽(0~16383)。
          我们将key存储在对应的slot中(一个slot可以存储多个key),而对应的master节点,负责一定范围的槽,所有的master节点组成的集群,覆盖了0~16283整个槽的范围。
        • 据说任何计算机问题都可以通过增加一个中间层来解决。槽的概念也是这么一个中间层。
          key—>solt—>master
          key关注slot,而不再关注具体上在哪个master上。由slot负责关注master,这样master节点只需要维护自己和slot的映射关系。大大降低了集群扩容
          和收缩的难度,提升了扩展性能。
        • 集群容错能力由slave来提供。
          一个master节点必须至少有一个slave,slave负责冷备份,保存master的数据,在master宕机时替换master。redis cluster的默认由master负责读写,slave只负责备份。如果请求量较大,且对过期性要求不高,对写请求不感兴趣,可通过readyonly命令将slave配置为可读。
        • 节点通信:
          Redis集群采用P2P的Gossip协议,节点之间不断地通信交换信息,最终所有节点的状态都会达成一致。
          ping消息:每个节点频繁地向其他节点发起ping消息,用于检测节点是否在线和交换节点状态信息。
          ~每个节点每秒10次ping,每次选择5个最久没有通信的节点。
          ~如发现与某节点cluster_node_timeout / 2的时间未联系,立即ping
          ~每次ping会带上自己节点的信息,还会携带1/10其他节点的信息,进行数据交换,至少包含3个其他节点的信息,最多包含总节点-2个其他节点信息
          pong消息:收到ping、meet消息时的响应消息。
          meet消息:新节点加入消息。
          fail消息:节点下线消息。
          forget消息:忘记节点消息,使一个节点下线。这个命令必须在60秒内在所有节点执行,否则超过60秒后该节点重新参与消息交换。实践中不建议直接使用forget命令来操作节点下线。
        • 节点下线:
          当某个节点出现问题时,需要一定的传播时间让多数master节点认为该节点确实不可用,才能标记标记该节点真正下线。Redis集群的节点下线包括两个环节:主观下线(pfail)和客观下线(fail)。
          主观下线:当节点A在cluster-node-timeout时间内和节点B通信(ping-pong消息)一直失败,则节点A认为节点B不可用,标记为主观下线,并将状态消息传播给其他节点。
          客观下线:当一个节点被集群内多数master节点标记为主观下线后,则触发客观下线流程,标记该节点真正下线。
        • 故障恢复:
          当一个master节点客观下线后,集群会从slave节点中选出一个提升为master节点来替换它。
          Redis集群使用选举-投票的算法来挑选slave节点。一个slave节点必须获得包括故障的master节点在内的多数master节点的投票后才能被提升为master节点。
          假设集群规模为3主3从,则必须至少有2个主节点存活才能执行故障恢复。如果部署时将2个主节点部署到同一台服务器上,则该服务器不幸宕机后集群无法执行故障恢复。
          默认情况下,Redis集群如果有master节点不可用,即有一些槽没有负责的节点,则整个集群不可用。可以在配置中将cluster-require-full-coverage配置为no,那么master节点故障时只会影响访问它负责的相关槽的数据,不影响对其他节点的访问。
      • 一致性哈希(https://www.cnblogs.com/huangfuyuan/p/9880379.html#commentform)
        • redis集群分区中,如果直接使用哈希算法:hash(key) % length。那么当服务器数量(length)变化,会导致所有缓存的数据需要重新进行hash运算,这样就导致原来的哪些数据访问不到了。而这段时间如果访问量上升了,容易引起服务器雪崩。因此,引入了一致性哈希(redis cluster未采用一致性哈希,而是哈希槽的概念)
        • 一致性哈希:通过对2^32取模的方式,保证了在增加/删除缓存服务器的情况下,其他缓存服务器的缓存仍然可用,从而不引起雪崩问题。
        • 2^32可以想象为:形成一个圆环,我们称之为:hash环。
        • 服务器:通过算法:hash(服务器的IP地址)% 2^32得到服务器映射到hash环上的位置。hash(缓存数据key)% 2^32,得到被缓存对象在hash环上的位置。
        • 缓存到哪个服务器:从被缓存对象的位置出发,沿顺时针方向遇到的第一个服务器,就是当前对象将要被缓存于的服务器
        • 所以在服务器不变的情况下,一个对象必定缓存在一个固定的服务器上,那么,当再次访问这对象时,只要再次使用相同的算法计算即可算出这对象被缓存到哪个服务器
        • hash环的偏斜:如果几个服务器计算后的位置相邻很近,那么被缓存的对象很有可能大部分集中缓存在某一台服务器上。这种现象称为Hash环的偏斜。
        • 如何解决Hash环的偏斜:将现有的物理节点通过虚拟的方法复制出来,而被复制出来的节点被称为虚拟节点。然后将其均匀的分布在哈希环上。
        • 某节点宕机:其他节点因为位置已固定,依然可以提供服务。而宕机的节点会在下次容灾分配时,会将宕机节点的数据分配到就近服务区上。

        • 扩容:当新增节点c,且c节点位置位于a,b节点之间,那么只会影响b节点。a到c节点之间的数据会转存到c节点,c到b节点的数据依然存储在b节点上。所以一致性哈希有较好的容错性与可扩展性。
      • Redis 哨兵模式的选举过程(redis集群选举机制_祈雨v的博客-CSDN博客_redis选举机制)
        • 判断节点宕机(主观宕机、客观宕机)
          如果一个节点a认为主节点b宕机,那么为pfail,主观宕机
          如果一半以上节点认为主节点b宕机,那么为fail,客观宕机
          注意⚠️:如果为从节点客观宕机,则到此为止。如为主节点客观宕机,则开始选举,进行故障转移
        • 选举Leader
          哨兵模式需要先选举出一个Leader节点,由该节点选择哪个b节点的子节点作为接下来的主节点
          每一个Sentinel节点都可以成为Leader,当一个Sentinel节点确认redis集群的主节点主观下线后,会请求其他Sentinel节点要求将自己选举为Leader。被请求的Sentinel节点如果没有同意过其他Sentinel节点的选举请求,则同意该请求(选举票数+1),否则不同意。
          如果一个Sentinel节点获得的选举票数达到Leader最低票数(quorum和Sentinel节点数/2+1的最大值),则该Sentinel节点选举为Leader;否则重新进行选举。
        • Leader决定新master
          选择的的过程如下:
          1.过滤故障的从节点
          2.选择优先级slave-priority最大的从节点作为主节点,如不存在则继续
          3.选择复制偏移量(数据写入量的字节。即:记录写了多少数据。主服务器会把偏移量同步给从服务器,当主从的偏移量一致,则数据是完全同步)最大的从节点作为主节点,如不存在则继续
          4.选择runid(redis每次启动的时候生成随机的runid作为redis的标识)最小的从节点作为主节点
          Redis概述与进阶技术点_第1张图片
      • Redis cluster的选举过程(Redis cluster集群模式的原理 - __Meng - 博客园)
        • 判断节点宕机(主观宕机、客观宕机)
          如果一个节点a认为主节点b宕机,那么为pfail,主观宕机
          如果一半以上节点认为主节点b宕机,那么为fail,客观宕机
        • 从节点过滤
          也就是过滤故障的从节点
          检查每个slave node与master node断开连接的时间,如果超过了cluster-node-timeout * cluster-slave-validity-factor,那么就没有资格切换成master
        • 从节点选举
          这里的选举与哨兵模式的Leader决定新master一样的规则
          slave priority(优先级)—>offset(复制偏移量)—>runId
          根据这几个规则对宕机的master的slave进行排序
          每个从节点,都根据自己对master复制数据的offset,来设置一个选举时间,offset越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举
          所有的master node开始slave选举投票,给要进行选举的slave进行投票,如果大部分master node(N/2 + 1)都投票给了某个从节点,那么选举通过,那个从节点可以切换成master
          从节点执行主备切换,从节点切换为主节点
      • 评redis clster与redis哨兵模式的选举过程
        • 非常相似,但也有不同
        • 不同点:对比哨兵模式,Redis cluster没有选举leader。而是直接进行了过滤,选举slave为新master的操作
        • 相同点:但选举的原理基本一致,都是根据优先级,复制量,runid来对节点进行选举
      • redis分布式锁
        • 使用setnx、getset、expire、del这4个命令实现
          setnx(SET if Not eXists):只在键 key 不存在的情况下,将键 key 的值设置为 value 。若键 key 已经存在, 则 SETNX 命令不做任何动作。返回值:命令在设置成功时返回 1 ,设置失败时返回 0 。
          getset:GETSET key value,将键 key 的值设为 value ,并返回键 key 在被设置之前的旧的value。返回值:如果键 key 没有旧值, 也即是说, 键 key 在被设置之前并不存在, 那么命令返回 null 。当键 key 存在但不是字符串类型时,命令返回一个错误。
          expire:为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除。返回值:设置成功返回 1,不成功返回0。
          del:删除给定的一个或多个 key ,不存在的 key 会被忽略。返回值:被删除 key 的数量
        • 分布式锁原理(一)(redis分布式锁原理与实现_dazou的博客-CSDN博客_redis分布式锁原理):
          • A尝试去获取锁lockkey,通过setnx(lockkey,currenttime+timeout)命令,对lockkey进行setnx,将value值设置为当前时间+锁超时时间;
          • 如果返回值为1,说明redis服务器中还没有lockkey,也就是没有其他用户拥有这个锁,A就能获取锁成功;
          • 在进行相关业务执行之前,先执行expire(lockkey),对lockkey设置有效期,防止死锁。因为如果不设置有效期的话,lockkey将一直存在于redis中,其他用户尝试获取锁时,执行到setnx(lockkey,currenttime+timeout)时,将不能成功获取到该锁;
          • 执行相关业务;
          • 释放锁,A完成相关业务之后,要释放拥有的锁,也就是删除redis中该锁的内容,del(lockkey),接下来的用户才能进行重新设置锁新值。
          • 如果A在setnx成功后,A成功获取锁了,也就是锁已经存到Redis里面了,此时服务器异常关闭或是重启,将不会执行closeOrder,也就不会设置锁的有效期,这样的话锁就不会释放了,就会产生死锁。
            在这里插入图片描述
        • 分布式锁原理(二)redis分布式锁原理与实现_dazou的博客-CSDN博客_redis分布式锁原理:
          • 为了解决原理1中会出现的死锁问题,提出原理2双重防死锁,可以更好解决死锁问题
          • 当A通过setnx(lockkey,currenttime+timeout)命令能成功设置lockkey时,即返回值为1,过程与原理1一致;
          • 当A通过setnx(lockkey,currenttime+timeout)命令不能成功设置lockkey时,这是不能直接断定获取锁失败;因为我们在设置锁时,设置了锁的超时时间timeout,当当前时间大于redis中存储键值为lockkey的value值时,可以认为上一任的拥有者对锁的使用权已经失效了,A就可以强行拥有该锁;具体判定过程如下;
          • A通过get(lockkey),获取redis中的存储键值为lockkey的value值,即获取锁的相对时间lockvalueA
          • lockvalueA!=null && currenttime>lockvalue,A通过当前的时间与锁设置的时间做比较,如果当前时间已经大于锁设置的时间临界,即可以进一步判断是否可以获取锁,否则说明该锁还在被占用,A就还不能获取该锁,结束,获取锁失败;
          • 步骤4返回结果为true后,通过getSet设置新的超时时间,并返回旧值lockvalueB,以作判断,因为在分布式环境,在进入这里时可能另外的进程获取到锁并对值进行了修改,只有旧值与返回的值一致才能说明中间未被其他进程获取到这个锁
          • lockvalueB == null || lockvalueA==lockvalueB,判断:若果lockvalueB为null,说明该锁已经被释放了,此时该进程可以获取锁;旧值与返回的lockvalueB一致说明中间未被其他进程获取该锁,可以获取锁;否则不能获取锁,结束,获取锁失败。
        • 注意⚠️:如果多服务器部署时,要统一各个服务器的时间。
          在这里插入图片描述
           

你可能感兴趣的:(JAVA,redis,reids,cluster)