首先科普一下CPU缓存,CPU缓存是指可以进行高速数据交换的存储器,它先于内存与CPU交换数据,因此速率很快。缓存的工作原理是当CPU要读取一个数据的时候,首先在CPU缓存中查找,找到就立即读取并送给CPU处理;没有找到,就从速率相对较慢的内存中读取并送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。
为什么要引入CPU缓存?在解释之前必须先了解程序的执行过程,首先从硬盘执行程序,存放到内存,再给cpu运算与执行。由于内存和硬盘的速度相比cpu实在慢太多了,每执行一个程序cpu都要等待内存和硬盘,引入缓存技术便是为了解决此矛盾,缓存与cpu速度一致,cpu从缓存读取数据比cpu在内存上读取快得多,从而提升系统性能。目前主流级CPU都有一级和二级缓存,高端些的甚至有三级缓存;
上面讲述的是CPU缓存,让你读起下面的文章来好有个药引子,如果读不懂没太大问题,继续向下读 。
重点来了! 在开发中我们常常提到的缓存和上面的CPU缓存有异曲同工之妙,但是并不等同于CPU缓存,我们写服务器程序时,使用缓存的目的无非就是减少数据库访问次数降低数据库的压力和提升程序的响应时间, 然而根据具体的使用场景又可以派生出无数种情况:
redis是一个基于C语言实现的高性能的key-value存储系统,运行在内存中但是可以持久化到硬盘上,有着多样的数据结构string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合),还有一些高级数据结构HyperLogLog、Geo、Pub/Sub。每种类型的存储在底层都会存在不同的编码格式(redisObject、SDS等)。
Redis到底强在哪里?
String(字符串)
Redis字符串是字节序列。Redis字符串是二进制安全的,这意味着他们有一个已知的长度没有任何特殊字符终止,所以你可以存储任何东西,512M为上限;
示例:
Hash(hash表)
Redis的哈希是键值对的集合。 Redis的哈希值是字符串字段和字符串值之间的映射,因此它们被用来表示对象。
示例:
xiaoming是对于redis的存储识别Hash的,而Hash真正存储的是key(year、score),value(18、99)。
List(链表)
Redis的链表是简单的字符串列表,排序插入顺序。可以添加元素到Redis的列表的头部或尾部,允许添加重复元素;
示例:
Set(集合)
SortedSet(有序集合)zset
Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。有序集合的成员是唯一的,但分数(score)却可以重复。
示例:
Redis内存模型
接下来我会从redis底层内存存储到系统使用级别来简单介绍redis,什么,一听内存就头大?那你还想不想要走向人生巅峰迎娶白富美,想就乖乖的看、乖乖读、乖乖学!
Redis通过info memory查询内存使用情况,作为内存数据库,在内存中存储的主要是数据,redis内存主要划分为几部分:
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。至于它们的结构在这里暂且不说,我会在下面的文章中详细介绍。
众所周知,事务是指“一个完整的动作,要么全部执行,要么什么也没有做”,提起事务我们会首先想到事务的四大特性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事务不支持事务回滚,如果事务出现了命令执行错误,只会返回当前命令的错误给客户端,不会影响下面的命令的执行,所以很多人觉得和关系型数据库(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可以实现异步的访问,客户端不需要等待服务端的返回结果,可以持续的向服务端发送请求,等待最终把结果全部读取。
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服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
主从复制的作用:
主从复制大概分为三个阶段:连接建立、数据同步、命令传播;
(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节点,只不过不会存储数据。每一个哨兵节点只需要配置监控主节点(可以配置监控多个主节点),便可以自动发现其他的哨兵节点和从节点。
哨兵机制原理
通过向主从节点发送info命令获取最新的主从结构;
通过发布订阅功能获取其他哨兵节点的信息;
通过向其他节点发送ping命令进行心跳检测,判断是否下线。
需要特别注意的是,客观下线是主节点才有的概念;如果从节点和哨兵节点发生故障,被哨兵主观下线后,不会再有后续的客观下线和故障转移操作。
监视该主节点的所有哨兵都有可能被选为领导者,选举使用的算法是Raft算法;Raft算法的基本思路是先到先得:即在一轮选举中,哨兵A向B发送成为领导者的申请,如果B没有同意过其他哨兵,则会同意A成为领导者。选举的具体过程这里不做详细描述,一般来说,哨兵选择的过程很快,谁先完成客观下线,一般就能成为领导者。
(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的大体工作流程和部分原理,想要学习更多关于redis的细节以及实战,请移步redis系列篇。
你知道的越多,你不知道的也越多。keep hungry keep foolish!