Redis 缓存作为使用最多的缓存工具被各大厂商争相使用。通常我们会使用单体的 Redis 应用作为缓存服务,然而我们日常在对于redis的使用中,经常会遇到一些问题:
如果只使用一个redis实例时,其中保存了服务器中全部的缓存数据,这样会有很大风险,如果单台redis服务宕机了将会影响到整个服务。解决的方法就是我们可以采用分片/分区的技术,将原来一台服务器维护的整个缓存,现在换为由多台服务器共同维护内存空间。下面对高可用和高并发处理方案进行介绍。
redis分片概念:按照某种规则去划分数据,分散存储在多个节点上。通过将数据分到多个 Redis 服务器上,来减轻单个 Redis 服务器的压力。
分片思路:采用在一台主机上实现分片的方式,所以只需要在该主机上配置启动三台redis的实例即可。因为redis默认使用的端口号为6379,所以这里我们分别使用6379、6380以及6381三个端口来实现。
分片算法:分片方式虽然解决了高可用的问题,但是分片后数据如何均匀的分步到每个机器上呢?
【重点】实现数据均匀分布在多个节点方式:
①普通的HASH算法
通常的做法就是获取节点的 Hash 值,然后根据节点数求模。 hash(key) % length
但是当缓存服务器变化时(宕机或新增节点),length字段变化,导致所有缓存的数据需要重新进行HASH运算,这样就导致原来的哪些数据访问不到了。而这段时间如果访问量上升了,容易引起服务器雪崩。因此,引入了一致性哈希算法。
②一致性 Hash 算法
该算法对 2^32 取模,将 Hash 值空间组成虚拟的圆环,整个圆环按顺时针方向组织,每个节点依次为 0、1、2…2^32-1。
Hash 环的数据倾斜问题:
Hash 环在服务器节点很少的时候,容易遇到服务器节点不均匀的问题,这会造成数据倾斜,数据倾斜指的是被缓存的对象大部分集中在 Redis 集群的其中一台或几台服务器上。
如上图,一致性 Hash 算法运算后的数据大部分被存放在 A 节点上,而 B 节点只存放了少量的数据,久而久之 A 节点将被撑爆。我们可以通过虚拟节点的方式解决Hash环的偏移。
如果想要均衡的将缓存分布到这三台服务器上,最好能让这三台服务器尽量多的,均匀的出现在Hash环上,但是,真实的服务器资源只有3台,那么如何凭空的让他们多起来呢?
做法就是既然没有多余的真正的物理服务器节点,我们就可能将现有的物理节点通过虚拟的方法复制出来,而被复制出来的节点被称为“虚拟节点”
简单地说,就是为每一个服务器节点计算多个 Hash,每个计算结果位置都放置一个此服务器节点,称为虚拟节点,可以在服务器 IP 或者主机名后放置一个编号实现。例如上图:将 NodeA 和 NodeB 两个节点分为 Node A#1-A#3,NodeB#1-B#3。
一致性Hash能解决容灾问题吗?
我们假设有5台服务器,有a,f,c对象映射在A服务器上,r,t,v对象映射在E服务器上。如下图:
假如此时E服务器宕机了,其他服务器的对象仍然能被命中,因为对象的映射到服务器的位置已经固定了,不会出现因为宕机而让对象找不到。而宕机E上的对象会在下次容灾分配的时候,会把r,t,v这些对象重新分配到就近的服务器上,如下图:
分区的不足:
高可用的解决方案:可以采用哨兵机制实现主从复制从而实现高可用。
所谓集群,就是通过添加服务器的数量,提供相同的服务,从而使服务器达到一个稳定、高效的状态。为什么要使用Redis集群?
总结起来使用集群的原因可以归为提高服务器的稳定性和提高读写能力。
【主从同步,读写分离】
像MySQL一样,redis是支持主从同步的,而且也支持一主多从以及多级从结构。特点:
因此,主从模型可以很大的提高数据库读的能力,也能间接的提高写的能力,由于在Slave中分担Master读的压力,使Master中有更多的资源可以分配到写资源中。
【主从同步原理】
①全量复制
从服务器会向主服务器发出SYNC指令,当主服务器接到此命令后,就会调用BGSAVE指令来创建一个子进程专门进行数据持久化工作,也就是将主服务器的数据写入RDB文件中。在数据持久化期间,主服务器将执行的写指令都缓存在内存中。
在BGSAVE指令执行完成后,主服务器会将持久化好的RDB文件发送给从服务器,从服务器接到此文件后会将其存储到磁盘上,然后再将其读取到内存中。这个动作完成后,主服务器会将这段时间缓存的写指令再以redis协议的格式发送给从服务器。
另外,要说的一点是,即使有多个从服务器同时发来SYNC指令,主服务器也只会执行一次BGSAVE,然后把持久化好的RDB文件发给多个下游。
②部分复制
部分复制是Redis 2.8以后出现的,用于处理在主从复制中因网络闪断等原因造成的数据丢失场景,当从节点再次连上主节点后,如果条件允许,主节点会补发丢失数据给从节点。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销
在redis2.8版本之前,如果从服务器与主服务器因某些原因断开连接的话,都会进行一次主从之间的全量的数据同步;而在2.8版本之后,redis支持了效率更高的增量同步策略,这大大降低了连接断开的恢复成本。
增量同步功能,需要服务器端支持全新的PSYNC指令。这个指令,只有在redis-2.8之后才具有。
注:服务器运行ID(run_id):每个Redis节点(无论主从),在启动时都会自动生成一个随机ID(每次启动都不一样),由40个随机的十六进制字符组成;run_id用来唯一识别一个Redis节点。 通过info server命令,可以查看节点的run_id。
【主从同步数据不一致问题】
主库进行写操作,同时同步从库,这时还没有完成主从同步,新来个查请求会在从库查询到旧值,完后数据完成主从同步。解决方案:
附上链接:https://blog.csdn.net/zdy0_2004/article/details/50565117
【扩展1】数据库和缓存的不一致问题的解决方案:
两个实践:第一个是缓存双淘汰机制,第二个是建议为所有item设定过期时间(前提是允许cache miss)。
(1)缓存双淘汰,传统的玩法在进行写操作的时候,先淘汰cache再写主库。上文提到,在主从同步时间窗口之内可能有脏数据入cache,此时如果再发起一个异步的淘汰,即使不一致时间窗内脏数据入了cache,也会再次淘汰掉。
public void write(String key,Object data){
redis.delKey(key);
db.updateData(data);
Thread.sleep(1000);
redis.delKey(key);
}
这个1秒怎么确定的,具体该休眠多久呢?
针对上面的情形,读者应该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
(2)为所有item设定超时时间,例如10分钟。极限时序下,即使有脏数据入cache,这个脏数据也最多存在十分钟。带来的副作用是,可能每十分钟,这个key上有一个读请求会穿透到数据库上,但我们认为这对数据库的从库压力增加是非常小的。
【扩展2】redis持久化机制
聊了主从同步问题,避免不了redis持久化问题。Redis 将数据存储在内存而不是磁盘中,所以内存一旦断电,Redis 中存储的数据也随即消失,这往往是用户不期望的,所以 Redis 有持久化机制来保证数据的安全性。redis提供了两种持久化的方式,分别是RDB(Redis DataBase)和AOF(Append Only File)。
①聊聊redis持久化 – RDB
RDB方式,是将redis某一时刻的数据持久化到磁盘中,是一种快照式的持久化方法。
redis在进行数据持久化的过程中,会先将数据写入到一个临时文件中,待持久化过程都结束了,才会用这个临时文件替换上次持久化好的文件。正是这种特性,让我们可以随时来进行备份,因为快照文件总是完整可用的。
对于RDB方式,redis会单独创建(fork)一个子进程来进行持久化,而主进程是不会进行任何IO操作的,这样就确保了redis极高的性能。
RDB 持久化方式的缺点如下:
RDB指令:
SAVE:阻塞 Redis 的服务器进程,直到 RDB 文件被创建完毕。SAVE 命令很少被使用,因为其会阻塞主线程来保证快照的写入,由于 Redis 是使用一个主线程来接收所有客户端请求,这样会阻塞所有客户端请求。
BGSAVE:该指令会 Fork 出一个子进程来创建 RDB 文件,不阻塞服务器进程,子进程接收请求并创建 RDB 快照,父进程继续接收客户端的请求。
子进程在完成文件的创建时会向父进程发送信号,父进程在接收客户端请求的过程中,在一定的时间间隔通过轮询来接收子进程的信号。
我们也可以通过使用 lastsave 指令来查看 BGSAVE 是否执行成功,lastsave 可以返回最后一次执行成功 BGSAVE 的时间。
②聊聊redis持久化 – AOF
相对 RDB 来说,RDB 持久化是通过备份内存中某一时刻数据的全量快照实现持久化,而 AOF 持久化是备份数据库接收到的写指令,在数据恢复时按照从前到后的顺序再将指令都执行一遍,就这么简单:
因为采用了追加方式,如果不做任何处理的话,AOF文件会变得越来越大,为此,redis提供了AOF文件重写(rewrite)机制,即当AOF文件的大小超过所设定的阈值时,redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。举个例子或许更形象,假如我们调用了100次INCR指令,在AOF文件中就要存储100条指令,但这明显是很低效的,完全可以把这100条指令合并成一条SET指令,这就是重写机制的原理。
重写过程如下:
注:fork() 在 Linux 中创建子进程采用 Copy-On-Write(写时拷贝技术),即如果有多个调用者同时要求相同资源(如内存或磁盘上的数据存储)。他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给调用者,而其他调用者所见到的最初的资源仍然保持不变。
开启 AOF 持久化指令:
①打开 redis.conf 配置文件,将 appendonly 属性改为 yes。
②修改 appendfsync 属性,该属性可以接收三种参数,分别是 always,everysec,no。
always 表示总是即时将缓冲区内容写入 AOF 文件当中,everysec 表示每隔一秒将缓冲区内容写入 AOF 文件,no 表示将写入文件操作交由操作系统决定。
一般来说,操作系统考虑效率问题,会等待缓冲区被填满再将缓冲区数据写入 AOF 文件中。
AOF 和 RDB 的优缺点如下:
RDB-AOF 混合持久化方式
Redis 4.0 之后推出了此种持久化方式,RDB 作为全量备份,AOF 作为增量备份,并且将此种方式作为默认方式使用。
问题:在上述两种方式中,RDB 方式是将全量数据写入 RDB 文件,这样写入的特点是文件小,恢复快,但无法保存最近一次快照之后的数据,AOF 则将 Redis 指令存入文件中,这样又会造成文件体积大,恢复时间长等弱点。
方案:在 RDB-AOF 方式下,持久化策略首先将缓存中数据以 RDB 方式全量写入文件,再将写入后新增的数据以 AOF 的方式追加在 RDB 数据的后面,在下一次做 RDB 持久化的时候将 AOF 的数据重新以 RDB 的形式写入文件。
可以说,在此种方式下的持久化文件,前半段是 RDB 格式的全量数据,后半段是 AOF 格式的增量数据。此种方式是目前较为推荐的一种持久化方式。
这种方式既可以提高读写和恢复效率,也可以减少文件大小,同时可以保证数据的完整性。
Redis的主从模式配置简单,在提高单台服务器数据库读性能的同时,也能间接性的提高写的能力;与此同时,它的弊端也显而易见,那就是当主节点Master宕机后,整个集群就没有可写的节点了,为此就衍生出了一种新的模式,Sentinel(哨兵模式)
Redis的哨兵(sentinel) 机制用于管理多个 Redis 服务器,该系统执行以下三个任务:
哨兵(sentinel) 是一个分布式系统,你可以在一个架构中运行多个哨兵(sentinel) 进程, 这些进程使用流言协议(gossipprotocols)来接收关于Master是否下线的信息,并使用投票协议(agreement protocols)来决定是否执行自动故障迁移,以及选择哪个Slave作为新的Master。
每个哨兵(sentinel) 会向其它哨兵(sentinel)、master、slave定时发送消息,以确认对方是否”活”着,如果发现对方在指定时间(可配置)内未回应,则暂时认为对方已挂(所谓的”主观认为宕机” Subjective Down,简称sdown).
若“哨兵群”中的多数sentinel,都报告某一master没响应,系统才认为该master"彻底死亡"(即:客观上的真正down机,Objective Down,简称odown),通过一定的vote算法,从剩下的slave节点中,选一台提升为master,然后自动修改相关配置。
注:选点的依据依次是:网络连接正常->5秒内回复过INFO命令->10*down-after-milliseconds内与主连接过的->从服务器优先级->复制偏移量->运行id较小的。选出之后通过slaveif no ont将该从服务器升为新主服务器。
附上链接:https://blog.csdn.net/u012240455/article/details/81843714
Redis在3.0版正式引入了集群这个特性。Redis集群是一个分布式(distributed)、容错(fault-tolerant)的 Redis内存K/V服务, 集群可以使用的功能是普通单机 Redis 所能使用的功能的一个子集(subset),比如Redis集群并不支持处理多个keys的命令,因为这需要在不同的节点间移动数据,从而达不到像Redis那样的性能,在高负载的情况下可能会导致不可预料的错误。
Redis集群的几个重要特征:
(1). Redis 集群的分片特征在于将键空间分拆了16384个槽位,每一个节点负责其中一些槽位。
(2). Redis提供一定程度的可用性,可以在某个节点宕机或者不可达的情况下继续处理命令.
(3). Redis 集群中不存在中心(central)节点或者代理(proxy)节点, 集群的其中一个主要设计目标是达到线性可扩展性(linear scalability)。
Redis 集群没有使用一致性hash, 而是引入了 哈希槽的概念.Redis 集群有16384个哈希槽(slot),每个key通过CRC16校验后对16384取模来决定放置哪个槽.集群的每个节点负责一部分hash槽,也就是说,*** 每个slot都对应一个node负责处理。当动态添加或减少node节点时,需要将16384个槽做个再分配,槽中的键值也要迁移 ***。举个例子,比如当前集群有3个节点,那么:
这种结构很容易添加或者删除节点. 比如如果我想新添加个节点D, 我需要从节点 A, B, C中得部分槽到D上. 如果我想移除节点A,需要将A中的槽移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可. 由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态.
Redis Cluster特点如下:
Redis Cluster的新节点识别能力、故障判断及故障转移能力是通过集群中的每个node都在和其它nodes进行通信,这被称为集群总线(cluster bus)。它们使用特殊的端口号,即对外服务端口号加10000。例如如果某个node的端口号是6379,那么它与其它nodes通信的端口号是16379。
缓存信息通常是用 Key-Value 的方式来存放的,在存储信息的时候,集群会对 Key 进行 CRC16 校验并对 16384 取模(slot = CRC16(key)%16383)。得到的结果就是 Key-Value 所放入的槽,从而实现自动分割数据到不同的节点上。然后再将这些槽分配到不同的缓存节点中保存。
如图 所示,假设有三个缓存节点分别是 1、2、3。Redis Cluster 将存放缓存数据的槽(Slot)分别放入这三个节点中:
缓存节点 1 存放的是(0-5000)Slot 的数据。
缓存节点 2 存放的是(5001-10000)Slot 的数据。
缓存节点 3 存放的是(10000-16383)Slot 的数据。
此时 Redis Client 需要根据一个 Key 获取对应的 Value 的数据,首先通过 CRC16(key)%16383 计算出 Slot 的值,假设计算的结果是 5002。将这个数据传送给 Redis Cluster,集群接受到以后会到一个对照表中查找这个 Slot=5002 属于那个缓存节点。
发现属于“缓存节点 2”,于是顺着红线的方向调用缓存节点 2 中存放的 Key-Value 的内容并且返回给 Redis Client。
Redis Cluster中有一个16384长度的槽的概念,他们的编号为0、1、2、3……16382、16383。这个槽是一个虚拟的槽,并不是真正存在的。正常工作的时候,Redis Cluster中的每个Master节点都会负责一部分的槽,当有某个key被映射到某个Master负责的槽,那么这个Master负责为这个key提供服务,至于哪个Master节点负责哪个槽,这是可以由用户指定的,也可以在初始化的时候自动生成(redis-trib.rb脚本)。这里值得一提的是,在Redis Cluster中,只有Master才拥有槽的所有权,如果是某个Master的slave,这个slave只负责槽的使用,但是没有所有权。Redis Cluster怎么知道哪些槽是由哪些节点负责的呢?某个Master又怎么知道某个槽自己是不是拥有呢?
Master节点维护着一个16384/8字节的位序列,由于每个字节包含 8 个 bit 位(二进制位),所以共包含 16384 个 bit,也就是 16384 个二进制位。Master节点用bit来标识对于某个槽自己是否拥有。假设这个图表示节点 A 所管理槽的情况。
通过二进制数组存放槽信息0、1、2 三个数组下标就表示 0、1、2 三个槽,如果对应的二进制值是 1,表示该节点负责存放 0、1、2 三个槽的数据。同理,后面的数组下标位 0 就表示该节点不负责存放对应槽的数据。用二进制存放的优点是,判断的效率高,例如对于编号为 1 的槽,节点只要判断序列的第二位,时间复杂度为 O(1)。
Redis集群,要保证16384个槽对应的node都正常工作,如果某个node发生故障,那它负责的slots也就失效,整个集群将不能工作。
为了增加集群的可访问性,官方推荐的方案是将node配置成主从结构,即一个master主节点,挂n个slave从节点。这时,如果主节点失效,Redis Cluster会根据选举算法从slave节点中选择一个上升为主节点,整个集群继续对外提供服务,Redis Cluster本身提供了故障转移容错的能力。
图中描述的是六个redis实例构成的集群
6379端口为客户端通讯端口,16379端口为集群总线端口。集群内部划分为16384个数据分槽,分布在三个主redis中。
从redis中没有分槽,不会参与集群投票,也不会帮忙加快读取数据,仅仅作为主机的备份。三个主节点中平均分布着16384数据分槽的三分之一,每个节点中不会存有有重复数据,仅仅有自己的从机帮忙冗余。
其中三个节点对应16384个哈希槽,下面以6个槽为列进行说明:
从这种redis cluster的架构图中可以很容易的看出首先将数据根据hash规则分配到6个slot中(这里只是举例子分成了6个槽),然后根据CRC算法和取模算法将6个slot分别存储到3个不同的Master节点中,每个master节点又配套部署了一个slave节点,当一个master出现问题后,slave节点可以顶上。这种cluster的方案对比第一种简单的主从方案的优点在于提高了读写的并发,分散了IO,在保障高可用的前提下提高了性能。
Codis是一个豌豆荚团队开源的使用Go语言编写的Redis Proxy使用方法和普通的redis没有任何区别,设置好下属的多个redis实例后就可以了,使用时在本需要连接redis的地方改为连接codis,它会以一个代理的身份接收请求 并使用一致性hash算法,将请求转接到具体redis,将结果再返回codis,和之前比较流行的twitter开源的Twemproxy功能类似,但是相比官方的redis cluster和twitter的Twemproxy还是有一些独到的优势
优点:Codis一个比较大的优点是可以不停机动态新增或删除数据节点,旧节点的数据也可以自动恢复到新节点。并且提供图形化的dashboard,方便集群管理。
codis方案推出的时间比较长,而且国内很多互联网公司都已经使用了该集群方案,所以该方案还是比较适合大型互联网系统使用的,毕竟成功案例比较多,但是codis因为要实现slot切片,所以修改了redis-server的源码,对于后续的更新升级也会存在一定的隐患。,但是codis的稳定性和高可用确实是目前做的最好的,只要有足够多的机器能够做到非常好的高可用缓存系统。
对于小规模应用最好还是使用官方的cluster方案,在redis3.0后官方社区也在逐步完善cluster方案,小团队可以随着官方版本的升级享受功能和稳定性的提升,也便于日后的维护。
(1)心跳和gossip消息
Redis Cluster持续的交换PING和PONG数据包。这两种数据包的数据结构相同,都包含重要的配置信息,唯一的不同是消息类型字段。PING和PONG数据包统称为心跳数据包。
每个节点在每一秒钟都向一定数量的其它节点发送PING消息,这些节点应该向发送PING的节点回复一个PONG消息。节点会尽可能确保拥有每个其它节点在NOTE_TIMEOUT/2秒时间内的最新信息,否则会发送一个PING消息,以确定与该节点的连接是否正常。
假定一个Cluster有301个节点,NOTE_TIMEOUT为60秒,那么每30秒每个节点至少发送300个PING,即每秒10个PING, 整个Cluster每秒发送10x301=3010个PING。这个数量级的流量不应该会造成网络负担。
(2)故障检测。
Redis Cluster的故障检测用于检测一个master节点何时变得不再有效,即不能提供服务,从而应该让slave节点提升为master节点。如果提升失败,则整个Cluster失效,不再接受客户端的服务请求。
Redis Cluster故障检测机制最终应该让所有节点都一致同意某个节点处于某个确定的状态。如果发生这样的情况少数节点确信某个节点为FAIL,同时有少数节点确认某个节点为非FAIL,则Redis Cluster最终会处于一个确定的状态:
文章开始讲到,Redis Cluster并不会代理查询,那么如果客户端访问了一个key并不存在的节点,这个节点是怎么处理的呢?比如我想获取key为msg的值,msg计算出来的槽编号为254,当前节点正好不负责编号为254的槽,那么就会返回客户端下面信息:
GET msg
-MOVED 254 127.0.0.1:6381
表示客户端想要的254槽由运行在IP为127.0.0.1,端口为6381的Master实例服务。如果根据key计算得出的槽恰好由当前节点负责,则当期节点会立即返回结果。这里明确一下,没有代理的Redis Cluster可能会导致客户端两次连接急群中的节点才能找到正确的服务,推荐客户端缓存连接,这样最坏的情况是两次往返通信。
Redis Cluster采用两种方式进行各个master节点的slots配置信息的传播。所谓slots配置信息,即master负责存储哪几个slots。
(1)所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.
(2)节点的fail是通过集群中超过半数的节点检测失效时才生效.
(3)客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
(4)redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->value
Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。
至此redis高并发高可用集群方案介绍完毕 ,如有疑问欢迎交流。最后提出一个问题点:Redis 集群中为什么内置 16384 个哈希槽呢?下期见
文章参考:
https://www.jianshu.com/p/1ecbd1a88924
https://www.cnblogs.com/kerwinC/p/6611634.html
https://www.cnblogs.com/huangfuyuan/p/9880379.html
https://blog.51cto.com/14279308/2484807?source=drh