读懂Redis这一篇真够了!

4,448 阅读34分钟

相信我,读下去,没有收获你拿刀来砍我!

在这里插入图片描述

前言

首先科普一下CPU缓存,CPU缓存是指可以进行高速数据交换的存储器,它先于内存与CPU交换数据,因此速率很快。缓存的工作原理是当CPU要读取一个数据的时候,首先在CPU缓存中查找,找到就立即读取并送给CPU处理;没有找到,就从速率相对较慢的内存中读取并送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存

在这里插入图片描述

为什么要引入CPU缓存?在解释之前必须先了解程序的执行过程,首先从硬盘执行程序,存放到内存,再给cpu运算与执行。由于内存和硬盘的速度相比cpu实在慢太多了,每执行一个程序cpu都要等待内存和硬盘,引入缓存技术便是为了解决此矛盾,缓存与cpu速度一致,cpu从缓存读取数据比cpu在内存上读取快得多,从而提升系统性能。目前主流级CPU都有一级和二级缓存,高端些的甚至有三级缓存

在这里插入图片描述

上面讲述的是CPU缓存,让你读起下面的文章来好有个药引子,如果读不懂没太大问题,继续向下读

重点来了!在开发中我们常常提到的缓存和上面的CPU缓存有异曲同工之妙,但是并不等同于CPU缓存,我们写服务器程序时,使用缓存的目的无非就是减少数据库访问次数降低数据库的压力和提升程序的响应时间, 然而根据具体的使用场景又可以派生出无数种情况:

  • 比如说程序频繁读取数据库, 但是查询获得的结果却总是相同的,这部分相同的结果是不是可以放入缓存 ?
  • 获得查询结果要进行复杂的运算,非常消耗时间, 运算结果是不是可以放入缓存 ?
  • 有一些在网站每个页面都需要使用的数据, 比如说用户数据, 是不是可以放入缓存 ?

在这里插入图片描述

缓存有很多实现方式,谷歌的guava包的Cache、分布式缓存redis,memcached、EHcache、自定义缓存(例如使用静态Map实现)等。下面我们来讲解最常用的redis,会有一些简单的操作,还没有下载,不会?别慌,点击 download.redis.io/releases/下载,下载完成之后的步骤请移步我的redis教程系列;

Redis数据类型和内存原理

      redis是一个基于C语言实现的高性能的key-value存储系统,运行在内存中但是可以持久化到硬盘上,有着多样的数据结构string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合),还有一些高级数据结构HyperLogLog、Geo、Pub/Sub。每种类型的存储在底层都会存在不同的编码格式(redisObject、SDS等)。

Redis到底强在哪里?

  • 性能极高,Redis能读的速度是110000次/s,写的速度是81000次/s 。 -支持多种数据结构,如 string(字符串)、 list(双向链表)、dict(hash表)、set(集合)、zset(排序set)、hyperloglog(基数估算)
  • 原子,Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
  • 支持AOF和RDB持久化操作,数据备份;
  • 丰富的特性,Redis还支持pub/sub, 通知, key 过期等等特性。

数据类型简介

String(字符串)

Redis字符串是字节序列。Redis字符串是二进制安全的,这意味着他们有一个已知的长度没有任何特殊字符终止,所以你可以存储任何东西,512M为上限; 示例:

redis 127.0.0.1:6379> SET name kevin
OK
redis 127.0.0.1:6379> GET name
"kevin"

  • set key name存储一个字符串对象value

Hash(hash表)

Redis的哈希是键值对的集合。 Redis的哈希值是字符串字段和字符串值之间的映射,因此它们被用来表示对象。

在这里插入图片描述

示例:

redis 127.0.0.1:6379> HSET key field value
OK
redis 127.0.0.1:6379> HGET key field
value

  • hset 存储一个哈希键值对的集合(hset key field value)
  • hget 获取一个哈希键的值(hget key field)

xiaoming是对于redis的存储识别Hash的,而Hash真正存储的是key(year、score),value(18、99)。

List(链表)

Redis的链表是简单的字符串列表,排序插入顺序。可以添加元素到Redis的列表的头部或尾部,允许添加重复元素;

在这里插入图片描述

示例:

在这里插入图片描述

  • lpop key 从左边移出一个元素,rpop key 从右边移出一个元素
  • len key 返回链表中元素的个数 相当于关系型数据库中 select count(*)
  • lrange key start end lrange命令将返回索引从start到stop之间的所有元素。Redis的列表起始索引为0。

Set(集合)

Redis的集合是字符串的无序集合,不允许有重复。

在这里插入图片描述

示例:

在这里插入图片描述

  • sadd key value 添加一个string元素到,key对应的set集合中,成功返回1,如果元素已经在集合中返回0;
  • smembers key 返回key对应set的所有元素,结果是无序的;

SortedSet(有序集合)zset

Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。有序集合的成员是唯一的,但分数(score)却可以重复。

示例:

在这里插入图片描述

  • zadd key score value 将一个或多个value及其socre加入到set中
  • zrange key start end 0和-1表示从索引为0的元素到最后一个元素(同LRANGE命令相似)
  • zrangebycore key start end 范围内按照分数排序输出
  • zremrangebyscore key start end 可用于范围删除操作

Redis内存模型

接下来我会从redis底层内存存储到系统使用级别来简单介绍redis,什么,一听内存就头大?那你还想不想要走向人生巅峰迎娶白富美,想就乖乖的看、乖乖读、乖乖学!

在这里插入图片描述

Redis通过info memory查询内存使用情况,作为内存数据库,在内存中存储的主要是数据,redis内存主要划分为几部分:

  • 数据:作为数据库,数据是最主要的部分;
  • 进程本身运行需要的内存:Redis主进程本身运行肯定需要占用内存,如代码、常量池等等;
  • 缓冲内存:缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF缓冲区等;
  • 内存碎片:内存碎片是Redis在分配、回收物理内存过程中产生的。

Redis数据内存

在这里插入图片描述

上面图是执行set hello world时所设计到的数据模型;

Redis是键值对数据库,每个键值对都会有一个dictEntry,里面存储着指向Key和Value的指针;next指向下一个dictEntry,与本Key-Value无关。 Key和Value又都有相应的存储结构,每种类型都有至少两种内部编码,这样做的好处在于一方面接口与实现分离,当需要增加或改变内部编码时,用户使用不受影响,另一方面可以根据不同的应用场景切换内部编码,提高效率。

但是,无论是哪种类型,redis都不会直接存储,而是通过redisObject的对象进行存储,redisObject对象很重要,Redis对象的类型、内部编码、内存回收、共享对象等功能,都需要redisObject支持。

Redis中还有一种SDS结构也比较重要,SDS是简单动态字符串(Simple Dynamic String)的缩写。Redis没有直接使用C字符串(即以空字符’\0’结尾的字符数组)作为默认的字符串表示,而是使用了SDS。至于它们的结构在这里暂且不说,我会在下面的文章中详细介绍。

事务和管道pipline

众所周知,事务是指一个完整的动作,要么全部执行,要么什么也没有做,提起事务我们会首先想到事务的四大特性ACID:

      A:原子性(Atomicity) 事务是数据库的逻辑工作单位,事务中包括的诸操作要么全做,要么全不做。

      C:一致性(Consistency) 事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。

      I:隔离性(Isolation) 一个事务的执行不能被其他事务干扰。

      D:持续性/永久性(Durability) 一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。

Redis事务

简单聊下redis中的事务,先介绍几个redis指令,即MULTI、EXEC、DISCARD、WATCH、UNWATCH。这五个指令构成了redis事务处理的基础。

1、multi用来组装提供事务;
2、exec执行所有事务块内的命令。
3、discard取消事务,放弃执行事务块内的所有命令。
4、watch监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
5、unwatch取消 watch 命令对所有 key 的监视。

Redis事务可以一次执行多个命令,会经历三个阶段:开始事务,命令入队,执行事务。并且带有以下三个重要的保证:

    1、批量操作在发送 EXEC 命令前被放入队列缓存。

    2、收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。

    3、在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。

在这里插入图片描述

在上面的例子中,我们看到了QUEUED的字样,这表示我们在用MULTI组装事务时,每一个命令都会进入到内存队列中缓存起来,如果出现QUEUED则表示我们这个命令成功插入了缓存队列,在将来执行EXEC时,这些被QUEUED的命令都会被组装成一个事务来执行。

对于事务的执行来说,如果redis开启了AOF持久化的话,那么一旦事务被执行,事务中的命令便会通过write命令一次性写入到磁盘中(下面会介绍持久化)。

在事务执行中,经常会遇到两类问题,一是调用EXEC之前的问题,另一个是调用EXEC之后的问题。

调用EXEC之前的问题

“调用EXEC之前的错误”,有可能是由于语法有误导致的,也可能时由于内存不足导致的。只要出现某个命令无法成功写入缓冲队列的情况,redis都会进行记录,在客户端调用EXEC时,redis会拒绝执行这一事务。(这时2.6.5版本之后的策略。在2.6.5之前的版本中,redis会忽略那些入队失败的命令,只执行那些入队成功的命令)。

在这里插入图片描述

redis无情的拒绝了事务的执行,原因是“之前出现了错误”;(error) EXECABORT Transaction discarded because of previous errors。

调用EXEC之后的问题

对于“调用EXEC之后的错误”,redis则采取了完全不同的策略,即redis不会理睬这些错误,而是继续向下执行事务中的其他命令。这是因为,对于应用层面的错误,并不是redis自身需要考虑和处理的问题,所以一个事务中如果某一条命令执行失败,并不会影响接下来的其他命令的执行。

在这里插入图片描述

看到这里可能很多人会提出问题:你前面不是说事务有一个特性是原子性,事务中的操作要么全部执行,要不全部不执行。那上面的情况岂不是违背了原子性?是不是意味着redis不支持事务原子性?

解惑:redis事务不支持事务回滚机制,redis事务执行过程中,如果一个命令出现错误,那么就返回错误,下面的命令还是会继续执行下去。正是因为redis事务不支持事务回滚,如果事务出现了命令执行错误,只会返回当前命令的错误给客户端,不会影响下面的命令的执行,所以很多人觉得和关系型数据库(MySQL) 不一样,而 MySQL 的事务是具有原子性的,所以大家都认为 Redis 事务不支持原子性。

其实,正常情况下,redis事务是支持原子性的,它也是要不所有命令执行成功,要不一个命令都不执行。看我们上面介绍的调用EXEC之前的错误的实例,在事务开始后,用户可以输入事务要执行的命令;在命令入事务队列前,会对命令进行检查,如果命令不存在或者是命令参数不对,则会返回错误可客户端,并且修改客户端状态。当后面客户端执行 EXEC 命令时,服务器就会直接拒绝执行此事务了。

Redis不支持事务回滚,但是它会检查事务中的每一个命令是否错误(不支持检查程序员个人的逻辑错误),如果有错误便不会执行事务,只有通过redis这一层的检查才会开启事务执行并且会全部执行(并不会保证全部执行成功),所以客观来讲redis事务是支持原子性的。

思考redis和mysql、oracle这种关系型数据库事务的区别,首先redis定位是nosql非关系数据库,而mysql、oracle这种是关系型数据库。

在关系型数据库中执行的sql查询可以是相当复杂的,sql真正开始执行的时候才会进行检查分析(有些情况可能会预编译),没有事务队列这一概念,mysql数据库不知道下一条sql是否正确,所以有必要支持事务回滚。但是在redis中,redis使用了事务队列来将命令存储起来并且会进行格式检查,提前可以知道命令是否正确,所以如果只要有一个命令是错误的,那么这个事务是不能执行的。

Redis 作者认为基本只会出现在开发环境的编程错误其实在生产环境基本是不可能出现的(例如对 String 类型的数据库键执行 LPUSH 操作),所以他觉得没必要为了这事务回滚机制而改变 Redis 追求简单高效的设计主旨。

所以最后,其实 Redis 事务真正支持原子性的前提:开发者不要傻不拉几的写有逻辑问题的代码!

Redis管道技术

Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。这意味着通常情况下一个请求会遵循以下步骤:客户端向服务端发送一个查询请求,并监听Socket返回,通常是以阻塞模式,等待服务端响应。服务端处理命令,并将结果返回给客户端。

Redis 管道技术可以在服务端未响应时,客户端可以继续向服务端发送请求,并最终一次性读取所有服务端的响应。形象点说明就是对于redis来说一般是同步模式来请求返回结果,而管道技术可以让redis可以实现异步的访问,客户端不需要等待服务端的返回结果,可以持续的向服务端发送请求,等待最终把结果全部读取。

持久化机制(RDB和AOF)

Redis持久化

数据持久化技术,也是Redis的一大特色。主要作用是数据的备份,将内存中的数据持久化到硬盘上,保证数不会因为服务的退出而造成丢失。Redis是内存数据库,我们需要定期的将redis中的数据以某种形式(数据或者命令)存储在硬盘上,当下次redis重启时,利用持久化的技术可以实现数据的恢复。有时为了进行灾难备份,我们也可以将持久化生成的数据文件拷贝到一个远程位置。

和咱们在朋友圈看见好看的图片一样,得把它保存到手机,这样下次才能找到它继续用;而这里的内存就相当于咱们的脑子,脑子经过多天的“打磨”忘却了,而存起来更便于下次找到它!

在这里插入图片描述

Redis持久化分为RDB持久化和AOF持久化:前者将当前数据保存到硬盘,后者则是将每次执行的写命令保存到硬盘(类似于MySQL的binlog);由于AOF持久化的实时性更好,即当进程意外退出时丢失的数据更少,因此AOF是目前主流的持久化方式,不过RDB持久化仍然有其用武之地。

RDB持久化

RDB持久化是将当前进程中的数据生成快照保存到硬盘(因此也称作快照持久化),保存的文件是经过压缩的二进制文件,后缀是rdb;当Redis重新启动时,可以读取快照文件恢复数据。RDB持久化的触发分为手动触发和自动触发两种。

优点:

1、体积小:相同的数据量rdb数据比aof的小,因为rdb是紧凑型文件;

2、恢复快:因为rdb是数据的快照,数据复制,不需要重新执行命令;

3、性能高:父进程在保存rdb时候只需要fork一个子进程,无需父进程的进行其他io操作,也保证了服务器的性能。

缺点:

1、故障丢失:因为rdb是全量的,我们一般是使用shell脚本实现30分钟或者1小时或者每天对redis进行rdb备份,但是最少也要5分钟进行一次的备份,所以当服务死掉后,最少也要丢失5分钟的数据。

2、耐久性差:相对aof的异步策略来说,因为rdb的复制是全量的,即使是fork的子进程来进行备份,当数据量很大的时候对磁盘的消耗也是不可忽视的,尤其在访问量高的时候,fork的时间会延长,导致cpu吃紧,耐久性相对较差。

AOF持久化

AOF持久化(即Append Only File持久化),是将Redis执行的每次写命令记录到单独的日志文件中(有点像MySQL的binlog);当Redis重启时再次执行AOF文件中的命令来恢复数据。

它的出现是为了弥补RDB的不足(数据的不一致性),所以它采用日志的形式来记录每个写操作,并追加到文件中。我们可以设置不同的 fsync 策略,比如无 fsync ,每秒钟一次 fsync ,或者每次执行写入命令时 fsync 。

AOF 的默认策略为每秒钟 fsync 一次,在这种配置下,Redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据( fsync 会在后台线程执行,所以主线程可以继续努力地处理命令请求)。

优点:

1、数据保证:我们可以设置不同的fsync策略,一般默认是everysec,也可以设置每次写入追加,服务宕机最多丢失一秒数据

2、文件重写:当aof文件大小到达一定程度的时候,后台会自动的去执行aof重写,此过程不会影响主进程,重写完成后,新的写入将会写到新的aof中,旧的就会被删除掉。

缺点:

1、性能相对较差:恢复数据需要重新执行命令,性能较RDB低;

2.体积相对更大:尽管是将aof文件重写了,依然大;

3.恢复速度更慢;

主从复制(读写分离)

上面介绍的持久化侧重解决的是redis数据的单机备份问题(从内存到硬盘),而主从复制则侧重解决的是数据的多机热备份。先来说下热备份,就是保证服务正常不间断运行,通过一台服务器对另一台服务器进行实时数据复制,且保障两边数据的一致性。我们将在线的备份称为热备份,而相对的,将脱机数据备份称为冷备份。冷备份是在系统不运作时,定时的将数据备份至备份服务器或存储。

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

主从复制的作用:

  • 数据冗余:主从复制实现了数据的热备份,可以提供远程备份、数据冗余,也可以作为服务的冗余。

  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。

在这里插入图片描述

主从复制的原理:

主从复制大概分为三个阶段:连接建立、数据同步、命令传播;

(1)连接建立阶段主要作用是在主从节点之间建立连接,为数据同步做好准备;

(2)建立连接好之后便可以进行数据同步,也是从阶段数据的初始化,数据同步阶段是主从复制最核心的阶段,根据主从节点当前状态的不同,可以分为全量复制和部分复制。全量复制顾名思义就是把主节点数据全部复制到从节点中进行备份数据,全量复制在主节点数据量较大时效率太低。于是在Redis2.8引入了部分复制,用于处理网络中断时的数据复制,主从节点会自动判断当前状态适合全量复制还是部分复制;

(3)数据同步阶段完成后,主从节点进入命令传播阶段;在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING和REPLCONF ACK,心跳机制对于主从复制的超时判断、数据安全等有作用。

详细原理请移步至Redis教程主从复制篇!

哨兵机制

提起哨兵,大家想到的是什么呢?

在这里插入图片描述

我们上面说到持久化是为了解决单机redis的数据存储问题,主从复制可以实现数据冗余,侧重于解决数据的多机热备份。但是主从复制中存在着一个问题就是故障恢复无法自动化,而redis中的哨兵机制,基于Redis主从复制,主要作用便是解决主节点故障恢复的自动化问题,进一步提高系统的高可用性。但是哨兵机制也存在一定的缺陷,就是写操作无法负载均衡,存储能力受到单机限制。

Redis Sentinel,即Redis哨兵,在Redis 2.8版本开始引入。哨兵的核心功能是主节点的自动故障转移。下面是Redis官方文档对于哨兵功能的描述:

监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。 自动故障转移(Automatic failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。 配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。 通知(Notification):哨兵可以将故障转移的结果发送给客户端。

Redis哨兵机制的架构

在这里插入图片描述

它由两部分组成,哨兵节点和数据节点:

1、哨兵节点:哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的redis节点,不存储数据。 2、数据节点:主节点和从节点都是数据节点。哨兵系统中的主从节点和普通的主从节点没有什么区别,故障发现和转移是由哨兵来控制和完成的,哨兵的本质也是redis节点,只不过不会存储数据。每一个哨兵节点只需要配置监控主节点(可以配置监控多个主节点),便可以自动发现其他的哨兵节点和从节点。

哨兵机制原理

  1. 定时任务:每个哨兵节点维护了3个定时任务。

通过向主从节点发送info命令获取最新的主从结构

通过发布订阅功能获取其他哨兵节点的信息;

通过向其他节点发送ping命令进行心跳检测,判断是否下线。

  1. 主观下线:在心跳检测的定时任务中,如果其他节点超过一定时间没有回复,哨兵节点就会将其进行主观下线。顾名思义,主观下线的意思是一个哨兵节点“主观地”判断下线;

  2. 客观下线:哨兵节点在对主节点进行主观下线后,会通过命令询问其他哨兵节点该主节点的状态;如果判断主节点下线的哨兵数量达到一定数值,则对该主节点进行客观下线。

需要特别注意的是,客观下线是主节点才有的概念;如果从节点和哨兵节点发生故障,被哨兵主观下线后,不会再有后续的客观下线和故障转移操作。

  1. 选举领导者哨兵节点:当主节点被判断客观下线以后,各个哨兵节点会进行协商,选举出一个领导者哨兵节点,并由该领导者节点对其进行故障转移操作。

监视该主节点的所有哨兵都有可能被选为领导者,选举使用的算法是Raft算法;Raft算法的基本思路是先到先得:即在一轮选举中,哨兵A向B发送成为领导者的申请,如果B没有同意过其他哨兵,则会同意A成为领导者。选举的具体过程这里不做详细描述,一般来说,哨兵选择的过程很快,谁先完成客观下线,一般就能成为领导者。

  1. 故障转移:选举出的领导者哨兵,开始进行故障转移操作,该操作大体可以分为3个步骤:

(1)在从节点中选择新的主节点:选择的原则是,首先过滤掉不健康的从节点;然后选择优先级最高的从节点(由slave-priority指定);如果优先级无法区分,则选择复制偏移量最大的从节点;如果仍无法区分,则选择runid最小的从节点。

(2)更新主从状态:通过slaveof no one命令,让选出来的从节点成为主节点;并通过slaveof命令让其他节点成为其从节点。

(3)将已经下线的主节点设置为新的主节点的从节点,当原主节点重新上线后,它会成为新的主节点的从节点。

集群机制

前面的哨兵机制存在缺陷,写操作无法负载均衡,存储能力受到单机限制。而集群就是为了解决这些问题而诞生的,它是redis3.0开始引入的分布式存储方案,集群由多个节点(Node)组成,Redis的数据分布在这些节点中。集群中的节点分为主节点和从节点:只有主节点负责读写请求和集群信息的维护;从节点只进行主节点数据和状态信息的复制。注意区分和哨兵机制中的主从节点,哨兵中的只有主节点负责写请求,而从节点负责读请求。

集群的主要作用可以归纳为两点:数据分区和高可用;这两点也正是解决上述的两个问题,数据分区是为了解决存储能力受到单机限制,而高可用则是为了解决写操作无法负载均衡以及实现故障恢复自动化。

** 数据分区存储**

数据分区有顺序分区,哈希分区等,而哈希分区具有天然的随机性,集群使用的分区方案便是哈希分区的一种。

衡量数据分区方法好坏的标准有很多,其中比较重要的两个因素是

  • 数据分布是否均匀
  • 增加或删减节点对数据分布的影响。

由于哈希的随机性,哈希分区基本可以保证数据分布均匀,因此在比较哈希分区方案时,重点要看增减节点对数据分布的影响。

哈希分区又可分为哈希取余分区、一致性哈希分区、带虚拟节点的一致性哈希分区,接下来简单介绍下。

** 哈希取余分区**

哈希取余分区思路非常简单:计算key的hash值,然后对节点数量进行取余,从而决定数据映射到哪个节点上。该方案最大的问题是,当新增或删减节点时,节点数量发生变化,系统中所有的数据都需要重新计算映射关系,引发大规模数据迁移。

** 一致性哈希**

一致性哈希:将整个哈希值空间组织成一个虚拟的圆环,范围为0-2^32-1;对于每个数据,根据key计算hash值,确定数据在环上的位置,然后从此位置沿环顺时针行走,找到的第一台服务器就是其应该映射到的服务器。

在这里插入图片描述

与哈希取余分区相比,一致性哈希分区将增减节点的影响限制在相邻节点。以上图为例,如果在node1和node2之间增加node5,则只有node2中的一部分数据会迁移到node5;如果去掉node2,则原node2中的数据只会迁移到node4中,只有node4会受影响。

一致性哈希分区的主要问题在于,当节点数量较少时,增加或删减节点,对单个节点的影响可能很大,造成数据的严重不平衡。还是以上图为例,如果去掉node2,node4中的数据由总数据的1/4左右变为1/2左右,与其他节点相比负载过高。

关于哈希一致性的原始论文连接:

原始论文《Consistent Hashing and Random Trees》链接如下:

官方链接 - PDF 版本

相关论文《Web Caching with Consistent Hashing》链接如下:

官方链接 - PDF 版本

带虚拟节点的一致性哈希

集群采用的是带虚拟节点的一致性哈希分区,在redis中这里的虚拟节点被称为槽(slot),redis被设计为16384个槽。槽是介于数据和实际节点之间的虚拟概念;每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据。引入槽以后,数据的映射关系由数据hash->实际节点,变成了数据hash->槽->实际节点。

在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽解耦了数据和实际节点之间的关系,增加或删除节点对系统的影响很小。

在这里插入图片描述

举个简单例子说明:我们存取的key会根据crc16的算法得出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。

节点通信

在哨兵系统中,节点分为数据节点和哨兵节点:前者存储数据,后者实现额外的控制功能。在集群中,没有数据节点与非数据节点之分:所有的节点都存储数据,也都参与集群状态的维护。为此,集群中的每个节点,都提供了普通端口和集群端口两个端口。集群端口端口是普通端口+10000(10000是固定值,无法改变),集群端口只用于节点之间的通信,如搭建集群、增减节点、故障转移等操作时节点间的通信;不要使用客户端连接集群接口。节点间通信发送的消息主要分为5种:meet消息、ping消息、pong消息、fail消息、publish消息。不同的消息类型,通信协议、发送的频率和时机、接收节点的选择等是不同的。消息具体含义不在这里介绍

集群中的节点需要专门的数据结构来存储集群的状态。节点为了存储集群状态而提供的数据结构中,最关键的是clusterNode和clusterState结构:前者记录了一个节点的状态,后者记录了集群作为一个整体的状态。

容错选举

在发送消息的过程中,Redis之间通过互相的ping-pong判断是否节点可以连接上。如果有一半以上的节点去ping一个节点的时候没有回应,集群就认为这个节点宕机了,然后去连接它的从节点。如果某个节点和所有从节点全部挂掉,我们集群就进入fail状态。还有就是如果有一半以上的主节点宕机,那么我们集群同样进入fail了状态。这就是我们的redis的投票机制。

投票过程是集群中所有master参与,如果半数以上master节点与master节点通信超时(cluster-node-timeout),认为当前master节点挂掉。

Redis到底用在哪里

  • 缓存:缓存现在几乎是所有中大型网站都在用的必杀技,合理的利用缓存不仅能够提升网站访问速度,还能大大降低数据库的压力。使用缓存提升性能的同时,也会带来更多的问题,举几个例子,如果缓存如果突然失效造成大量请求打到DB上造成缓存雪崩、缓存击穿如何处理,那就设计到缓存的过期时间(定期删除和惰性删除)和6种内存淘汰机制。如果大量不存在于缓存中的请求过来打到DB上造成缓存穿透如何处理,就涉及到如何进行请求过滤,以及使用高效的布隆过滤器(不知道?移步我的Redis教程系列,面试加分利器!!)。如何保证缓存和数据库的一致性问题,是要保证强一致性还是最终一致性。

在这里插入图片描述

  • 分布式问题:分布式事务、分布式会话、分布式锁等;简单介绍下

分布式事务

就是指的会涉及多个数据库的事务,关键在于需要保证多个节点之间的数据写操作,要么全部都执行,要么全部都不执行,但是一台机器在执行本地事务的时候无法知道其他机器中的本地事务的执行结果,所以也就不知道本次事务到底应该 commit 还是 roolback。常规的解决办法就是引入一个协调者来统一调度所有分布式节点的执行,而redis可以充当这一角色

分布式会话

指的是集群环境下,用户访问系统随机分配到不同机器之间的用户状态跟踪问题。用户访问登录网站,第一次负载均衡到服务器A上,第二次却负载均衡到服务器B上,如何让用户的状态数据不会丢失

分布式锁

如果我们一台机器上多个不同线程抢占同一个资源,并且如果多次执行会有异常,我们称之为非线程安全。如果是同一台机器里面不同的java实例,我们可以使用系统的文件读写锁来解决,如果再扩展到不同的机器呢?我们通常用分布式锁来解决。

  • 消息队列:消息队列是大型网站必用中间件,如有Broker的暴力路由Kafka,有Broker的复杂路由RabbitMQ、RocketMQ以及无Broker的通信流派ZeroMQ等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,阻塞队列利用的是超时机制,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。

  • 自动过期:其实上面介绍的阻塞队列也是redis的自动过期实例,但是我觉得还是有必要提一下,Redis针对数据都可以设置过期时间,这个特点也是大家应用比较多的,过期的数据清理无需使用方去关注,性能也比较高。最常见的就是:短信验证码、具有时间性的商品展示等,无需像数据库还要去查时间进行对比。

作者寄语

干货有质量,水文有情怀,微信搜索【程序控】,关注这个有趣的灵魂

在这里插入图片描述