五种数据类型使用场景
string类型
Redis提供了原生命令
**典型示例1:**商品库存数,商品的浏览次数,问题或者回复的点赞次数等
incr key
decr key
incrby key increment
decrby key decrement
**典型示例2:**时效信息存储
Redis的数据存储具有自动失效能力,存储的key-value可以设置过期时间。
例如,用户登录某个App需要获取登录验证码,验证码在30秒内有效。就可以使用string类型存储验证 码,同时设置30秒的失效时间。
keys = redisCli.get(key);
if(keys != null)
{
return false;
}
else
{
sendMsg();
redisCli.set(keys, value, expireTime);
}
list类型
list是按照插入顺序排序的字符串链表,可以在头部和尾部插入新的元素
消息队列实现(一般不使用)主流:Kafka、RocketMQ、RabbitMQ
典型示例1:最新上架商品
展示最新上架前100名,进行TOP产品的存储。
// 把新上架商品添加到链表里
ret = r.lpush("new:goods", goodsId)
// 保持链表100位
ret = r.ltrim("new:goods", 0, 99)
// 获得前100个最新上架的商品id列表
newest_goods_list = r.lrange("new:goods", 0, 99)
set类型
set存储了一个无序集合,具备去重功能。 当需要存储一个列表信息,同时要求列表内的元素不能有重复,用set比较合适。与此同时,set还提供 求交集、并集、差集。
获取到两个用户相似的产品,然后确定相似产品的类目就可以进行用户分析。类似的应用场景还有,社交场景下共同关注好友,相似兴趣等来做人物画像。
// userid为用户ID,goodID为感兴趣的商品信息
// sadd "user:userId" goodID
sadd "user:1001" 1
sadd "user:1001" 2
sadd "user:1002" 1
sadd "user:1002" 3
sinter "user:1001" "user:1002"
// 结果为1
hash类型
Redis在存储对象(例如,用户信息)的时候需要对对象进行序列化转换然后存储。
{
"name": "isisiwish",
"phone": "1008611",
"sex": "男"
}
这种类似场景还很多,例如订单数据,产品数据,商家基本信息等。
Redis sorted set的使用场景与set类似,区别是set无序的,而sorted set可以通过提供一个score参数来 为存储数据排序,并且是自动排序,插入既有序。业务中如果需要一个有序且不重复的集合列表,就可 以选择sorted set数据结构。
SDS
Redis中字符串的实现。在3.2以后的版本中,SDS又有多种结构(sds.h):
- sdshdr5
- sdshdr8
- sdshdr16
- sdshdr32
- sdshdr64
用于存储不同的长度的字符串,分别代表:
2^5=32byte 2^8=256byte 2^16=65536byte=64KB 2^32byte=4GB
Redis使用SDS实现字符串的原因:
C语言本身没有字符串类型(只能用字符数组char[]实现)。
- 使用字符数组必须先给目标变量分配足够的空间,否则可能会溢出。
- 如果要获取字符长度,必须遍历字符数组,时间复杂度是O(n)。
- C字符串长度的变更会对字符数组做内存重分配。
- 通过从字符串开始到结尾碰到的第一个'\0'来标记字符串的结束,因此不能保存图片、音频、视频、 压缩文件等二进制(bytes)保存的内容,二进制不安全。
SDS的特点
- 不用担心内存溢出问题,如果需要会对SDS进行扩容。
- 获取字符串长度时间复杂度为O(1),因为定义了len属性。
- 通过“空间预分配”(sdsMakeRoomFor)和“惰性空间释放”,防止多次重分配内存。
- 判断是否结束的标志是len属性(它同样以'\0'结尾是因为这样就可以使用C语言中函数库操作字符串 的函数了),可以包含'\0'。
持久化机制
持久化概述
Redis是内存数据库,数据都是存储在内存中,为了避免进程退出导致数据的永久丢失,需要定期将Redis中的数据以某种形式(数据或命令)从内存保存到硬盘。当下次Redis重启时,利用持久化文件实现数据恢复。
Redis持久化分为RDB持久化和AOF持久化,前者将当前数据保存到硬盘,后者则是将每次执行的写命令保存到硬盘(类似于MySQL的Binlog)。由于AOF持久化的实时性更好,即当进程意外退出时丢失的数据更少,因此AOF是目前主流的持久化方式。
RDB持久化
RDB持久化执行流程:
将Reids在内存中的数据定时dump到磁盘上的rdb文件,在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
RDB优点
- RDB文件紧凑,体积小,网络传输快,适合全量复制
- 恢复速度比AOF快很多,对性能的影响相对较小
RDB缺点
- 数据快照的持久化方式做不到实时持久化
AOF持久化
AOF持久化是将Redis执行的每次写命令记录到单独的日志文件中(有点像MySQL的binlog);当Redis重启时再次执行AOF文件中的命令来恢复数据。与RDB相比,AOF的实时性更好,因此已成为主流的持久化方案,
AOF执行流程:
命令追加(append):
将Redis的写命令追加到缓冲区aof_buf,再将命令写入文件,避免每次有写命令都直接写入硬盘,导致硬盘IO成为Redis负载的瓶颈。
AOF缓存区的同步文件策略由参数appendfsync控制:
- always:命令写入aof_buf后立即调用系统fsync操作同步到AOF文件,fsync完成后线程返回。1、硬盘IO成为性能瓶颈;2、使用固态硬盘会大大降低寿命。
- no:命令写入aof_buf后调用系统write操作,不对AOF文件做fsync同步;同步由操作系统负责,通常同步周期为30秒。数据会很多,数据安全性无法保证。
- everysec:命令写入aof_buf后调用系统write操作,write完成后线程返回;fsync同步文件操作由专门的线程每秒调用一次。默认配置且为推荐配置。
AOF优点
- 支持秒级持久化、兼容性好
- 更好地保护数据不丢失 appen-only模式写入性能比较高 适合做灾难性的误删除紧急恢复
AOF缺点
- 对于同一份文件,AOF文件要比RDB快照大 AOF开启后,写的QPS会有所影响,相对于RDB来说写QPS要下降 数据库恢复比较慢,不合适做冷备
AOF常用配置总结:
- appendonly no:是否开启AOF
- appendfilename "appendonly.aof":AOF文件名
- dir ./:RDB文件和AOF文件所在目录
- appendfsync everysec:fsync持久化策略
- no-appendfsync-on-rewrite no:AOF重写期间是否禁止fsync;如果开启该选项,可以减轻文件重写时CPU和硬盘的负载(尤其是硬盘),但是可能会丢失AOF重写期间的数据;需要在负载和安全性之间进行平衡
- auto-aof-rewrite-percentage 100:文件重写触发条件之一
- auto-aof-rewrite-min-size 64mb:文件重写触发提交之一
- aof-load-truncated yes:如果AOF文件结尾损坏,Redis启动时是否仍载入AOF文件
策略选择
实际生产环境中,根据数据量、应用对数据的安全要求、预算限制等不同情况,会有各种各样的持久化策略;如完全不使用任何持久化、使用RDB或AOF的一种,或同时开启RDB和AOF持久化等。此外,持久化的选择必须与Redis的主从策略一起考虑,因为主从复制与持久化同样具有数据备份的功能,而且主机master和从机slave可以独立的选择持久化方案。
主从模式
概述
主从复制是指将一台Redis服务器的数据,复制到其它的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。且为1:n的关系。
作用
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
- 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复,但实际上是一种服务的冗余。
- 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
- 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
主从复制的实现原理
一、连接建立阶段
- 步骤1:从节点保存主节点信息,从节点服务器内部维护了两个字段,即masterhost和masterport字段,用于存储主节点的ip和port信息。
- 步骤2:建立socket连接,从节点每秒1次调用复制定时函数replicationCron(),如果发现了有主节点可以连接,便会根据主节点的ip和port,创建socket连接。
- 步骤3:发送ping命令,从节点成为主节点的客户端之后,发送ping命令进行首次请求,目的是检查socket连接是否可用以及主节点当前是否能够处理请求。
- 步骤4:身份验证。从节点进行身份验证是通过向主节点发送auth命令进行的,auth命令的参数即为配置文件中的masterauth的值。 如果主节点设置密码的状态,与从节点masterauth的状态一致(一致是指都存在,且密码相同,或者都不存在),则身份验证通过,复制过程继续;如果不一致,则从节点断开socket连接,并重连。
- 步骤5:发送从节点端口信息。身份验证后,从节点会向主节点发送其监听的端口号(前述例子中为6380),主节点将该信息保存到该从节点对应的客户端的slave_listening_port字段中;该端口信息除了在主节点中执行info Replication时显示以外,没有其他作用。
二、数据同步阶段
数据同步阶段是主从复制最核心的阶段,根据主从节点当前状态的不同,可以分为全量复制和部分复制
全量复制过程
- 从节点判断无法进行部分复制,向主节点发送全量复制的请求;或从节点发送部分复制的请求,但主节点判断无法进行全量复制。
- 主节点收到全量复制的命令后,执行bgsave,在后台生成RDB文件,并使用一个缓冲区(称为复制缓冲区)记录从现在开始执行的所有写命令。
- 主节点的bgsave执行完成后,将RDB文件发送给从节点;从节点首先清除自己的旧数据,然后载入接收的RDB文件,将数据库状态更新至主节点执行bgsave时的数据库状态。
- 主节点将前述复制缓冲区中的所有写命令发送给从节点,从节点执行这些写命令,将数据库状态更新至主节点的最新状态。
- 如果从节点开启了AOF,则会触发bgrewriteaof的执行,从而保证AOF文件更新至主节点的最新状态。
全量复制的影响
- 主节点通过bgsave命令fork子进程进行RDB持久化,该过程是非常消耗CPU、内存(页表复制)、硬盘IO的。
- 主节点通过网络将RDB文件发送给从节点,对主从节点的带宽都会带来很大消耗。
- 从节点清空老数据、载入新RDB文件的过程是阻塞的,无法响应客户端的命令。如果从节点执行bgrewriteaof,也会带来额外的消耗。
部分复制过程
- 主节点和从节点分别维护一个复制偏移量(offset),代表的是主节点向从节点传递的字节数;主节点每次向从节点传播N个字节数据时,主节点的offset增加N;从节点每次收到主节点传来的N个字节数据时,从节点的offset增加N。
- 复制积压缓冲区是由主节点维护的、固定长度的、先进先出(FIFO)队列,默认大小1MB;当主节点开始有从节点时创建,其作用是备份主节点最近发送给从节点的数据。
- 主从节点初次复制时,主节点将自己的runid(redis-cli info server |grep run_id)发送给从节点,从节点将这个runid保存起来;当断线重连时,从节点会将这个runid发送给主节点;主节点根据runid判断能否进行部分复制: 如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况); 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。
psync
psync命令执行来判断主从节点是如何确定使用全量复制还是部分复制的
- 从节点根据当前状态,决定如何调用psync命令
- 主节点根据收到的psync命令及当前服务器状态来决定执行全量复制还是部分复制
主从模式优点
- Master/Slave 角色方便水平扩展,QPS 增加,增加 Slave 即可;
- 降低 Master 读压力,转交给 Slave 节点;
- 主节点宕机,从节点作为主节点的备份可以随时顶上继续提供服务。
主从模式缺点
- 可靠性保证不是很好,主节点故障便无法提供写入服务;
- 没有解决主节点写的压力;
- 数据冗余(为了高并发、高可用和高性能,一般是允许有冗余存在的);
- 一旦主节点宕机,从节点晋升成主节点,需要修改应用方的主节点地址,还需要命令所有从节点去 复制新的主节点,整个过程需要人工干预;
- 主节点的写能力受到单机的限制;
- 主节点的存储能力受到单机的限制。
各场景下主从复制的选择及优化技巧:
**第一次建立复制:
**
此时全量复制不可避免,但仍有几点需要注意:如果主节点的数据量较大,应该尽量避开流量的高峰期,避免造成阻塞;如果有多个从节点需要建立对主节点的复制,可以考虑将几个从节点错开,避免主节点带宽占用过大。此外,如果从节点过多,也可以调整主从复制的拓扑结构,由一主多从结构变为树状结构(中间的节点既是其主节点的从节点,也是其从节点的主节点);但使用树状结构应该谨慎——主节点的直接从节点减少,降低了主节点的负担,但是多层从节点的延迟增大,数据一致性变差,且结构复杂,维护相当困难。
主节点重启:
- **主节点宕机:**主节点宕机重启后,Runid会发生变化,因此不能进行部分复制,只能全量复制。实际上在主节点宕机的情况下,应进行故障转移处理,将其中的一个从节点升级为主节点,其它从节点从新的主节点进行复制;且故障转移应尽量的自动化,后面文章将要介绍的哨兵便可以进行自动的故障转移。
- **安全重启:**在一些场景下,可能希望对主节点进行重启,例如主节点内存碎片率过高,或者希望调整一些只能在启动时调整的参数。如果使用普通的手段重启主节点,会使得runid发生变化,可能导致不必要的全量复制。 为了解决这个问题,Redis提供了debug reload的重启方式:重启后,主节点的runid和offset都不受影响,避免了全量复制。
- **从节点重启:**从节点宕机重启后,其保存的主节点的runid会丢失,因此即使再次执行slaveof,也无法进行部分复制。
- **网络中断:**①网络问题时间极为短暂,只造成了短暂的丢包,主从节点都没有判定超时(未触发repl-timeout)。此时只需要通过REPLCONF ACK来补充丢失的数据即可。②网络问题时间很长,主从节点判断超时(触发了repl-timeout),且丢失的数据过多,超过了复制积压缓冲区所能存储的范围。此时主从节点无法进行部分复制,只能进行全量复制。为了尽可能避免这种情况发生,应该根据实际情况适当调整复制积压缓冲区的大小;此外及时发现并修复网络中断,也可以减少全量复制。
主从复制相关配置
与主从节点都有关的配置:
- slaveof:Redis启动时起作用;作用是建立复制关系,开启了该配置的Redis服务器在启动后成为从节点。该注释默认注释掉,即Redis服务器默认都是主节点
- repl-timeout 60:与各个阶段主从节点连接超时判断有关
主节点相关配置:
- repl-diskless-sync no:作用于全量复制阶段,控制主节点是否使用diskless复制(无盘复制)。所谓diskless复制,是指在全量复制时,主节点不再先把数据写入RDB文件,而是直接写入slave的socket中,整个过程中不涉及硬盘;diskless复制在磁盘IO很慢而网速很快时更有优势。需要注意的是,截至Redis3.0,diskless复制处于实验阶段,默认是关闭的
- repl-diskless-sync-delay 5:该配置作用于全量复制阶段,当主节点使用diskless复制时,该配置决定主节点向从节点发送之前停顿的时间,单位是秒;只有当diskless复制打开时有效,默认5s。之所以设置停顿时间,是基于以下两个考虑:a.向slave的socket的传输一旦开始,新连接的slave只能等待当前数据传输结束,才能开始新的数据传输;b.多个从节点有较大的概率在短时间内建立主从复制
- client-output-buffer-limit slave 256MB 64MB 60:与全量复制阶段主节点的缓冲区大小有关
- repl-disable-tcp-nodelay no:与命令传播阶段的延迟有关
- masterauth :与连接建立阶段的身份验证有关
- repl-ping-slave-period 10:与命令传播阶段主从节点的超时判断有关
- repl-backlog-size 1mb:复制积压缓冲区的大小
- repl-backlog-ttl 3600:当主节点没有从节点时,复制积压缓冲区保留的时间,这样当断开的从节点重新连进来时,可以进行全量复制(默认3600s)。如果设置为0,则永远不会释放复制积压缓冲区。
- min-slaves-to-write 3与min-slaves-max-lag 10:规定了主节点的最小从节点数目及对应的最大延迟
从节点相关配置:
- slave-serve-stale-data yes:与从节点数据陈旧时是否响应客户端命令有关
- slave-read-only yes:从节点是否只读;默认是只读的。由于从节点开启写操作容易导致主从节点的数据不一致,因此该配置尽量不要修改
单机内存大小限制:
- **切主:**当主节点宕机时,一种常见的容灾策略是将其中一个从节点提升为主节点,并将其它从节点挂载到新的主节点上,此时这些从节点只能进行全量复制。如果Redis单机内存达到10GB,一个从节点的同步时间在几分钟的级别;如果从节点较多,恢复的速度会更慢;如果系统的读负载很高,而这段时间从节点无法提供服务,会对系统造成很大的压力。
- 从库扩容:****如果访问量突然增大,此时希望增加从节点分担读负载,如果数据量过大,从节点同步太慢,难以及时应对访问量的暴增。
- 缓冲区溢出:****切主和从库扩容都是从节点可以正常同步的情形(虽然慢),但是如果数据量过大,造成全量复制阶段主节点的复制缓冲区溢出,从而导致复制中断,则主从节点的数据同步会全量复制→复制缓冲区溢出导致复制中断→重连→全量复制→复制缓冲区溢出导致复制中断……的循环。
- 超时:****如果数据量过大,全量复制阶段主节点fork+保存RDB文件耗时过大,从节点长时间接收不到数据触发超时,主从节点的数据同步同样可能陷入全量复制→超时导致复制中断→重连→全量复制→超时导致复制中断……的循环。
- 内存限制**:**主节点单机内存除了绝对量不能太大,其占用主机内存的比例也不应过大:最好只使用50%-65%的内存,留下30%-45%的内存用于执行bgsave命令和创建复制缓冲区等。
哨兵模式
在主从复制的基础上,哨兵模式实现了自动化故障恢复。
哨兵模式由两部分组成,哨兵节点和数据节点:
- 哨兵节点:哨兵节点是特殊的 Redis 节点,不存储数据;
- 数据节点:主节点和从节点都是数据节点。
Redis Sentinel 是分布式系统中监控 Redis 主从服务器,并提供主服务器下线时自动故障转移功能的模式。
哨兵模式的三个特性
- 监控:Sentinel 会不断地检查你的主服务器和从服务器是否运作正常;
- 提醒:当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知;
- 自动故障迁移:当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作。
- 配置提供者:客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。
哨兵模式原理
三个定时任务
- 每 1 秒每个 Sentinel 对其他 Sentinel 和 Redis 节点执行 PING 操作(监控),这是一个心跳检 测,是失败判定的依据。
- 每 2 秒每个 Sentinel 通过 Master 节点的 channel 交换信息(Publish/Subscribe);
- 每 10 秒每个 Sentinel 会对 Master 和 Slave 执行INFO命令,这个任务主要达到两个目的: 发现 Slave 节点; 确认主从关系。
主观下线
在心跳检测的定时任务中,如果其他节点超过一定时间没有回复,哨兵节点就会将其进行主观下线
客观下线
is-master-down-by-addr命令询问其他哨兵节点该主节点的状态;如果判断主节点下线的哨兵数量达到一定数值,则对该主节点进行客观下线
选举领导者哨兵节点
当主节点被判断客观下线以后,各个哨兵节点会进行协商,选举出一个领导者哨兵节点,并由该领导者节点对其进行故障转移操作。
故障转移
- 在从节点中选择新的主节点:选择的原则是,首先过滤掉不健康的从节点;然后选择优先级最高的从节点(由slave-priority指定);如果优先级无法区分,则选择复制偏移量最大的从节点;如果仍无法区分,则选择runid最小的从节点。
- 更新主从状态:通过slaveof no one命令,让选出来的从节点成为主节点;并通过slaveof命令让其他节点成为其从节点。
- 将已经下线的主节点设置为新的主节点的从节点,当原主节点重新上线后,它会成为新的主节点的从节点。
哨兵模式优点
- 哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都有;
- 主从可以自动切换,系统更健壮,可用性更高;
- Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
哨兵模式缺点
- 主从切换需要时间,会丢失数据;
- 还是没有解决主节点写的压力;
- 主节点的写能力,存储能力受到单机的限制;
- 动态扩容困难复杂,对于集群,容量达到上限时在线扩容会变得很复杂。
配置与实践建议
配置
- **sentinel monitor {masterName} {masterIp} {masterPort} {quorum}:**sentinel monitor是哨兵最核心的配置,在前文讲述部署哨兵节点时已说明,其中:masterName指定了主节点名称,masterIp和masterPort指定了主节点地址,quorum是判断主节点客观下线的哨兵数量阈值:当判定主节点下线的哨兵数量达到quorum时,对主节点进行客观下线。建议取值为哨兵数量的一半加1。
- **sentinel down-after-milliseconds {masterName} {time}:**sentinel down-after-milliseconds与主观下线的判断有关:哨兵使用ping命令对其他节点进行心跳检测,如果其他节点超过down-after-milliseconds配置的时间没有回复,哨兵就会将其进行主观下线。该配置对主节点、从节点和哨兵节点的主观下线判定都有效。 down-after-milliseconds的默认值是30000,即30s;可以根据不同的网络环境和应用要求来调整:值越大,对主观下线的判定会越宽松,好处是误判的可能性小,坏处是故障发现和故障转移的时间变长,客户端等待的时间也会变长。
- **sentinel parallel - syncs {masterName} {number}:**sentinel parallel-syncs与故障转移之后从节点的复制有关:它规定了每次向新的主节点发起复制操作的从节点个数。例如,假设主节点切换完成之后,有3个从节点要向新的主节点发起复制;如果parallel-syncs=1,则从节点会一个一个开始复制;如果parallel-syncs=3,则3个从节点会一起开始复制。parallel-syncs取值越大,从节点完成复制的时间越快,但是对主节点的网络负载、硬盘负载造成的压力也越大。
- **sentinel failover - timeout {masterName} {time}:**sentinel failover-timeout与故障转移超时的判断有关,但是该参数不是用来判断整个故障转移阶段的超时,而是其几个子阶段的超时,例如如果主节点晋升从节点时间超过timeout,或从节点向新的主节点发起复制操作的时间(不包括复制数据的时间)超过timeout,都会导致故障转移超时失败。 failover-timeout的默认值是180000,即180s;如果超时,则下一次该值会变为原来的2倍。
实践
- 哨兵节点的数量应不止一个。一方面增加哨兵节点的冗余,避免哨兵本身成为高可用的瓶颈;另一方面减少对下线的误判。此外,这些不同的哨兵节点应部署在不同的物理机上。
- 哨兵节点的数量应该是奇数,便于哨兵通过投票做出“决策”:领导者选举的决策、客观下线的决策等。
- 各个哨兵节点的配置应一致,包括硬件、参数等;此外,所有节点都应该使用ntp或类似服务,保证时间准确、一致。
- 哨兵的配置提供者和通知客户端功能,需要客户端的支持才能实现,如前文所说的Jedis;如果开发者使用的库未提供相应支持,则可能需要开发者自己实现。
- 当哨兵系统中的节点在Docker(或其他可能进行端口映射的软件)中部署时,应特别注意端口映射可能会导致哨兵系统无法正常工作,因为哨兵的工作基于与其他节点的通信,而Docker的端口映射可能导致哨兵无法连接到其他节点。