1.redis是一种支持KV等多种数据结构的存储系统,可用于缓存,事件发布和订阅,高速队列等场景,使用C语言编写,支持网络,提供字符串,哈希,列表,队列
集合结构直接存取,基于内存,可持久化。
2.redis支持大部分语言
3.应用场景:会话缓存最常用,消息队列,活动排行榜或计数。发布和订阅消息,商品列表和评论列表
4.redis支持的数据类型:string,hash,list,set,zset
string:最基本的数据类型,一个键对应一个值,一个键最大存储是512M
hash:一个键值对的集合,是一个string类型的field和value的映射表,适用于存储对象
list:简单的字符串列表,按插入顺序排序
set集合:字符串类型的无序集合,不可重复
zset有序集合:字符串类型的有序集合,不可重复,有序集合中的每一个元素都要指定一个分数,根据分数对元素进行升序排序,如果多个元素有相同的分数
则以字典序进行升序排序,因此zset非常适合现实排名
5.redis服务相关的命令:
通常被称为数据结构服务器,因为值value可以使用字符串,哈希,list,set和zset
进入redis:./redis-cli -c -h 10.251.26.107 -p 6380 --raw
redis与Memcached的区别和比较:
1.redis支持的数据类型比Memcached丰富,Memcached值支持string和二进制数据,redis支持string,list,hash,set和zset
2.redis支持数据的持久化,有两种持久化方式。Memcached把数据全部存储在内存中
3.redis支持数据备份,即主从模式的数据备份
4.redis是单线程IO复用模型,Memcached是多线程非阻塞IO复用的网络模型
5.redis的速度比Memcached快
使用redis的string类型做的事,都可以用Memcached去替换,可以提升性能,除此之外,优先考虑redis
redis的支持事务,操作都是原子性,即对数据的更改要么全部成功,要么全部失败
redis常见数据结构的使用场景:
1.String:常用命令:set,get,decr,incr,mget
string类型也就是简单点的KV类型,V可以是字符串,也可以是数字
常用场景:微博数,粉丝数
2.Hash:常用命令: hget,hset,hgetall 等。
Hash是一个string类型的field和V的映射表,非常适合用于存储对象。经常用hash数据结构存储用户信息和商品信息
3.List:常用命令: lpush,rpush,lpop,rpop,lrange等
list就是链表,list的应用场景较多,比如微博关注列表,粉丝列表,最新消息排行等功能
是一个双向链表,可以支持反向查找和遍历,更方便操作,不过带来了额外的内存开销
4.set:常用命令: sadd,spop,smembers,sunion 等
类似list,但是可以去重,并且提供了判断某个成员是否在一个set集合内的重要接口
在微博应用中,可以将一个用户所有的关注人存在一个集合中,所有粉丝在一个集合中,redis可以非常方便的实现共同关注,共同喜好
5.zset:常用命令: zadd,zrange,zrem,zcard等
与set类似,不过比set多一个排序,增加了一个权重参数score,使集合中的元素按score进行有序排序
redis的数据淘汰策略:
redis内存数据集大小上升到一定大小的时候,就会实行数据淘汰策略,也叫回收策略,redis有6中回收策略:
1.volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
2.volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
3.volatile-random:从已设置过期时间的数据集中随机选择数据淘汰
4.allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
5.allkeys-random:从数据集中随机挑选数据进行淘汰
6.no-enviction:禁止驱逐数据
redis并发竞争问题如何解决:
redis是单进程单线程模式,采用队列模式将并发访问变成串行访问,redis本身没有锁的概念,redis对于多个客户端连接并不存在竞争,但是在
jedis客户端对redis进行并发访问时会发生连接超时,数据转换错误,阻塞,客户端关闭连接问题,这些均是由于客户端连接混乱造成,对此有
两种解决方法:
1.客户端角度,为保证每个客户端正常有序的与redis进行通信,对连接进行池化,同时对客户端读写redis操作采用内部锁
2.服务器角度。利用setnx实现锁
redis大量数据插入:
有些时候,redis实例需要装载大量用户在短时间内产生的数据,数以百万级的keys需要被快速的创建
redis如何尽快的处理这些数据呢?
正常模式下,大量数据的插入不是很好,因为一个个插入会有大量的时间浪费在每一个命令往返时间上,使用管道是一个可行的方法,但是在
大量数据插入的时候有需要执行其他命令,读取数据的同时要确保尽可能快的写入数据,
redis的分区:
分区是分割数据到多个redis实例的处理过程,因此每个实例只保存key的一个子集。
优势:利用多台计算机内存的和值,允许构造更大的数据库
通过多核和多台计算机,允许我们扩展计算能力,通过多台计算机和网络适配器,允许扩展网络带宽
不足:涉及到多个key的操作通常是不支持的,举例:当两个set映射到不同的redis实例上时,就不能对这两个set执行交集操作
涉及多个key的redis事务不能使用
使用分区时,数据处理比较复杂,
1.为什么使用redis
项目中使用redis,主要要从两个方面考虑,性能和并发,
1.性能:
一些执行时间特别久,但是执行的结果不会频繁的发生改变的时候,就比较适合将执行的结果放入到缓存中,这样,后面的请求就去缓存中读取,能快速的响应
理想状态下,页面的跳转需要在0.5S以下解决,对于页面里的操作,需要在0.1S以下解决,超过3S的操作则需要有进度提示,并且可以随时中止和取消。这样才能
给用户良好的体验
2.并发:
在大并发的情况下,所有的请求直接访问数据库,数据库可能会出现连接异常,这个时候,需要用redis做一个缓冲,让请求先访问到redis,而不是直接访问到
数据库
2.使用redis的缺点
1.缓存和数据库双写一致性问题
2.缓存雪崩问题
3.缓存击穿问题
4.缓存的并发竞争问题
3.单线程的redis为什么很快
这是redis的一个内部机制,redis是单线程工作模式,快的原因主要是因为,第一是纯内存操作,第二,由于是单线程,避免了频繁的上下文切换,
第三,采用了非阻塞I/O多路复用机制
I/O多路复用:单个线程通过跟踪每个I/O流的状态,来管理多个I/O流,一句话说明,任务放置在一个队列之中,单个线程依次去执行这些任务。
redis客户端在操作的时候,会产生不同事件类型的socket,在服务端,有一段I/O多路复用程序,将其置入队列之中,然后会有文件事件分派器依次去
该队列中取,转发到不同的事件处理器中。
4.redis数据类型和各自使用的场景
1.string:最常规的set和get操作,value可以是数字也可以是string,一般做一些复杂的计数功能的缓存
2.hash:value一般存放结构化的对象,比较方便的就是操作其中的某个字段,笔者在做单点登录的时候,就是用hash来存储用户信息,用cookid作为key,
设置30分钟为缓存过期时间,能模拟出session的效果
3.list:可以用来当做简单的消息队里的功能,还有就是可以利用lrange命令,做基于redis的分页功能,性能很好,用户体验好
4.set:用来全局去重等功能,比起JVM自带的set去重好的一点是,我们的系统一般是集群部署,使用JVM的set比较麻烦,需要再起一个公共的服务。还可以
用set进行交集,并集,差集操作。
5.Sorted set:比set多一个权重参数score,集合中的元素可以按照score进行排列,可以用作排行榜。常见的应用还有用来做延时任务,以及范围查找
5.redis的过期策略和内存淘汰机制
这个问题很重要,可以看出redis有没有用到位,比如,redis只能存5G的数据,但是写了8G,那么会删除3G的数据,删除的方式是什么?还有,数据已经设置了
过期时间,但是时间到了,内存占用率还是比较高,为什么?
redis采用的是定期删除和惰性删除策略
为什么不用定时删除策略呢?
定时删除要有一个定时器来负责监控key,过期则自动删除,虽然内存及时释放,但是十分消耗CPU资源,在大并发请求下,CPU要将时间应用在处理请求
而不是删除key,因此没有采用这一策略
定期删除和惰性删除是如何工作的呢,定期删除,redis默认每个100MS随机抽取一部分key检查是否过期,有过期的就删除。惰性删除,在获取某个key的
时候,redis会检查一下,这个key是否设置了过期时间,如果设置了过期时间,现在是否过期,如果过期,就删除
定期删除和惰性删除的综合使用还是有问题的,在定期删除没删除key,并且也没有及时的请求key,也就是说惰性删除也没有生效,那么key就会一直存在,
这样的key多了,redis的内存也就是会越来越高,高到一定程度吗,就会启动内存淘汰机制。在redis的配置文件中有这一项:maxmemory-policy volatile-lru
该配置就是配置了内存淘汰的几种机制:
1.Noeviction:当内存较高,不能容纳新写入的数据时,新的写操作会报错,一般不用
2.Allkeys-lru:当内存不足以写入新数据的时候,在键空间中,移出最近最少使用的key,推荐使用
3.Allkeys-random:在键空间中,随机移出key,一般不用
4.Volatile-lru:内存很高时,在设置了过期时间的键空间中,移出最近最少使用的key,当redis又当缓存又当持久化存储的时候才用
5.Volatile-random:在设置了过期时间的键空间中,随机移出key,不推荐
6.Volatile-ttl:在设置了过期时间的键空间中,有更早过期时间的key优先删除。
如果key都没有设置过期时间,不满足先决条件,那么Volatile等策略的行为和Noeviction(不删除)基本一致
6.redis和数据库双写一致性问题
一致性问题是分布式常见的问题,可分为强一致性和最终一致性,数据库和缓存双写,就必然会存在不一致的问题。如果对数据有强一致性的要求,就不能放入缓存,
redis所能做到的,只能是最终一致性。
另外,对数据一致性的优化,从根本上来说,只能降低数据不一致发生的概率,无法完全避免,因此有强一致性要求的数据不能放缓存
采取正确的更新策略,先更新数据库,再更新缓存,其次,因此可能存在删除缓存时失败的问题,提供一个补偿措施,比如利用消息队列
7.应对缓存穿透和缓存雪崩问题
对于大的并发项目,才需要考虑的问题,流量要有几百万
1.应对缓存穿透
即黑客故意去请求缓存中不存在的数据,导致所有的请求都需要访问数据库,从而导致数据库压力过大
解决方案:
1.利用互斥锁,缓存失效的时候,先去获得锁,得到锁了再去请求数据库,没得到锁,则休眠一段时间重试
2.采用异步更新策略,无论key是否取到值,都直接返回,value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程读取数据库,更新缓存。
3.提供一个能迅速判断请求是否有效的拦截机制,比如利用布隆过滤器,内部维护一系列合法有效的key,迅速做出判断,不合法的请求,直接返回
2.应对缓存雪崩
即缓存同一时间大面积失效,这个时候又来了一波请求,这些请求只好去访问数据库,导致数据库压力激增
解决方案:
1.给缓存的失效时间加上一个随机值,避免集中失效
2.使用双缓存,一个缓存有失效时间,一个没有失效时间。
8.如何解决redis并发竞争key的问题
对于多个子系统同时去set一个key。
1.如果对该key操作不要求顺序,可以准备一个分布式锁,多个子系统去抢锁,抢到锁就做set操作
2.如果对该key操作要求顺序,每当一个系统抢到锁,更改了key值后,写入数据库时增加一个时间戳,后续的更改只能比该时间大。
redis的内存模型和应用解读:
了解redis的内存模型,对redis的使用会有很大的帮助,例如:
估算redis内存使用量,根据需求合理的估算redis的内存使用量,选择合适的机器配置,可以在满足需求的情况下节约成本。
优化内存占用,合理的选择适合的数据类型和编码,更好的利用redis内存
当redis出现阻塞,内存占用等问题,尽快的发现问题的原因,便于分析解决问题
1.redis内存统计
进入redis集群后,通过info memory命令查看redis服务器的内存信息
used_memory:20836976 redis分配的内存总量。包含了redis进程所占内存和数据占用内存。字节为单位
used_memory_human:19.87M 上述项的直观体现
used_memory_rss:47058944 向操作系统申请的内存大小,字节为单位
used_memory_rss_human:44.88M 上述项的直观体现
used_memory_peak:42442328 redis的内存消耗的峰值,字节为单位
used_memory_peak_human:40.48M 上述项的直观体现
used_memory_peak_perc:49.09% 使用内存达到峰值内存的百分比, used_memory/used_memory_peak
used_memory_overhead:17011776 维护数据集的内部机制所需的内存开销,字节为单位
used_memory_startup:1461128 redis启动时消耗的内存
used_memory_dataset:3825200 数据占用内存的大小
used_memory_dataset_perc:19.74% 数据占用的内存大小的百分比, used_memory_dataset/(used_memory-used_memory_startup)
total_system_memory:135015960576 整个系统的内存
total_system_memory_human:125.74G 上述项的直观体现
used_memory_lua:37888 lua脚本存储占用的内存
used_memory_lua_human:37.00K 上述项的直观体现
maxmemory:48318382080 redis实例的最大内存配置
maxmemory_human:45.00G 上述项的直观体现
maxmemory_policy:noeviction 内存达到最大内存配置时的淘汰策略
mem_fragmentation_ratio:2.26 内存碎片率 used_memory_rss/used_memory
mem_allocator:jemalloc-4.0.3 内存分配器
active_defrag_running:0 0表示没有活动的defrag任务正在运行,1表示有活动的defrag任务正在运行。defrag:表示内存碎片整理
lazyfree_pending_objects:0 0表示不存在延迟释放的挂起现象
2.redis的内存划分
redis作为内存数据库,在内存中存储的内容主要是数据,但是除了数据外,其他部分也会占用内存
redis内存占用主要可以划分几个部分
1.数据,会统计到used_memory中
2.进程本身运行需要的内存,如代码,常量池,这部分不会统计在used_memory中
3.缓冲内存,包括客户端缓冲区,AOF缓冲区:用于在进程AOF重写时,保存最近写入的命令,这些内存会统计到used_memory中
4.内存碎片,内存碎片不会统计到used_memory中,内存碎片过高可以通过安全重启redis解决,因为重启之后,redis会重新从备份文件中读取数据,在内存中
进行重排,为每个数据重新选择合适的内存单元,减小内存碎片
3.redis数据存储的细节
4.redis的对象类型和内部编码
5.应用场景:
1.估算redis内存使用量:
以最简单的string类型说明:
假设有90000个键值对,每个key的长度是7个字节,每个value的长度也是7个字节
9万个键值对占据的内存空间主要可分为两个部分,一部分是9万个dictEntry占据的空间,一个是9万个键值对所需的空间
每个dictEntry占据的空间包括:
1.一个dictEntry,24字节,jemalloc会分配32字节的内存块
2.key,7个字节
3.redisObject 16字节
4.value,7字节
bucket空间,bucket数组的大小为大于90000的最小的2的N次方,是131072
所以90000个键值对所占内存,估算为900000*80+131072*8=8248576 约8M
2.优化内存占用
利用jemalloc特性进行优化,
1.由于jemalloc分配内存时数值是不连续的,因此key和value字符串变化一个字节,可能会引起占用内存很大的变动,设计时可利用这一点
2.使用整型和长整型,如果是整型和长整型,redis会使用int类型存储来代替字符串。节省空间
3.共享对象:减少了对象的创建,节省空间,目前redis的共享对象只包括10000个整数,即0--9999,可以通过调整REDIS_SHARED_INTEGERS参数提高
共享对象的个数,例如提高到20000,则0--19999之间的对象都可以共享
4.避免过度设计
3.关注内存碎片率
内存碎片一般大于1,1.5以上说明内存碎片过高,内存浪费严重,可以对redis进行重启,减少碎片
如果内存碎片小于1,说明redis内存不足,部分数据使用了虚拟内存,由于虚拟内存比物理内存的存取速度差很多,所以当碎片率小于1时,redis的
访问速度可能变的很慢,必须设法增大物理内存。或减少redis中的数据。注意设置合理的数据回收策略。
redis高可用详解:持久化技术和方案选择
1.持久化
2.复制以及读写分离
3.哨兵
4.集群
1.redis高可用的概述:
在web服务器中,高可用指服务器可以正常访问的时间,衡量的标准是在多长时间内可以提供正常的服务,但是在redis中,高可用的含义要宽泛一些,
除了保证提供正常的服务,如主从分离和快速容灾技术等,还需要考虑数据容量的扩展,数据安全不会丢失等。
在redis中,实现高可用的技术主要包括持久化,复制,哨兵,和集群。
1.持久化:主要是数据的备份,将数据备份到磁盘中,保证数据不会因为进程退出而丢失
2.复制:复制是redis高可用的基础,哨兵和集群都是在复制基础上实现高可用的,复制主要实现了数据的多机备份以及对读操作的负载均衡和简单的故障恢复
缺陷是故障恢复无法自动化,写操作无法负载均衡,存储能力受到单机的限制
3.哨兵:在复制的基础上,哨兵实现了自动化的故障恢复,缺陷是写操作无法负载均衡,存储能力受到单机限制。
4.集群:通过集群,redis解决了写操作无法负载均衡以及存储能力受到单机限制的问题。实现了较为完善的高可用方案
2.redis持久化概述
持久化功能:redis是内存数据库,数据都存储在内存中,为了避免进程退出导致数据的永久丢失,需要定期将redis中的数据以某种形式(数据或命令)从内存
保存到磁盘,当下次redis重启时,利用持久化文件进行数据的恢复,除此之外,为了进行灾难备份,可以将持久化文件拷贝到一个远程位置
redis的持久化方式有两种,一种是RDB,一种是AOF。
RDB:将当前进程中的数据生成快照保存在磁盘,保存的文件后缀是rdb,当redis重启时,可以读取快照文件恢复数据
触发条件有两种,一种是手动触发,一种是自动触发。
手动触发:进入集群,输出save和bgsave命令都可以进行生成rdb文件。区别是save会阻塞redis服务器进程,在rdb文件创建
完毕之前,redis服务器不能处理任何请求,bgsave会创建一个子线程负责创建rdb文件,父进程也就是redis主
进程不会阻塞,会继续处理请求。所以一般使用bgsave命令进行持久化。
自动触发:在配置文件中设置 save m n,指定当m秒内发生n次变化时,会触发bgsave。例如 save 60 10。60s内redis数据
发生了至少10次变化,那么就进程bgsave。可配置多个,放置在多行,只要满足一个条件都会触发
其他自动触发的条件:在主从复制场景下,如果从节点执行全量复制操作,则主节点会执行bgsave命令,并将rdb文件
发送给从节点
执行shutdown命令时,自动执行rdb持久化。
bgsave的执行流程:
当执行save命令时,父进程也就是redis的主进程执行fork操作,创建子进程,这个过程是阻塞的,redis这个操作内不能执行
来自客户端的任何命令,但是这个时间是短暂的。子进程创建rdb文件,根据父进程的内存快照生成临时快照文件,完成后对原
有文件进行原子替换。子进程生成rdb文件后,发送信号给父进程表示完成。整个过程中若有其他save命令要执行磁盘的写操作,
父进程是不允许的,会直接将命令返回,因为同时进行两个save命令对磁盘进行写操作,严重影响磁盘性能。
rdb文件是经过压缩的二进制文件
rdb文件的存储路径可以在启动前配置,也可以通过命令动态设定
命令:config set dir {newdir} 设定存储路径
config set dbfilename {newFileName} 设定指定文件名
rdb文件默认采用LZF算法对RDB文件进行压缩,虽然压缩耗时,但是大大减少RDB文件的体积,因此压缩是默认开启的。想要
关闭的话,可以通过命令关闭:config set rdbcompression no
需要注意的是:rdb文件的压缩并不是针对整个文件,而是对数据库中的字符串进行的,只有字符串达到一定长度才会压缩。
启动时的加载:rdb文件的载入是在服务器启动时自动执行的,没有专门的命令,但是由于AOF的优先级更高,因此当AOF开启时,
redis会优先载入AOF文件来恢复数据,只有有AOF关闭时,redis服务器启动时,才会检测RDB文件,并自动载入,
服务器载入RDB文件期间处于阻塞状态,直到载入完成。redis载入RDB文件时,会对RDB文件进行校验,如果文件
损坏,则会启动失败
RDB常用配置总结:
save m n:bgsave自动触发的条件,可配置多个
stop-writes-on-bgsave-error yes:当bgsave出现错误时,redis是否停止执行写命令,默认yes,当出现错误,不继续写
及时发现错误,可以避免数据大量的丢失,如果设置成no,则无视错误,继续写
rdbcompression yes:是否开启RDB文件压缩。
rdbchecksum yes:是否开启RDB文件的校验,在写入文件和读取文件时都起作用
dbfilename dump.rdb:RDB文件名。
dir ./:RDB文件和AOF文件所在目录。
AOF持久化:AOF持久化是将redis执行的每次写命令记录到单独的日志文件中,当redis重启时,执行AOF文件中的命令来恢复数据
与RDB相比,AOF的实时性更好,目前是主流的持久化方案
1.开启AOF,redis服务器默认是开启RDB,关闭AOF,要开启AOF,需要在配置文件中配置:
appendonly yes
2.AOF的执行流程
1.将redis的写命令追加到缓冲区aof_buf中
2.文件写入和文件同步:根据不同的同步策略将aof_buf中的内容同步到硬盘
3.文件重写:定期重写AOF文件,达到压缩的目的
1.redis先将写命令追加到缓冲区,而不是直接写入文件,主要是为了避免每次写命令都写入磁盘,导致硬盘IO成为redis的负载瓶颈
2.AOF缓冲区的同步文件策略由参数appendfsync控制,有三种配置
1.always:命令写入缓冲区aof_buf中后,立即同步到AOF文件,这样造成磁盘的IO压力大,降低了redis的性能
2.no:对缓冲器aof_buf的写入命令写入到AOF文件由系统决定,通常为30S一次,这样做的弊端是缓冲器可能堆积太多数据
导致redis出现故障,可能会丢失大量的数据
3.everysec:对aof_buf缓冲区内的写入命令每秒调用一次写入到AOF文件中去,是前两种策略的折中方案,达到数据安全和
性能的兼顾。是redis的默认配置,也是推荐配置
3.文件重写:随着redis的运行,AOF文件中的写入命令越来越多,文件越来越大,过大的AOF文件不仅会影响服务器正常的运行,
也造成了数据恢复时间变的很长,因为要从头开始一个个的运行这些命令
文件重写是指定期重写AOF文件,减小AOF文件的体积,需要注意的是,AOF重写是把redis进程内的数据转换成写命令,
同步到新的AOF文件,不会对旧的AOF文件进行任何读取和写入操作。
文件重写虽然是推荐的,但不是必须的,即使没有文件重写,数据也是可以被持久化,并在redis启动的时候导入,因此
有的时候会把redis重写机制关闭,然后通过定时任务每天的某一时刻定时执行重写
文件重写之所以能够压缩AOF文件,原因在于,过期的数据不再写入文件
无效的命令不再写入文件中。如有些数据被重复的设置值,有些数据被删除了,多条命令可以合并成一条命令,
为了防止单条命令过大,造成客户端缓冲区溢出,并不一定只使用一条命令,而是将命令拆分成多条。
重写后AOF执行的命令减少了,所以持久化数据恢复的速度也变快了,同时减少了AOF文件占用的空间。
文件重写的触发条件:分为手动触发和自动触发
手动触发:直接调用bgrewriteaof命令,该命令的执行与bgsave类型,都是fork进程进行具体工作,只有在fork时阻塞
自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定
auto-aof-rewrite-min-size:默认是64MB,AOF文件达到这一大小时
auto-aof-rewrite-percentage:当前AOF文件大小(aof_current_size)和
上一次重写时AOF文件大小(aof_base_size)的比值。
比值的两项可以通过info persistence命令查看。其他配置项通过config get命令查看
只有两个条件同时满足后,才会触发AOF重写操作。
文件重写的流程:
注意:重写由父进程fork子进程进行,重写期间redis执行的写命令,需要加入到新的AOF文件中,为此redis引入了
aof_rewrite_buf缓存
bgsave和bgrewriteaof命令有且只能存在一个。不能同时存在两个命令,或两个相同的命令,在执行命令之前都会
检测集群目前是否正在运行有这些命令,如果有,则返回。等待执行完成。
流程:父进程执行fork操作创建子进程,这个过程父进程是阻塞的,当fork成功后,不再阻塞,可以响应其他命令,
redis的所有写命令依然写入AOF缓冲区,并根据appendfsync策略同步到硬盘,保证原有AOF机制的正确。
在重写执行期间,父进程依然在响应命令,这时的写命令是同时追加到aof_buf和aof_rewirte_buf两个缓冲区
内的。
子进程根据内存快照,按照命令合并规则写入到新的AOF文件中,子进程写完新的AOF文件后,向父进程发信号,
父进程更新统计信息,通过info persistence可以查看。
父进程把AOF重写缓冲区(aof_rewirte_buf)的数据写入到新的AOF文件中,保证了新的AOF文件所保存的数据库
状态和服务器当前状态一致。
使用新的AOF文件替换老的文件,完成重写。
启动时加载,当AOF开启时,redis会优先载入AOF文件来恢复数据,只有在AOF关闭时,才会载入RDB文件恢复数据
当AOF开启,但是AOF文件不存在时,即使RDB文件存在也不会加载,至少是3.0版本不会,早一点的版本可能会
载入AOF文件时,会对AOF文件进行校验,如果损坏,redis会启动失败,日志打印错误信息,但是如果是AOF文件结尾
不完整,并且aof-load-truncated参数使开启的,则日志会给出告警,redis会忽略尾部文件的缺失,成功启动。
重写的这一操作,redis会启动一个没有网络连接的伪客户端执行。
AOF常用配置总结:
appendonly no:是否开启AOF
appendfilename "appendonly.aof":AOF文件名
dir ./:RDB文件和AOF文件所在目录
appendfsync everysec:fsync持久化策略 no always everysec
auto-aof-rewrite-percentage 100:文件重写触发条件之一
auto-aof-rewrite-min-size 64mb:文件重写触发提交之一
aof-load-truncated yes:如果AOF文件结尾损坏,Redis启动时是否仍载入AOF文件
no-appendfsync-on-rewrite no:AOF重写期间是否禁止fsync,如果开启(yes),可能会丢失重写期间的数据,但是减轻硬盘和CPU的负载
方案的选择和常见问题:
RDB和AOF的优缺点:
RDB优点:RDB文件小,网络传输快,适合全量复制,恢复速度比AOF快,RDB方式对性能的影响很小。
RDB缺点:RDB持久化方式是数据快照的持久化方式,这种方式必然做不到实时的持久化。集群如果宕机,没有持久化的数据全部丢失。
AOF优点:支持秒级的持久化,可以保证不丢失数据,或者只丢失一秒钟的数据。
AOF缺点:AOF文件大,恢复速度慢,对redis性能影响比RDB方式大
持久化方式的选择,根据应用对数据安全,数据量的不同要求选择不同的持久化方式。
可以不适用持久化方式,可以选择两种都使用,或者使用一种。此外,持久化的选择必须与redis的主从策略一起考虑,因为主从复制与
持久化同样具有数据备份的功能,并且主机master和从机slave可以独立的选择持久化方案。
多数情况下,都会选择主从配置,slave的存在既可以实现数据的热备,也可以进行读写分离分担redis的读请求,以及在master宕掉后
继续提供服务。在这种的情况下,一种可行的做法是:
master:完全关闭持久化,这样可以让master的性能保证到最好,
slave:关闭RDB,开启AOF,如果对数据的安全要求不高,开启RDB,关闭AOF也可以。并定时对持久化文件进行备份,如果备份到其他文件夹,
标记好备份的时间,然后关闭AOF的自动重写,添加定时任务,每天redis的闲时调用重写。
为什么开启了主从复制,可以实现数据的热备份,还需要设置持久化呢,因为在一些特殊情况下,主从复制依然不足以保证数据的安全:
master和slave进程同时停止。redis进程停止,如果没有持久化,则数据会完全丢失。
fork阻塞:CPU阻塞
在redis的实践中,众多因素限制了redis单机的内存不能过大。
fork操作:
父进程通过fork操作可以创建子进程,创建后,父子进程共享代码段,不共享进程的数据空间,但是子进程会获得父进程
的数据空间的副本,在操作系统fork的实际实现中,基本都采用了写时复制技术,也就是在父子进程试图修改数据空间之前,
父子进程实际上共享数据空间,但是当父子进程的任何一个试图修改数据空间时,操作系统会为修改的那一部分(内存的一页)
制作一个副本
虽然fork时,子进程不会复制父进程的数据空间,但是会复制内存页表,页表相当于内存的索引,目录。父进程的数据空间
越大,内存页表越大,fork时复制耗时也会越多。
在redis中,无论是RDB持久化的bgsave,还是AOF的重写bgrewriteaof,都需要fork出子进程来进行操作,如果redis内存过大,
会导致fork操作时复制内存页表耗时过多,而redis主进程在进行fork时,是完全阻塞的,也就意味着无法响应客户端的请求,
造成请求延迟过大。
如果AOF缓冲区的文件同步策略是每秒一次:在主线程中,命令写入缓冲区aof_buf后调用系统write操作,write完成后主线程返回,
fsync同步文件操作由专门的文件同步线程每秒调用一次。但是如果,硬盘负载过高,那么写入磁盘的操作可能超过一秒钟,这时,
如果redis主线程持续高速向aof_buf缓冲区写入命令,硬盘的负载就会越来越大,IO资源消耗的很快,在此时如果redis进程异常
退出,丢失的数据也会越来越多,可能远超1S。为此,redis的处理策略是这样的,主线程每次进行AOF会对比上次写入磁盘成功的
时间,如果距离上次不到2S,主线程直接返回,如果超过2S,则主线程阻塞直到同步完成,因此,如果系统硬盘负载过大导致
同步速度太慢,会导致redis主线程阻塞,此外,使用everysec,最多丢失2S的数据,而不是1S。
info命令与持久化:
info Persistence命令:
能查看到的比较重要的信息:
1.rdb_last_bgsave_status:上次bgsave 执行结果,可以用于发现bgsave错误
2.rdb_last_bgsave_time_sec:上次bgsave执行时间(单位是s),可以用于发现bgsave是否耗时过长
3.aof_enabled:AOF是否开启
4.aof_last_rewrite_time_sec: 上次文件重写执行时间(单位是s),可以用于发现文件重写是否耗时过长
5.aof_last_bgrewrite_status: 上次bgrewrite执行结果,可以用于发现bgrewrite错误
6.aof_buffer_length和aof_rewrite_buffer_length:aof缓存区大小和aof重写缓冲区大小
7.aof_delayed_fsync:AOF追加阻塞情况的统计
info stats命令;
其中与持久化关系比较大的是:latest_fork_usec,代表上次fork耗时
redis高可用技术方案总结:
1.高可用的常见使用方式
1.redis单副本
没有备用节点实时同步数据,不提供数据持久化和备份策略,适用于数据可靠性要求不高的纯缓存业务。
优点:结构简单,部署方便。性能较高。
缺点:只有一个实例提供服务,数据的可靠性低。高性能受限于单核CPU的处理能力。
2.redis多副本(主从)
采用主从部署结构,相对于单副本而言最大的特点就是主从实例间数据实时同步,并且提供数据持久化和备份策略,主从实例部署在不同的
物理服务器上,可以实现同时对外提供服务和读写分离策略
优点:可靠性高,采用双机主备架构,实现故障时主备的切换,保证服务稳定运行。数据可持久化,安全性更高。实现读写分离。分担主的读压力
缺点:故障恢复复杂,如果没有redisHA系统(需要开发),当主库节点出现故障时,需要手动将从库晋升为主库,同时需要通知业务方更变配置,
整个过程需要人为干预,比较麻烦
3.哨兵机制
redis的原生高可用解决方案,部署架构主要包括两个部分,redis数据集群和redis 哨兵集群,其中redis哨兵集群若干个哨兵节点组成分布式集群,
可以实现故障发现,故障自动转移,配置中心和客户端通知。哨兵节点数量要是大于1的奇数。
优点:redis 哨兵集群部署简单。能够解决redis主从模式下的高可用切换问题,方便实现redis数据节点的线性扩展,满足redis大容量和高性能业务
需求。还可以用一套哨兵集群监控多套数据节点集群
缺点:部署相对redis主从模式要复杂一些,原理理解更加繁琐。资源浪费,redis数据节点中slave节点作为备份节点不提供服务。不能读写分离问题
4.redis集群
redis集群主要解决redis分布式方面的需求,比如,当遇到单机内存,并发和流量等瓶颈的时候,redis集群能起到很好的负载均衡的目的
redis集群最小配置是6个节点。三主三从,其中主节点提供读写操作,从节点作为备用节点,不提供请求,只做故障转移使用。
redis集群采用虚拟槽分区,所有的键根据哈希函数映射到0-16383个整数槽内,每个节点负责维护一部分槽以及槽所映射的键值数据。
优点:无中心架构,数据按照槽存储分布在多个节点,节点间数据共享,可动态调整数据分布。可线性扩展到1000多个节点,节点可动态添加和删除
部分节点不可用时,集群仍可用,通过增加slave做standy数据副本,能够实现故障自动转移,节点之间通过协议交换状态信息,用投票机制
完成slave到master的角色提升。系统扩展性好,也易于维护。
缺点:单机模式下的redis支持16个数据库,集群模式下只能使用一个数据,即DB0。多个业务使用同一套集群时,无法根据统计区分冷热数据,
资源隔离性差。slave在集群中充当冷备,不能缓解读压力。
redis的主从复制:
redis的高可用方案包括了持久化,主从复制及读写分离,哨兵和集群。其中持久化侧重解决的是redis数据单机备份问题,从内存到硬盘的备份,而主从复制
则侧重解决数据的多机热备。此外,主从复制还可以实现负载均衡和故障恢复。
1.主从复制概述:
1.主从复制是指将一台redis服务器的数据,复制到其他的redis服务器。前者成为主节点,后者成为从节点,数据的复制是单向的,只能由主到从
默认情况下,每台redis服务器都是主节点,且一个主节点可以有多个或者没有从节点,但是一个从节点只能有一个主节点。
2.主从复制的作用:
减轻数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式
故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复。
负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供些服务,由从节点提供读服务,分担服务器负载。提高redis服务的并发。
高可用的基石:主从复制是哨兵和集群实现的基础,因此redis主从复制是redis高可用的基础。
2.如何使用主从复制:
1.建立复制
需要注意,主从复制的开启,完全是在从节点发起的,不需要主节点做任何事情
从节点开启主从复制,有三种方式:
1.配置文件:在从服务器的配置文件中加入:slaveof<masterip><masterport>
2.启动命令:redis-server启动命令后加入:slaveof<masterip><masterport>
3.客户端命令:Redis服务器启动后直接通过客户端执行命令:slaveof<masterip><masterport>,则该Redis实例成为从节点。
上述三种方式是等效的。
2.断开复制
通过上述第三中命令建立起redis的主从复制之后,可以通过slaveof no one断开。需要注意的是,从节点断开与主节点的复制后,不会删除
已有的数据,只是不再接受主节点新的数据变化。断开后,从节点又会变成主节点。
3.主从复制的原理:
主从复制过程大体可以分为3个阶段:连接建立阶段,数据同步阶段,命令传播阶段
1.连接建立阶段:
该阶段的主要作用是在主从节点之间建立连接,为数据同步做好准备
步骤1:保存主节点信息
从节点服务器内部维护了两个字段,即masterhost和masterport字段。用于存储主节点ip和port信息
需要注意的是,slaveof是异步命令,从节点完成主节点ip和port的保存后,向发送slaveof命令的客户端直接返回OK,实际的
复制操作在这之后才开始进行。
步骤2:建立socket连接
从节点每秒调用复制定时函数,如果发现了有主节点可以连接,便会根据主节点的ip和port。创建socket连接。如果连接成功,
从节点为该socket建议一个专门处理复制工作的文件事件处理器,复制后续的复制工作,比如接收RDB文件和接收命令传播等。
主节点接收到从节点的socket连接后,即accept。为该socket创建相应的客户端状态,并将从节点看做是一个连接主节点的客户端,
后面的步骤会以从节点向主节点发送命令请求的方式来进行。
步骤3:发送ping命令
从节点成为主节点的客户端后,发送ping命令进行首次请求,目的是检查socket连接是否可用以及主节点当前是否能够处理请求。
从节点发送ping命令后,可能出现3种情况:
1.返回pong。说明socket正常,主节点可以处理请求,复制过程继续
2.超时:一定时间后,从节点仍未收到主节点的回复,说明socket连接不可用,则从节点断开socket连接,并重连
3.返回pong以外的结果:如果主节点返回其他结果,如正在处理超时运行的脚本,说明主节点当前无法处理命令,从节点断开连接,重试。
步骤4:如果从节点中设置了masterauth选项,则从节点需要向主节点进行身份验证,如果没有设置该选项则不需要验证,从节点进行身份验证
是通过向主节点发送auth命令进行的,auth命令的参数即使配置文件中masterauth的值。如果主从节点设置的密码都是一样的,则
身份验证通过,复制可以继续.
2.数据同步阶段
主从节点之间的连接建立以后,就可以进行数据同步了,该阶段可以理解为从节点数据的初始化,具体的执行方式是,从节点向主节点发送psync
命令,开始同步。
数据同步阶段是主从复制的核心阶段,根据主从节点当前状态的不同,可以分为全量复制和部分复制。
在数据同步阶段之前,从节点是主节点的客户端,主节点不是从节点的客户端,但是在数据同步阶段,主从节点互为客户端,原因在于:
在数据同步之前。主节点只需要响应从节点的请求即可,不需要主动发送请求,而在数据同步阶段和后面的命令传播阶段,主节点需要
主动向从节点发送请求,比如推送缓冲区中的写命令,才能完成复制。
3.命令传播阶段
数据同步完成后,主从节点进入命令传播阶段,在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证
主从节点数据的一致性。在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:ping和perlconfack。心跳机制的原理涉及到
“部分复制”。
需要注意的是,命令传播是异步的过程,即主节点发送写的命令后并不会等待从节点的回复,因此实际上主从节点之间很难保持实时的
一致性,延迟在所难免,与主从节点之间的网络状态,写命令的频率,以及主节点中的repl-disable-tcp-nodelay配置有关。
repl-disable-tcp-nodelay:该配置作用于命令传播阶段,控制主节点是否禁止与从节点的TCP_NODELAY。默认是no,即不禁止TCP_NODELAY。
当设置成yes时,TCP会对包进行合并从而减少带宽,但是发送的频率会低,从节点数据延迟增加,一致性
变差。具体发送频率与linux内核的配置有关,默认是40MS,当设置成no的时候,TCP会立马将主节点的数据
发送给从节点,带宽增加但延迟变小。一般说,只有当应用对redis数据不一致的容忍度较高,且主从节点
之间的网络状态不好时,才会设置为yes,多数情况下,默认no。
全量复制和部分复制:
全量复制:用于初次复制和其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个重型的操作
部分复制:用于网络中断后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效,
需要注意的是,如果网络中断时间过长,造成主节点没有能够完整的保存中断期间执行的写命令,那么还是使用全量复制
1.全量复制:
redis通过psync命令进行全量复制的过程如下:
1.从节点判断无法进行部分复制,向主节点发送全量复制的请求,
2.主节点收到全量复制的请求后,执行bgsave命令,在后台生成RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令
3.主节点执行完bgsave后,将RDB文件发送给从节点,从节点先清理自己的旧数据,然后载入新的RDB文件,将数据库状态更新
至主节点执行bgsave时的数据库状态。
4.主节点将第二步中缓冲区里所有的命令发送给从节点,从节点执行这些写命令,将数据库状态更新至最新
5.如果从节点开启了AOF,则会触发bgrewriteaof的执行,从而保证AOF文件更新至主节点的最新状态。
有几点需要注意:
1.从节点在载入主节点数据之前要先将老数据清除
2.从节点在同步数据后,调用了bgrewriteaof
全量复制是非常重型的操作:
1.主节点通过bgsave命令fork进程进行RDB持久化,该过程非常损耗CPU,内存,和硬盘IO。
2.主节点通过网络将RDB文件发送给从节点,对主从节点的带宽都会带来很大的消耗
3.从节点清除老数据和载入新数据都是阻塞的,无法响应客户端的命令。
2.部分复制:
由于全量复制在主节点数据量较大时效率太低,因此有部分复制用于处理网络中断时的数据同步。
部分复制的实现,依赖于三个重要的概念:
1.复制偏移量
主节点和从节点分别维护一个复制偏移量,代表的是主节点向从节点传递的字节数,主节点向从节点每次发送N个字节
数据的时候,主节点的offset增加N,从节点每次收到主节点发送的N个字节数的时候,从节点的offset增加N
偏移量用来确定主从节点的数据库状态是否一致,如果主从偏移量相同,则一致,如果不同,则不一致。也可以用它们的
差值,计算出从节点缺少的部分数据。
2.复制积压缓冲区
复制积压缓冲区是由主节点维护的,固定长度的,先进先出队列,默认是1MB,当主节点开始有从节点时创建,其作用是
备份最近发送给从节点的数据,无论主节点有多少个从节点,缓冲区只有一个
在命令传播阶段,主节点除了将写命令发送给从节点,还会发送给缓冲区一份,用作备份,除了存储写命令,缓冲区还
存储了其中每个字节对应的偏移量,由于缓冲区定长且先进先出,所以它保存的是主节点最近执行的写命令,时间较早
的写命令会被挤出缓冲区
repl-backlog-size:为了保证大多数断线情况都使用,可以将该值调高
从节点将偏移量发送给主节点后,主节点根据偏移量和缓冲区大小决定是否执行部分复制。
3.服务器运行ID(runid)
每个redis节点,在每次启动后都会生成一个随机的ID,由40个随机的十六进制字符组成,是runid,runid用来标识一个
redis节点,通过info server命令可以查看。
主从节点初次复制时,主节点将自己的runid发送给从节点,从节点会保存起来,当断线重连时,从节点会将这个runid
发送给主节点,主节点根据runid判断能否进行部分复制。
如果这个runid和主节点现在的runid相同。主节点会尝试部分复制
如果不同,则会全量复制。
3.psync命令的执行
在了解了复制偏移量,复制积压缓冲区,节点运行id之后,现在介绍psync命令的参数和返回值,从而说明psync命令执行过程中,
主从节点是如何确定使用全量复制还是部分复制的。
psync命令的执行过程:
1.如果从节点之前未执行过slaveof或最近执行了slaveof no one,则从节点发送命令为psync ? -1,向主节点请求全量
复制,如果从节点之前执行了slaveof,则发送命令为psync<runid><offset>,其中runid为上次复制的主节点的
runid,offset为上次复制截止时从节点保存的复制偏移量
2.主节点根据收到的psync命令以及当前服务器状态来决定是进行全量复制还是部分复制
如果runid相同,且从节点的offset之后的数据都还在缓冲区,则回复+CONTINUE,表示将进行部分复制,从节点
等待主节点发送数据即可。如果runid不一样,或者缓冲区里的数据不全,则回复FULLRESYNC<runid><offset>,
表示进行全量复制。
4.部分复制的演示
演示:网络中断几分钟后恢复,断开连接的主从节点进行了部分复制
网络中断:
网络中断一段时间后,主从节点都发现与对方失去了联系,从节点会执行对主节点的重连,并一直尝试
网络恢复:
网络恢复后,主从连接成功,并请求进行部分复制,主节点接收请求后,进行部分复制。
5.命令传播阶段的心跳机制
在命令传播阶段,处理发送写请求,主从还保持着心跳机制。PING和REPLCONF ACK。心跳机制对于主从复制的超时判断,数据安全有作用。
主连接从:ping
每隔指定时间,主节点会向从节点发送ping命令,这个ping命令的作用,主要是为了让从节点进行超时判断,ping发送的频率由
repl-ping-slave-period参数控制,单位是秒,默认是10S
从连接主:REPLCONF ACK
在命令传播阶段,从节点会向主节点发送REPLCONF ACK命令,频率是每秒一次,命令格式是REPLCONF ACK{offset},其中offset
指从节点保存的复制偏移量。REPLCONF ACK命令的作用包括:
1.实时监测主从节点网络状态:
2.检测命令丢失:从节点发送自身的offset,主节点会进行对比,如果从节点数据缺失,主节点会推送缺失的数据,注意,
offset和复制积压缓冲区不仅可以用于部分复制,还可以用于处理命令丢失的情景,区别在于,部分复制是在断线后
重连进行的,命令丢失的情景是主从节点没有断线的情况下进行的。
3.辅助保证从节点的数量和延迟
redis主节点中使用min-slaves-to-write和min-slaves-max-lag参数,来保证主节点在不安全的情况下不会执行写命令,
所谓不安全,是指从节点数量太少,或者延迟过高,例如这两个参数的值分别是3和10,那么含义就是如果从节点数量
小于3个,或所有的从节点的延迟值都大于10S,那么主节点拒绝执行写命令。而这里从节点延迟值的获取,就是通过
主节点接收到REPLCONF ACK命令的时间来判断的,即前面所说的info Replication中的lag值。
6.应用中的问题
1:读写分离及其中的问题
在主从复制基础上实现的读写分离,可以实现redis读的负载均衡,由主节点提供写服务,由一个或者多个从节点提供读服务,在读负载
较大的场景下,可以提高redis的并发量。
在使用redis读写分离时,需要注意的问题:
1.延迟与不一致问题,
由于主从复制的命令传播是异步的,延迟和数据的不一致不可避免,如果应用对数据不一致的接收程度较低,可能的
优化措施包括:
1.优化主从节点之间的网络环境,比如在同机房部署
2.监控主从节点延迟(通过offset)判断,如果从节点延迟过大,通知应用不再通过该从节点读取数据
3.使用集群同时扩展写负载和读负载等
在命令传播阶段以外的其他情况,从节点数据不一致可能更加严重,例如连接在数据同步阶段,或者从节点失去与
主节点的连接等。
从节点的slave-serve-stale-data参数:默认是yes,从节点可以响应客户端的命令,如果是no,则从节点只能响应
info,slaveof等少数命令,如果应用对数据的一致性要求高,应该设置成no
2.数据过期问题:
单机redis,数据的删除有两种策略:
1.惰性删除
2.定期删除
在主从复制场景下,为了主从节点的数据一致性,从节点不会主动删除数据,而是由主节点控制从节点中过期数据的删除
由于主节点的惰性删除和定期删除都不能保证主节点及时对过期数据执行删除操作,因此,当客户端通过redis从节点
读取数据时,很容易读取到已经过期的数据。redis3.2中,从节点在读取数据时,增加了对数据是否过期的判断,如果
该数据已经过期,则不返回给客户端。
3.故障切换问题
在没有使用哨兵的读写分离场景下,应用针对读和写分别连接不同的redis节点,当主节点或者从节点出现问题而发生变更
时,需要及时修改应用程序读写redis数据的连接,连接的切换可以手动执行,或者自己写监控进行切换,但是前者响应慢
且容易出错,后者实现复杂。
2.复制超时问题
主从节点复制超时是导致复制中断的最重要的原因之一
在复制连接建立过程中及之后,主从节点都有机制判断连接是否超时,如果主节点判断连接超时,其会释放相应从节点的连接,
从而释放各种资源,否则无效的从节点仍会占用主节点的各种资源。如果从节点判断连接超时时,则可以及时重新建立连接,
避免与主节点数据的长期不一致。
判断的机制:
主从节点超时判断的核心,在于repl-timeout参数,默认是60S,对于主从节点同时有效,主节点每秒调用一次定时函数,
在其中判断当前时间距离上次收到各个从节点的时间,是否超过60S,如果超过,释放该从节点的连接。从节点类似。
复制阶段与连接超时有关的一些实际问题:
1.数据同步阶段:在主从节点进行全量复制bgsave时,主节点首先需要fork子进程将当前数据保存到RDB文件中,然后再将
RDB文件通过网络传输到从节点,如果RDB文件过大,主节点在fork子进程和保存RDB文件时耗时过多,可能会导致从节点
长时间收不到数据而触发超时,此时从节点会重连主节点,然后再次全量复制,再次超时。一些循环下去,为了避免这些
情况,除了注意redis单机数据量不要过大,另一方面就是适当的增加repl-timeout值,具体可根据bgsave耗时来调整。
2.命令传播阶段:在该阶段主节点会向从节点发送ping命令,频率由repl-ping-slave-period控制。该参数应明显小于
repl-timeout值。否则,如果两个参数相等或者接近,网络抖动导致个别PING命令丢失,此时恰巧主节点也没有向从节点
发送数据,那从节点很容易判断超时。
3.慢查询导致的阻塞:如果主节点或从节点执行了一些慢查询,也就是一些耗时较长的查询,导致服务器阻塞,
阻塞期间无法响应复制链接中对方节点的请求,可能导致复制超时。
3.复制中断问题
复制缓冲区溢出问题也可能导致复制中断。
在全量复制阶段,主节点会将执行的写命令放到复制缓冲区中,该缓冲区放的数据包括了以下几个时间段内主节点执行的
写命令,bgsave生成的RDB文件,RDB文件由主节点发送到从节点,从节点清空老数据并载入RDB文件中的数据。
当主节点数据量过大,或者主从节点之间网络延迟较大时,可能导致该缓冲区的大小超过了限制,此时主节点会断开与
从节点之间的连接,这种情况可能引起全量复制,然后复制缓冲区溢出导致连接中断,再重连后进行全量复制,全量复制
又导致复制缓冲区溢出导致连接中断这样的循环。复制缓冲区的大小由
client-output-buffer-limit slave{hard limit}{soft limit}{soft seconds}配置,默认是
client-output-buffer-limit slave 256MB 64MB 60,意思是如果缓冲区大于256M,或者连续60S大于64M,则主节点会
断开与该从节点的连接。
需要注意的是,复制缓冲区是客户端输出缓冲区的一种,主节点会为每一个从节点分配一个复制缓冲区,而
复制积压缓冲区则是一个主节点只有一个,无论有多少个从节点。
各场景下复制的选择和优化技巧:
第一次建立复制:
此时全量复制是不可避免的,但是需要注意的是,如果主节点数据量较大,应该避开流量的高峰期,避免造成阻塞,
如果有多个从节点需要建立对主节点的复制,可以考虑将几个从节点错开,避免主节点带宽占用大,此外,如果从节点过多,
可以调整主从的拓扑结构,由一主多从,变成树状结构,也就是,中间的节点即使主节点的从节点,也是其从节点的主节点,
缺点是数据一致性差,延迟增大,维护较困难。
主节点重启:一种故障导致宕机重启,一种是有计划的重启
主节点重启,runid会发生变化,会进行全量复制,实际上主节点宕机的情况下,应进行故障转移处理,将其中的一个从节点
升级成主节点。其他从节点从新的主节点进行复制。
安全重启:debug reload
在一些场景下,可能希望对主节点进行重启,例如主节点内存碎片率过高,但是普通的重启会使runid发生变化,
导致不必要的全量复制。而redis有一种debug reload的重启方式,重启后,redis的runid和offset都不会发生变化,
避免全量复制。
命令:redis-cli debug reload
但是这种方式会清空当前内存中的数据,重新从RDB中加载,这个过程会导致主节点的阻塞,谨慎使用。
网络中断:
中断时间极短:只造成了暂时的丢包,主从节点都没有判断超时,此时只需要通过REPLCONF ACK补充数据即可
中断时间长:主从节点判断了超时,且丢失的数据过多,超过了复制积压缓冲区所能存储的范围,此时只能进行
全量复制,但是可以调整复制积压缓冲区的大小,或者及时的修复网络中断。减轻压力
单机内存大小限制:
在redis的实际使用中,限制单机内存大小的因素非常多,单机内存过大可能造成的影响:
1.切主:当主节点宕机时,一种常见的容灾策略是将其中一个从节点提升为主节点,并将其他主节点挂载到新的主节点上,此时这些从节点
只能进行全量复制,如果redis单机内存达到10G,一个从节点的同步时间在几分钟的级别,如果从节点较多,恢复的速度更慢,
如果系统的读负载很高,而这段时间从节点无法进行服务,会对系统造成很大的压力
2.从库扩容:如果访问量突然增大,此时希望增加从节点分担读负载,如果数据量过大,从节点同步太慢,难以及时应对访问量的暴增
3.缓冲区溢出:切主和从库扩容都是从节点可以正常同步的过程,虽然慢,但是如果数据量很大,造成全量复制阶段主节点的复制
缓冲区溢出,从而导致复制中断,则主从节点的数据复制会全量复制。然后会再次导致复制缓冲区溢出的循环。
4.超时:如果数据量过大,全量复制阶段主节点fork和保存RDB文件耗时过大,从节点长时间接收不到数据触发超时,主从节点的数据
同步同样可能陷入全量复制,然后会超时导致复制中断,再次重连时,还是全量复制的循环
主节点单机内存不能太大,其占用主机内存的比例也不能太大,最好是使用一半左右的内存,剩余的用于执行bgsave命令和创建
复制缓冲区。
redis的哨兵技术:实现故障恢复自动化
redis主从复制的作用有数据热备,负载均衡和故障恢复等,但是主从复制存在的一个问题就是故障恢复无法自动化。
redis安装
1.redis是c语言进行开发的,安装redis需要C语言的编译环境,如果没有gcc需要在线安装:yum install gcc-c++
2.获取源码包:wget http://download.redis.io/releases/redis-3.0.0.tar.gz
3.解压redis:tar zxvf redis-3.0.0.tar.gz
4.编译,进入redis源码目录(cd redis-3.0.0)。执行make
5.安装,make install PREFIX=/usr/local/redis PREFIX参数指定redis的安装目录,一般软件安装到/usr目录下
这样redis就成功的安装到了usr/local/redis目录下
6.设置后台启动,进入到redis-3.0.0目录下:cp redis.conf /usr/local/redis/bin/ 将redis.conf复制到/usr/local/redis/bin/目录下
然后修改配置文件,将daemonize后面的参数改成yes
测试启动:在bin目录下: ./redis-server redis.conf
查看redis进程: ps aux|grep redis
redis集群搭建:3.0之前不支持集群
redis集群的架构:所有的redis服务器节点都是两两相连,所有客户端只要连接到其中一台redis服务器就可以对其他redis服务器进行存读操作。
如何实现:redis集群中内置了16384个哈希槽,当需要在redis集群中放置一个KV时,redis先对key使用crc16算法算出一个结果,然后把结果对
16384求余,这样每一个key都会对应一个编号在0-16383之间的哈希槽,redis会根据节点数量大致均等的将哈希槽映射到不同的节点。
例子:集群搭建时,假如有三个节点,node1,node2,node3。需要给集群的节点分配16384个插槽,为了大致均等,node1节点会分到第0-5000插槽,
node2节点会分到第5001-10000插槽,剩余的10001-16384个插槽就分到了node3上,当在node1上执行set age 18时,首先会使用crc16算法对key
进行计算,也就是对age进行计算,算出一个数字,假如算出一个数字是26384,然后使用26384对16384求余:26384%16384=10000,结果是10000,
然后查找包含10000插槽的节点,找到了node2,自动跳转到node2,在node2上执行set age 18。如果在node3上获取该值的时候:get age。
首先也是通过crc16算法对age进行计算,计算出来的结果对16384取余,算出10000,查找包含10000的节点,跳转到node2上。在执行get age。
redis的投票机制:
redis集群中有多台redis服务器,不可避免的就是会有服务器可能挂掉,redis集群之间通过互相的ping-pong判断是否节点可以连上,如果有一半的节点
去ping-pong一个节点都没有收到回应,集群就认为该节点宕机了。
redis集群搭建:
上述已经装好了一个redis实例,现在需要把它复制6份并修改相应的端口.
1.新建redis-cluster文件夹:与redis文件夹同级目录
2.复制redis实例,cp redis/bin redis-cluster/redis1:如果复制过去的redis实例有dump.rdb文件的话最好也要删除
3.修改配置文件,修改bin目录下的redis.conf配置文件。首先是改端口号,然后把cluster-enabled yes注释掉。
4.继续复制5个实例:将上述的redis1实例复制出5份在它的同级目录下,然后依次修改配置文件的端口号。
5.新建一个执行脚本,vim start-all.sh
cd redis1/bin
./redis-server redis.conf
cd ..
cd ..
cd redis2/bin
./redis-server redis.conf
cd ..
cd ..
cd redis3/bin
./redis-server redis.conf
cd ..
cd ..
cd redis4/bin
./redis-server redis.conf
cd ..
cd ..
cd redis5/bin
./redis-server redis.conf
cd ..
cd ..
cd redis6/bin
./redis-server redis.conf
6.为脚本赋予执行权限:chmod u+x start-all.sh,并启动执行脚本,启动全部实例:./start-all.sh
7.将redis-trib.rb复制到redis-cluster目录下。该文件在redis-3.0.0/src目录下,并给该脚本赋予执行权限:chmod u+x redis-trib.rb
8.安装ruby和ruby运行环境:yum install ruby yum install rubygems gem install redis-3.0.0.gem
9.使用ruby脚本搭建集群:
./redis-trib.rb create --replicas 1 192.168.25.155:7001 192.168.25.155:7002 192.168.25.155:7003 192.168.25.155:7004
192.168.25.155:7005 192.168.25.155:7006
端口修改错误或者没有将cluster-enabled yes前的注释去掉都会导致集群搭建的失败。至此,redis集群搭建完成
redis单机版和集群版的测试使用:
添加maven依赖:
<!-- Redis客户端 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
单机版redis测试:
@Test
public void testJedis() throws Exception {
// 创建一个连接Jedis对象,参数:host、port
Jedis jedis = new Jedis("192.168.25.155", 6379);
// 直接使用jedis操作redis。所有jedis的命令都对应一个方法。
jedis.set("age", "18");
String string = jedis.get("age");
System.out.println(string);//输出内容:18
// 关闭连接
jedis.close();
}
使用连接池测试单机版redis:
@Test
public void testJedisPool() throws Exception {
// 创建一个连接池对象,两个参数host、port
JedisPool jedisPool = new JedisPool("192.168.25.155", 6379);
// 从连接池获得一个连接,就是一个jedis对象。
Jedis jedis = jedisPool.getResource();
// 使用jedis操作redis
String string = jedis.get("age");
System.out.println(string);//输出内容:18
// 关闭连接,每次使用完毕后关闭连接。连接池回收资源。
jedis.close();
// 关闭连接池。
jedisPool.close();
}
测试集群版redis;
@Test
public void testJedisCluster() throws Exception {
// 创建一个JedisCluster对象。有一个参数nodes是一个set类型。set中包含若干个HostAndPort对象。
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("192.168.25.155", 7001));
nodes.add(new HostAndPort("192.168.25.155", 7002));
nodes.add(new HostAndPort("192.168.25.155", 7003));
nodes.add(new HostAndPort("192.168.25.155", 7004));
nodes.add(new HostAndPort("192.168.25.155", 7005));
nodes.add(new HostAndPort("192.168.25.155", 7006));
JedisCluster jedisCluster = new JedisCluster(nodes);
// 直接使用JedisCluster对象操作redis。
jedisCluster.set("test", "123");
String string = jedisCluster.get("test");
System.out.println(string);
// 关闭JedisCluster对象
jedisCluster.close();
}
在javaweb中实现单机和集群无缝切换使用:
如何在项目中实现不用修改代码就能在redis单机和集群模式之间的切换
创建相应的类的接口:JedisClient.java JedisClientCluster.java JedisClientPool.java
接口:
import java.util.List;
public interface JedisClient {
String set(String key, String value);
String get(String key);
Boolean exists(String key);
Long expire(String key, int seconds);
Long ttl(String key);
Long incr(String key);
Long hset(String key, String field, String value);
String hget(String key, String field);
Long hdel(String key, String... field);
Boolean hexists(String key, String field);
List<String> hvals(String key);
Long del(String key);
}
集群版使用:
import java.util.List;
import redis.clients.jedis.JedisCluster;
public class JedisClientCluster implements JedisClient {
private JedisCluster jedisCluster;
public JedisCluster getJedisCluster() {
return jedisCluster;
}
public void setJedisCluster(JedisCluster jedisCluster) {
this.jedisCluster = jedisCluster;
}
@Override
public String set(String key, String value) {
return jedisCluster.set(key, value);
}
@Override
public String get(String key) {
return jedisCluster.get(key);
}
@Override
public Boolean exists(String key) {
return jedisCluster.exists(key);
}
@Override
public Long expire(String key, int seconds) {
return jedisCluster.expire(key, seconds);
}
@Override
public Long ttl(String key) {
return jedisCluster.ttl(key);
}
@Override
public Long incr(String key) {
return jedisCluster.incr(key);
}
@Override
public Long hset(String key, String field, String value) {
return jedisCluster.hset(key, field, value);
}
@Override
public String hget(String key, String field) {
return jedisCluster.hget(key, field);
}
@Override
public Long hdel(String key, String... field) {
return jedisCluster.hdel(key, field);
}
@Override
public Boolean hexists(String key, String field) {
return jedisCluster.hexists(key, field);
}
@Override
public List<String> hvals(String key) {
return jedisCluster.hvals(key);
}
@Override
public Long del(String key) {
return jedisCluster.del(key);
}
}
单机版使用:
import java.util.List;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class JedisClientPool implements JedisClient {
private JedisPool jedisPool;
public JedisPool getJedisPool() {
return jedisPool;
}
public void setJedisPool(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
@Override
public String set(String key, String value) {
Jedis jedis = jedisPool.getResource();
String result = jedis.set(key, value);
jedis.close();
return result;
}
@Override
public String get(String key) {
Jedis jedis = jedisPool.getResource();
String result = jedis.get(key);
jedis.close();
return result;
}
@Override
public Boolean exists(String key) {
Jedis jedis = jedisPool.getResource();
Boolean result = jedis.exists(key);
jedis.close();
return result;
}
@Override
public Long expire(String key, int seconds) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.expire(key, seconds);
jedis.close();
return result;
}
@Override
public Long ttl(String key) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.ttl(key);
jedis.close();
return result;
}
@Override
public Long incr(String key) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.incr(key);
jedis.close();
return result;
}
@Override
public Long hset(String key, String field, String value) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.hset(key, field, value);
jedis.close();
return result;
}
@Override
public String hget(String key, String field) {
Jedis jedis = jedisPool.getResource();
String result = jedis.hget(key, field);
jedis.close();
return result;
}
@Override
public Long hdel(String key, String... field) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.hdel(key, field);
jedis.close();
return result;
}
@Override
public Boolean hexists(String key, String field) {
Jedis jedis = jedisPool.getResource();
Boolean result = jedis.hexists(key, field);
jedis.close();
return result;
}
@Override
public List<String> hvals(String key) {
Jedis jedis = jedisPool.getResource();
List<String> result = jedis.hvals(key);
jedis.close();
return result;
}
@Override
public Long del(String key) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.del(key);
jedis.close();
return result;
}
}
applicationContext-redis.xml
<?xml version="1.0" encoding="UTF-8"?><!-- 连接redis单机版 -->
<bean id="jedisClientPool" class="xx.xx.xx.JedisClientPool">
<property name="jedisPool" ref="jedisPool"></property>
</bean>
<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
<constructor-arg name="host" value="192.168.25.155"/>
<constructor-arg name="port" value="6379"/>
</bean>
<!-- 连接redis集群 -->
测试代码:
public class JedisClientTest {
@Test
public void testJedisClient() throws Exception {
//初始化spring容器
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring/applicationContext-redis.xml");
//从容器中获得JedisClient对象
JedisClient jedisClient = applicationContext.getBean(JedisClient.class);
jedisClient.set("aaa", "111");
String string = jedisClient.get("aaa");
System.out.println(string);
}
}