一、常用基本数据类型与结构
1、数据类型
String 字符串;
List 列表;
Hash 哈希;
Set 集合;
Sorted Set(zset) 有序集合
2、基本数据类型其底层数据结构
2.1、底层数据结构与时间复杂度
简单动态字符串;
双向链表 O(N);
压缩列表 O(N);
哈希表 O(1);
跳表 O(log N);
整数数组 O(N);
2.2、redis数据类型和底层数据结构的对应关系
2.2.1 string底层结构
redis - string类型除了记录实际数据,还有内存空间记录、数据长度、空间使用等消息,这些信息叫作元数据。
2.2.2、整数数组和双向链表
整数数组和双向链表它们操作特征都是顺序读写,也就是数组下标或者链表的指针逐个元素访问,操作复杂度是O(N),操作效率比较低。
2.2.3、压缩列表
压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据,但压缩列表在表头有三个字段:zlbytes、zltail和zllen,分别表示列表长度、列表尾的偏移量和列表中entry个数,压缩列表在表尾有一个zlend,表示列表结束,如下图所示:
查找第一个元素和最后一个元素,可以通过表头三个字段直接定位,时间复杂度为O(1),查找其它元素时为O(N)。
拓展:压缩列表与双向链表比较www.cnblogs.com/qiezi777/p/…
2.2.4、跳表
跳表是在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位,如下图所示:
2.2.5、全局哈希表
redis为了实现从键到值的快速访问,redis使用了一个哈希表来保存所有键值对,一个哈希表,其实就是一个数组,数组的每个元素称为哈希桶,每个哈希桶中保存了键值对数据,如下图所示:
1、全局哈希表的好处
可以通过O(1)的时间复杂度快速找到键值对,只需要计算键的哈希值,就可以找到对应的哈希桶位置,定位相应的entry元素。
2、全局哈希表潜在风险点
1、哈希表的冲突问题;
2、rehash可能带来操作阻塞;
上两点都有可能导致哈希表操作变慢。
3、链式哈希
当哈希表写入更多数据时,哈希冲突是不可避免的问题,哈希冲突是指两个key的哈希值和哈希桶计算对应的关系时,正好在同一个哈希桶中。
redis解决哈希冲突的方式就是链式哈希,是指同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接,如下图所示:
4、rehash操作
为啥要进行rehash操作?
哈希冲突链上的元素只能通过指针逐一查找再操作,极端情况下随着数据写入越来越多,哈希冲突可能越来越多,导致哈希冲突链过长,导致链上元素查找耗时长,效率低。
rehash操作过程
redis进行rehash操作,是增加现有的哈希桶数量,将entry元素更分散保存,减少哈希冲突,redis默认使用两个全局哈希表,哈希表1和哈希表2,一开始默认使用哈希表1,此时的哈希表2并没有被分配空间,随着数据越来越多,进行rehash操作,这个过程分为三步:
1、给哈希表2分配更大的空间,是当前哈希表1大小的两倍;
2、哈希表1中的数据重新映射并拷贝到哈希表2中;
3、释放哈希表1的空间。
第二步涉及到大量的数据拷贝,如何一次性把哈希表1中的数据都迁移完,会导致redis线程阻塞,无法服务其它请求,为了避免这问题,redis采用了渐进式rehash。
渐进式rehash
简单来说就是在第二步拷贝数据时,redis仍然正常处理客户端请求,每处理一个请求时,从哈希表1中的第一个索引位置开始,顺带着将这个索引位置上的所有entries拷贝到哈希表2中,等处理下一个请求时,再顺带拷贝哈希表1中的下一个索引位置的rentries,如下图所示:
这样就巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。
二、高性能IO模型
1、单线程redis为啥那么快?
a、redis的大部分操作在内存上完成;
b、高效的数据结构;
c、IO多路复用机制。
2、多路复用高性能I/O模型
redis只允许单线程的情况下,允许内核中同时存在多个监听套接字和已连接套接字,内核会一直监听这些套接字上连接请求或数据请求。一旦请求到达,就会交给redis线程处理,实现了一个redis处理多个IO流的效果,如下图所示:
图中多个FD就是套接字,redis网络框架调用epoll机制,让内核监听这些套接字,为了请求到达时能通知到redis线程,select/epoll提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。
回调机制
select/epoll一旦监测到FD上有请求到达时,就会触发相应的事件,这些事件会被放进一个事件队列,redis单线程对该事件队列不断进行处理,redis无需轮询是否有请求实际发生,避免了CPU资源浪费,提升了redis的响应性能。
3、redis单线程处理IO请求性能瓶颈
a、任意一个请求在server中一旦发生耗时,都会影响整个server的性能,如以下例子:
-
操作bigkey:写入或者删除一个bigkey时,分配内存或者释放内存会产生耗时。
-
使用复杂度过高的命令:例如:sort/sunion/zunionstore或者时间复杂度为O(N)的命令,但是N很大,比如:lran ge key 0-1 一次性查询全量数据。
-
大量可以集中过期:redis的过期机制也是在主线程中执行,大量key集中过期会导致处理一个请求时,耗时都在删除过期key,导致操作时间变长。
-
淘汰策略:淘汰策略也是在主线程执行的,当内存超过redis内存上限后,每次写入都需要淘汰一些key,也会造成耗时变长。
-
AOF刷盘开启always机制:每次写入都需要把这个操作刷到磁盘中,写磁盘的速度远比写入内存慢,会拖慢redis的性能。
-
主从全量同步生成RDB:虽然采用fork子进程生成数据快照,但fork这一瞬间会阻塞整个线程,实例越大,阻塞时间越久。
b、并发量非常大时,单线程读写客户端IO数据,存在性能瓶颈,虽然采用IO多路复用机制,但是读写客户端依旧是同步IO,只能单线程依次读取客户端的数据,无法利用到CPU多核。
针对以上两种问题处理方式:
问题1处理方式:
a、业务人员规避;
b、redis4.0推出lazy-free机制,把bigkey释放内存的耗时操作,放在了异步线程中执行,降低了对主线程的影响。
问题2处理方式:
redis3.0推出多线程,可以在高并发场景利用CPU多核多线程读写客户端数据,进一步提升server性能,只针对客户端的读写是并行的,每个命令的真正操作依旧是单线程的。
三、redis持久化机制:AOF日志和RDB快照
1、AOF日志
AOF是一种写后日志,意思是redis先执行命令,把数据写入内存,然后再记录日志。
1.1、AOF记录文件格式
如set testkey testvalue为例,格式如下:
*3
$3
set
$7
testkey
$9
testvalue
*3 表示命令有三个部分,每个部分都是由"$ + 数字"开头,后面命令,键,值。数字表示这部分中的命令,键,值有多少字节。
1.2、好处
1、先让系统执行命令,只有命令能执行成功,才会被记录到日志中,可以避免出现记录错误命令的情况。
2、不会阻塞当前的写操作。
1.3、潜在风险点
1、先写内存,还未来得及记录日志就宕机,会导致缺少数据。
2、AOF虽然避免了对当前命令的阻塞,但是在AOF刷入磁盘过程中带来阻塞风险,这个操作也是在主线程中执行的。
控制一个写命令执行完后AOF日志写回磁盘的时机,也就是写回策略,这两个风险就解除啦!
1.4、写回策略
AOF配置appendfsync的三个可选值:
-
Always(同步写回):每个写命令执行完,立马同步地将日志写回磁盘。
-
Everysec(每秒写回):每个写命令执行,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘。
-
NO(操作系统控制的写回):每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘中。
1.5、写回策略优缺点
1.6、重写机制AOF文件太大了怎么办?
AOF是采用追加写的形式,不断执行命令都会记录到一个文件中,会造成性能问题:
1、文件系统本身不能保存过大的文件。
2、文件过大,之后追加写效率也会慢。
3、文件过大,之后故障恢复过程也会慢。
解决方案:AOF重写机制,也就是redis根据数据库的现状创建一个新的AOF文件,一个键值对被多条写命令反复修改,AOF文件会记录相应的多条命令,在重写的时候,多条命令整合成一条命令,这样一个键值对在重写日志或者日志恢复只需要一条命令。
1.7、AOF重写机制是否阻塞
重写机制过程:
1、每次执行重写时,主线程fork出来后台的bgrewriteaof子进程,fork会把主线程的内存拷贝一份给bgrewriteaof子进程,bgrewriteaof子进程就可以在不影响主线程的情况下,将数据写入新的AOF文件。
2、当有新的数据,则会记录到aof旧日志的缓存区和重写日志的缓存区,保证数据不丢失,不会影响当前次的重写。
3、当第一步拷贝数据的所有操作记录重写完成后,会继续从AOF重写缓冲区,重写到新的AOF文件。
1.8、redis采用fork子进程重写AOF文件潜在阻塞风险
问题一、redis采用fork子进程重写AOF文件时,潜在的阻塞风险包括:fork子进程和AOF重写过程中父进程产生写入的场景,下面依次介绍:
a、fork子进程,fork这个瞬间一定是会阻塞主线程的(fork时并不会一次性拷贝所有内存数据给子进程),fork采用操作系统提供的写实复制(copy on write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成长时间阻塞问题,但fork子进程需要拷贝进程必要的数据结构,其中有一项就是拷贝内存页表(虚拟内存和物理内存的映射索引表),这个拷贝过程会消耗大量CPU资源,拷贝完成之前整个进程是会阻塞的,阻塞时间取决于整个实例的内存大小,实例越大,内存页表越大,fork阻塞时间越久。拷贝内存页表完成后,子进程与父进程指向相同的内存地址空间,也就是说此时虽然产生了子进程,但是并没有申请与父进程相同的内存大小。
那什么时候父子进程才会真正内存分离呢?
"写实复制"顾名思义就是在写发生时,才真正拷贝内存真正的数据,这个过程中,父进程可能会产产生阻塞的风险,就如下面介绍的场景。
b、fork出的子进程指向与父进程相同的内存地址空间,此时子进程就可以执行AOF重写,把内存中的所有数据写入到AOF文件中。但此时父进程依旧是会有流量写入的,如果父进程操作的是一个已经存在的key,那么这个时候父进程就会真正拷贝这个key对应的内存数据,申请新的内存空间,这样逐渐地,父子进程内存数据开始分离,父子进程逐渐拥有各自独立的内存空间。
因为内存分配是以页为单位进行分配的,默认4KB,如果父进程此时操作的是一个bigkey,重新申请大块内存耗时会变长,可能会产生阻塞风险。另外,如果操作系统开启了内存大页机制(Huge Page ,页面大小为2MB),那么父进程申请内存时阻塞的概率将会大大提高,所以redis机器上需要关闭Huge Page机制。redis每次fork生成RDB或AOF重写完成后,都可以在redis log中看到父进程重新申请了多大的内存空间。
问题二、AOF重写不复用AOF本身的日志,一个原因是父子进程写同一个文件必然会产生竞争问题,控制竞争就意味着会影响父进程的性能。
二是如果AOF重写过程中失败了,那么原本的AOF文件相当于被污染了,无法作恢复使用。所以 redis AOF重写一个新文件,重写失败的话,直接删除这个文件就好了,不会对原先的AOF文件产生影响。等重写完成之后,直接替换旧文件即可。
2、RDB内存快照
RDB持久化是把当前进程数据生成快照以二进制形式保存到磁盘中。
2.1、RDB持久化原理与过程
bgsave是主流的RDB持久化方式,如下图所示:
-
执行bgsave命令,redis父进程判断当前是否存在正在执行的子进程,如RDB/AOF子进程,如果存在bgsave命令直接返回。
-
父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞,通过info stats命令查看latest_fork_usec选项,可以获取最近一个fork操作的耗时,单位为微秒。
-
父进程fork完成后,bgsave命令返回"background saving stared"信息并不在阻塞父进程,可以继续响应其他命令。
-
子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换,执行last_save命令可以获取最后一次生成RDB的时间,对应info统计的rdb_last_save_time选项。
-
子进程发送信号给父进程表示完成,父进程更新统计信息。
2.2、写时复制
redis使用操作系统的多进程copy on write(写时复制) 机制来实现快照持久化,redis在持久化是会调用glibc的函数fork产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求,子进程产生时,会与父进程共享内存里面的代码段和数据段,为了节约内存资源。
-
子进程做数据持久化,不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。
-
父进程则持续服务客户端请求,然后对内存数据结构进行不间断的修改,这时候redis利用copy on write机制,将被共享的页面复制一份出来,然后对这个复制页面进行修改,子进程访问的还是原先共享的数据,每个页面的大小只有4KB,如下图所示:
2.3、RDB文件的处理
RDB文件处理流程如下:
1、生成临时rdb文件,并写入数据。
2、完成数据写入,用临时文件替代正式rdb文件。
3、删除原来的rdb文件(RDB默认生成文件名为dump.rdb)
2.4、RDB持久化优缺点
优点:
1、RDB文件小,非常适合定时备份,用于灾难恢复。
2、因为RDB文件中直接存储的是内存数据,而AOF文件中存储的是一条条命令,需要应用命令,redis加载RDB文件速度比AOF快。
缺点:
1、RDB持久化不能做到实时/秒级持久化
-
实时持久化需要全量刷内存到磁盘,成本太高。
-
每秒fork子进程也会阻塞主进程,影响性能。
2、使用RDB方式实现持久化,一旦redis异常退出,就会丢失最后一次快照,以后更改的所有的数据。
建议:
1、根据具体的应用场景,通过组合设置自动快照条件的方式,来将可能发生的数据损失控制在能够接受范围。
2、数据相对来说比较重要,希望将损失降到最小,则可以使用AOF方式进行持久化。
3、持久化问题
3.1、做快照是否阻塞
-
save:在主线程中执行,必定会阻塞。
-
bgsave:创建子进程,用于写入RDB文件,避免了主线程的阻塞,这也是默认配置。
3.2、RDB给哪些数据做快照
RDB执行的是全量快照,也就是把内存中的数据记录到磁盘中,全量数据越多,RDB文件就越大,往磁盘上写数据的时间开销就越大。
3.3、快照时数据能否修改
能,redis在持久化时会调用glibc的函数fork产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求,当主线程执行写指令修改数据的时候,这个数据就会复制一份副本,bgsave子进程读取这个副本数据写到RDB文件。如下图所示:
3.4、能否每秒做一次快照
虽然bgsave执行时不阻塞主线程,但是频繁地执行全量快照,也会带来两方面的开销:
-
频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没做完,后一个又开始做了,容易造成恶性循环。
-
bgsave子进程需要通过fork操作从主线程创建出来,但是fork创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长,频繁fork出bgsave子进程也会频繁阻塞主线程。
3.5、增量快照
所谓增量快照,就是指做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。
- 在第一次做完全量快照后,T1和T2时刻,如果再做快照,我们只需要将被修改的数据写入快照文件就行。但是,这么做的前提是,我们需要记住哪些数据被修改了,也就是需要我们使用额外的元数据信息去记录哪些数据被修改了,这会带来额外的空间开销问题。如下图所示:
- 如果我们对每一个键值对的修改都做个记录,那么,如果有1万个被修改的键值对,我们就需要有1万条额外的记录。而且,有时候,键值对非常小,比如:只有32字节,而记录它被修改的元数据信息,可能需要8字节,这样的话,为了"记住"修改,引入的额外空间开销比较大。这对于内存资源宝贵的redis来说,有些得不偿失。
可以看出,虽然跟AOF相比,快照的恢复速度快,但是快照的频率不好把握,如果频率太低,两次快照间一旦宕机,就可能有比较多的数据丢失。
如果频率太高,又会产生额外开销,那么还有什么方法既能利用RDB的快速恢复,又能以较小的开销做到尽量少丢失数据,混合模式即可。
3.6、AOF和RDB混合模式
内存快照一定的频率执行,在两次快照之间,使用AOF日志记录这期间的所有命令操作。
-
RDB快照不用很频繁地执行,避免了频繁fork对主线程的影响。
-
AOF日志不需要记录所有的操作,避免文件过大的情况,重写的开销。
四、数据同步
redis高可靠性是AOF和RDB持久化混合模式 和 服务的高可用实现的。
服务的高可用redis用两种模式:主从模式和集群切片模式。
主从库模式采用的是读写分离方式:
-
读操作:从库。
-
写操作:首先到主库执行,然后主库将写操作同步给从库。
1、主从库第一次同步
当有多个redis实例的时候,它们相互之间就可以通过replicaof(redis5.0之前使用slaveof)命令形成主从库关系,之后按照三个阶段完成数据的第一次同步。
例如,现在有两个实例(ip1:172.16.19.3; ip2:172.16.19.5),在ip2实例执行如下命令,ip2就是成为ip1的从库replicaof 172.16.19.3:6739
第一次主从库数据同步流程,如下图所示:
1、阶段一:从库和主库建立起链接,并告诉主库即将进行同步,主库确认回复后,主从库间可以开始同步
-
从库给主库发送psync命令,表示进行数据同步,主库根据主库的runId和复制度offset参数来启动复制。
-
runId:每个redis实例启动时都会自动生成一个随机Id,用来标记这个实例保证唯一性。
-
offset:设为-1,表示第一次复制。
-
主库收到psync命令后,会用fullresync响应命令带上两个参数:主库runId和主库目前的复制进度offset返回给从库,从库收到响应后,会记录这两个参数。
2、阶段二:主库将所有数据同步给从库,从库收到数据后,在本地完成数据加载,从库为了避免之前数据的影响,从库会把之前的数据清除掉。
- 主从库数据同步过程中,主库不会被阻塞,仍然可以正常接受请求,新的请求数据,主库会在内存中用专门的replication buff,记录新的数据,保证主从库的数据一致性。
3、阶段三:将replication buffer中的新数据发送给从库。
2、主从级联模式分担全量复制时的主库压力
主从库间数据同步过程中,会产生两个耗时操作:生成RDB文件和传输RDB文件。
如果从库很多,都要和主库进行全量复制的话,会有以下两个问题:
1、主库fork子进程生成RDB文件会阻塞主线程处理正常请求,导致主库响应速度变慢。
2、传输RDB文件会占用主库的网络带宽,同样会给主库的资源使用带来压力。
3、主-从-从模式
通过"主-从-从"模式将主库生成RDB和传输RDB的压力,以级联的方式分散到从库上,如下图所示:
4、主从库间网络断了怎么办
一旦主从库完成了全量复制,redis基于长连接会将后续新的数据操作同步给从库,避免频繁建立连接的开销,这个过程中存在着风险点,最常用的是网络断连或阻塞。
主从库断连后,主库会把断连期间收到的写操作命令,写入replication buffer中,同时也会写入在repl_backlog_buffer这个缓冲区中。
repl_backlog_buffer是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录了自己已经读到的位置。主库对应master_repl_offset,会随着新的写操作偏移量逐渐变大,从库对应slave_repl_offset,会随着从库读的位置逐渐变大,正常情况下,这两个偏移量基本相等。
主从库之间连接恢复后,从库首先会给主库发送psync命令,并把自己当前的slave_repl_offset发给主库,主库会判断master_repl_offset和slave_repl_offset之间的差距,增量复制流程图如下:
注意点:因为repl_backlog_buffer是环形缓冲区,如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,导致主从库之间的数据不一致。
为避免上面的情况,可以调整repl_backlog_size参数,这个参数和所需要的缓冲空间大小有关。缓冲空间的计算公式是:缓冲空间大小 = (主库写入命令速度 * 操作大小) - (主从库间网络传输命令速度 * 操作大小)。
考虑到可能存在一些突发的请求压力,可以把缓冲空间扩大一倍,例如:如果主库每秒写入2000个操作,每个操作的大小为2KB,网络每秒能传输1000个操作,那么有1000个操作需要缓冲起来,这就至少需要2MB的缓冲空间。否则,新写的命令就会覆盖掉旧操作了。为了应对可能的突然压力,我们最终把repl_backlog_size设为4MB。
五、哨兵机制
哨兵就是一个运行在特殊模式下的redis进程,主从库实例运行的同时,它在运行。哨兵主要负责三个任务:监控、选主和通知。
1、哨兵的基本流程
-
哨兵进程在运行时,周期性地给所有主从库发送ping命令,检测它们是否仍然在运行,如果从库没有响应哨兵的ping命令,哨兵就会把该实例标记为"下线状态",该阶段为监控。
-
若主库挂了,哨兵就会从很多从库里,按照一定的规则从从库选择实例,把它作为新的主库,该阶段为选主。
-
选择完主库后,哨兵会把新主库的连接信息发给其它从库,让它们执行replicaof命令,和新主库建立连接,并进行数据复制,同时哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上。
2、主观下线
哨兵进程会使用ping命令检测它自己和主从库的网络连接情况,用来判断实例的状态。如果哨兵发现主库或从库对ping命令响应超时,哨兵就会把它标记为主观下线。
如果检测的是从库,哨兵简单地把它标记为主观下线就行,但检测的是主库不能这样简单的标记为主观下线,开启主从切换,可能存在哨兵因为网络问题导致误判,其实主库并没有故障。一旦启动了主从切换,后续的选主和通知操作都会带来额外的计算和通信开销。
3、客观下线
在判断主库是否下线时,需要多数哨兵实例都判断主库已经主观下线了,主库才可以标记为客观下线。客观下线的标准就是当有N个哨兵实例时,最好要有N/2 + 1 实例判断主库为主观下线,这样就可以避免单个哨兵因为自身网络状况不好,而误判主库下线的情况,同时多个哨兵的网络同时不稳定的概率较小,由它们一起做决策,误判率也能降低。
4、选定新主库规则
哨兵选择是从多个从库中,先按照一定的筛选条件,把不符合条件的从库去掉,然后再按照一定的规则,给剩下的从库逐个打分,将得分最高的从库选定为主库,如下图所示:
5、筛选条件
检查从库的当前的在线状态,还要判断它之前的网络连接状态。如果从库总是和主库断连,并且断连次数超出了一定的阈值,说明该从库网络状况不是很好,直接筛掉,判断条件如下:
down-after-milliseconds * 10
down-after-milliseconds是指主从库断连的最大连接超时时间。如果down-after-milliseconds毫秒内断连了并且超过10次,则不符合条件。
6、打分规则
-
从库优先级:优先级配置项slave-priority处理
-
从库复制进度-复制进度越接近旧主库得分越高
-
从库Id号-优先级和复制进度都相同情况,id号越小从库得分越高。
7、基于pub/sub机制的哨兵集群组成
哨兵只要和主库建立连接,就可以在主库上发布连接信息(ip和端口)消息,也可以订阅消息,获取其它哨兵发布的连接信息(ip和端口),这样哨兵实例之间可以相互发现,基于redis提供的pub/sub机制,也就是发布/订阅机制。
注意点:只有订阅了同一频道的应用,才能通过发布消息进行信息交换。在主从集群中,该频道为主库的"_sentinel:hello",不同哨兵就是通过它来相互发现,实现相互通信。哨兵消息订阅频道汇总,如下图所示:
8、基于info命令-哨兵与从库建立连接
在哨兵的监控任务中,它需要对主从库都进行心跳判断,而且主从库切换完成后,它还需要通知从库与新主库进行同步。哨兵与从库建立连接流程图如下图所示:
9、基于pub/sub机制的客户端事件通知
客户端读取哨兵的配置文件,可以获得哨兵的IP和端口,并且与哨兵建立网络连接,这样就可以通过客户端执行订阅命令,来获取不同事件的消息。
案例一:订阅所有实例进入客观下线状态的事件(subscribe +odown)。
案例二:订阅所有事件(psubscribe *)。
10、如何确定那个哨兵执行主从切换
任何一个哨兵实例只要自身判断主库"主观下线"后,就会给其它哨兵实例发送is-master-down-by-addr命令。接着,其它哨兵实例根据自身和主库的连接情况,作出是否成功响应,如下图所示:
赞成票数配置由文件中的quorum(法定人数)配置设定,若哨兵最新收到的赞成票数达到甚至大于quorum数时,则标记为客观下线,由该哨兵执行主从切换。
例如:redis1主4从5哨兵,哨兵配置quorum为2,如果3个哨兵故障,当主机宕机时,哨兵能否判断主库"客观下线"?能否自动切换?
经过测试,实际结论如下:
1、哨兵集群可以判定主库"主观下线"。由于quorum = 2 ,所以当一个哨兵判断主库"主观下线"后,询问另外一个哨兵后也会得到同样的结果,2个哨兵都判定"主观下线",达到了quorum的值,因此,哨兵集群可以判定主库为"客观下线"。
2、但哨兵不能完成主从切换,哨兵标记主库"客观下线后",在选举"哨兵领导者"时,一个哨兵必须拿到超过多数的选票(5/2 + 1 = 3)。但目前只有2个哨兵存活,无论怎么投票,一个哨兵最多只能拿到2票,永远无法达到多数选票的结果。
六、切片集群
1、redis升级扩展方案
- 纵向扩展:升级单个redis实例的资源配置,包括增加内存容量,增加磁盘容量,使用更高的CPU配置。
优点:实施起来简单、直接。
缺点:纵向扩展会受到硬件和成本的限制,RDB对数据进行持久化,主线程fork子进程操作随着数据增加,耗时变长。
- 横向扩展:横向增加redis实例的个数
优点:不需要担心单个实例的硬件和成本限制。
2、数据切片和实例的对应分布关系
redis3.0之后,redis cluster方案采用哈希槽(hash slot)来处理数据和实例之间的映射关系。
3、数据映射流程
1、根据键值对的key,按照CRC16算法计算出16bit值。
2、然后这bit值对16384槽位进行取模,得到0 ~ 16383范围的模数对应着槽位。
4、客户端如何确定哈希槽在哪个实例
客户端和集权实例建立连接,实例就会把哈希槽的分配信息发给客户端,客户端收到哈希槽信息,就会缓存起来,这样后续通过计算能找到对应的实例。
redis实例会把自己的哈希槽信息发给和它相连接的其它实例进行哈希槽分配信息的扩散,这样每个实例就有所有的哈希槽的映射关系,后续moved,ask操作需要用到。
5、重定向机制
实例和哈希槽的对应关系并不是一成不变的,最常见的变化有两个:
-
在集群中,实例有新增或删除,redis需要重新分配哈希槽。
-
为了负载均衡,redis需要把哈希槽在所有实例上重新分布一遍。
以上两种情况,会导致客户端缓存的哈希槽信息和实例的哈希槽信息不一致,因为redis cluster方案提供一种重定向机制。
6、重定向机制原理
若客户端给一个实例发送数据读写操作时,这个实例上并没有相应的数据,该实例就会返回moved命令或者ask命令响应结果(包含新实例地址信息)给客户端,这样客户对新实例地址进行访问,ask会在访问前发送asking命令确认。
moved命令表示数据已经迁移到新实例中,如下图所示:
ASK命令表示数据正在迁移到新实例,如下图所示:
ASK命令有两层含义:
1、表明slot数据还在迁移中。
2、ASK命令把客户端所请求数据的最新实例地址返回给客户端,然后客户端给新客户端发送ASKING命令,然后再发送操作命令。
注意点:ASK命令并不会更新客户端缓存的哈希槽分配信息。
七、redis与mysql数据一致性问题
1、先更新数据库,再删除redis
在并发情况下可能出现如下所示情况: 存在问题: 数据库中的数据保持的是T2-2.1修改后的数据,而redis中保存的数据为T3-3.2中在T1-1.1修改数据后的结果,此时出现了redis中数据和数据库数据不一致的情况,在后面的查询过程中就会长时间去先查redis,从而出现查询到的数据并不是数据库中的真实数据。
2、先更新数据库,再更新redis
在并发情况可能出现如下所示情况: 存在问题: T1-1.1修改数据库最终保存到redis中,T2-2.1在T1-1.1之后也会修改了数据库数据,此时出现redis中数据和数据库不一致情况,后面查询会长时间去查询redis,从而出现数据不一致性问题。
3、先删除redis,再更新数据库
在并发情况下可能出现如下所示情况: 存在问题: 与先更新数据库,再删除redis情况类似。
4、先更新redis,再更新数据库
存在问题:redis数据更新成功,而数据库里面的数据更新失败了,从而导致出现数据不一致问题。
5、延迟双删策略
在并发情况下出现的情况如下所示:
双删策略因为存在延时时间,故T1-1.3或T2-2.3一定是最后执行的一步操作,延时的根本目的就是为了让程序先把T3-3.3执行完,再删除redis数据。
缺点:延时时间不好控制
6、基于binlog日志执行删除策略
如下图所示: 引用: