Redis进阶


Redis进阶

发布订阅

一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。

 

命令描述
SUBSCRIBE channel [channel ...] 订阅给定的一个或多个频道的信息。(创建频道)
UNSUBSCRIBE [channel [channel ...]] 指退订给定的频道。
PSUBSCRIBE pattern [pattern ...] 订阅一个或多个符合给定模式的频道。
PUNSUBSCRIBE [pattern [pattern ...]] 退订所有给定模式的频道。
PUBSUB subcommand [argument [argument ...]] 查看订阅与发布系统状态。
PUBLISH channel message 将信息发送到指定的频道。

事务

隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

原子操作:事务中的命令要么全部被执行,要么全部都不执行。

三个阶段:

  • 开始事务。MULTI

  • 命令入队(FIFO)。

  • 执行事务。EXEC

命令描述
MULTI 标记一个事务块的开始。
EXEC 执行所有事务块内的命令。
WATCH key [key ...] 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
UNWATCH 取消 WATCH 命令对所有 key 的监视。
DISCARD 取消事务,放弃执行事务块内的所有命令。

WATCH命令是一个乐观锁

Redis持久化机制

RDB

  1. RDB(Redis Database)持久化是把当前进程数据生成快照保存在RDB文件的过程。

  2. RDB持久化生成的RDB文件是一个经过压缩的二进制文件。

  3. 触发RDB持久化过程分为手动触发和自动触发。

手动触发

命令描述
SAVE 异步保存数据到硬盘(备份,将rdb文件移动到安装目录并重启服务集合恢复)
BGSAVE 在后台异步保存当前数据库的数据到磁盘

自动触发

Redis会判断是否满足持久化的条件,满足则自动执行bgsave命令进行持久化。是否满足则是通过Redis核心配置文件redis.conf来配置的。比如:

 1 # RDB 保存的条件
 2 save 900 1  # 表示 900 秒内如果至少有 1 个 key 值变化,则把数据持久化到硬盘
 3 save 300 10
 4 save 60 10000
 5 ?
 6 # bgsave 失败之后,是否停止持久化数据到磁盘,yes 表示停止持久化,no 表示忽略错误继续写文件。
 7 stop-writes-on-bgsave-error yes
 8 ?
 9 # RDB 文件压缩
10 rdbcompression yes
11 ?
12 # 写入文件和读取文件时是否开启 RDB 文件检查,检查是否有无损坏,如果在启动是检查发现损坏,则停止启动。
13 rdbchecksum yes
14 ?
15 # RDB 文件名
16 dbfilename dump.rdb
17 ?
18 # RDB 文件目录
19 dir ./

载入

  1. 如果Redis开启了AOF持久化功能,就会优先使用AOF文件来还原数据库状态,因为AOF文件的更新频率比RDB文件的更新频率高。

  2. 只有在AOF持久化关闭的状态时,服务器才会使用RDB文件来还原数据库状态。

  3. 服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。

优缺点

  1. 优点:RDB文件是一个紧凑的二进制文件,适用备份、全量复制的场景。加载RDB文件恢复数据远远快于AOF文件。

  2. 缺点:不能实时持久化,数据丢失的风险高。每次BGSAVE创建子进程属于重量级操作,频繁执行成本高。

AOF

  1. AOF(Append Only File)持久化以独立日志方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的。

  2. AOF的主要作用就是解决了数据持久化的实时性

  3. 被写入AOF文件的所有命令都是以Redis的命令请求协议格式保存的,因为Redis命令请求协议格式是纯文本格式,所以我们可以直接打开一个AOF文件观察里面的内容。

  4. 默认情况下AOF功能是关闭的。我们需要修改配置文件:appendonly no改为appendonly yes。

工作流程

  1. 命令追加:服务器执行完一个写命令后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区末尾。

  2. 文件同步:AOF缓冲区根据对应的策略向硬盘做同步操作。

  3. 文件重写:随着AOF越来越大,需要定期对AOF文件进行重写,达到压缩的目的。

  4. 重启加载:当Redis重启时,可以加载AOF文件进行数据恢复。

文件同步策略

  1. appendfsync always:每次写入都要同步AOF文件,从效率上看,是最慢的,从安全性上看,是最安全的。

  2. appendfsync everysec:每1秒钟同步一次,该策略为AOF的缺省策略。

  3. appendfsync no:从不同步。高效但是数据不会被持久化

Redis明明是单线程,为什么性能还那么高?

  1. 纯内存访问,所有数据放在内存中,内存响应的速度非常快。

  2. 非阻塞I/O,Redis使用epoll作为I/O多路复用技术的实现的,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多事件。

  3. 单线程避免了线程切换竞态产生的消耗

单线程优缺点

  1. 单线程可以简化数据结构和算法的实现。

  2. 单线程可以避免竞态产生的消耗,锁和线程切换通常是性能杀手。

  3. 对于每个命令的执行时间有要求,如果某个命令执行过长,会造成其他命令阻塞,对于Redis这种高性能的服务来说是致命的。

Redis流水线(管道)

  1. 在一般情况下,用户每执行一个Redis命令,客户端和服务器都需要进行一次通信:客户端向服务器发送命令请求,而服务器则会将执行命令所得的结果返回给客户端。

  2. 减少客户端和服务器之间的通信次数,可以有效提高程序的执行效率。

  3. 流水线功能可以将多条命令打包一起发送,并且在一次命令回复中包含所有被执行命令的结果,使用这个功能可以有效提升程序在执行多条命令时的效率。  

 1 import redis
 2   
 3 #创建连接池获取连接
 4 pool = redis.ConnectionPool(host='wykd', port=6379,password='123456', decode_responses=True)
 5 rp1 = redis.Redis(connection_pool=pool)
 6  
 7 #创建管道,可以选择开启或关闭事务,这里的事务与Redis事务一样是弱事务型
 8 pipe = rp1.pipeline(transaction=True)
 9 #在管道中添加命令
10 pipe.set('new','123')
11 pipe.set('name', 'wyk2')
12 pipe.set('company', 'csdn2')
13 pipe.hincrby('hage','wyk',1)
14  
15 #这个命令会报错,因为hage为hash类型不能使用get命令,此时无论开启关闭事务,管道中的其他命令也依然会正常执行
16 pipe.get('hage') 
17   
18 #也可以用下面的语法将多个命令拼接到一起
19 # pipe.set('name', 'wyk').set('company', 'csdn').hset('hage', 'wyk',28).hincrby('hage','wyk',1)
20   
21 #执行pipeline里的脚本
22 pipe.execute()

内存淘汰策略

策略描述
volatile-lru 设置了过期时间的key使用LRU算法淘汰;
allkeys-lru 所有key使用LRU算法淘汰;
volatile-lfu 设置了过期时间的key使用LFU算法淘汰;
allkeys-lfu 所有key使用LFU算法淘汰;
volatile-random 设置了过期时间的key使用随机淘汰;
allkeys-random 所有key使用随机淘汰;
volatile-ttl 设置了过期时间的key根据过期时间淘汰,越早过期越早淘汰;
noeviction 默认策略,当内存达到设置的最大值时,所有申请内存的操作都会报错,只读操作如get命令可以正常执行;
#配置文件
maxmemory-policy noeviction
 
#命令行
127.0.0.1:6379> config get maxmemory-policy
1) "maxmemory-policy"
2) "noeviction"
127.0.0.1:6379> config set maxmemory-policy allkeys-random
OK
127.0.0.1:6379> config get maxmemory-policy
1) "maxmemory-policy"
2) "allkeys-random"
# 重写配配置文件
127.0.0.1:6379> config rewrite

LRU算法

LRU(Least Recently Used)表示最近最少使用,该算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

新数据放在链表头部 ,链表中的数据被访问就移动到链头,链表满的时候从链表尾部移出数据。

LFU算法

LFU(Least Frequently Used)表示最不经常使用,它是根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。

LFU算法反映了一个key的热度情况,不会因LRU算法的偶尔一次被访问被误认为是热点数据

新数据放在链表尾部 ,链表中的数据按照被访问次数降序排列,访问次数相同的按最近访问时间降序排列,链表满的时候从链表尾部移出数据

LRU和LFU算法都是在Redis内存占用满的情况下的淘汰策略

过期删除策略

内存没占满时在Redis中执行过期删除策略

在Redis中过期的key不会立刻从内存中删除,而是会同时以下面两种策略进行删除:

  • 惰性删除:当key被访问时检查该key的过期时间,若已过期则删除;已过期未被访问的数据仍保持在内存中,消耗内存资源;

  • 定期删除:每隔一段时间,随机检查设置了过期的key并删除已过期的key;维护定时器消耗CPU资源;

Redis每10秒进行一次过期扫描:

  1. 随机取20个设置了过期策略的key;

  2. 检查20个key中过期时间中已过期的key并删除;

  3. 如果有超过25%的key已过期则重复第一步;

这种循环随机操作会持续到过期key可能仅占全部key的25%以下时,并且为了保证不会出现循环过多的情况,默认扫描时间不会超过25ms;

为了保证一致性,在AOF持久化模式中,当key过期时候,会同时发送DEL命令给AOF文件和所有节点;

从节点不会主动的删除过期key除非它升级为主节点或收到主节点发来的DEL命令;

主从复制

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器,主从是哨兵和集群模式能够实施的基础。前者称为主节点(master),后者称为从节点(slave),数据的复制是单向的,只能由主节点到从节点。

主从一般部署在不同机器上,复制时存在网络延时问题,使用参数repl-disable-tcp-nodelay选择是否关闭TCP_NODELAY,默认为关闭:

  • 关闭:无论数据大小都会及时同步到从节点,占带宽,适用于主从网络好的场景;

  • 开启:主节点每隔指定时间合并数据为TCP包节省带宽,默认为40毫秒同步一次,适用于网络环境复杂或带宽紧张,如跨机房;

作用

  • 数据冗余:数据的热备份,是持久化之外的一种数据冗余方式。

  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。

  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务,分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。

  • 读写分离:主库写、从库读,读写分离不仅可以提高服务器的负载能力,同时可根据需求的变化,改变从库的数量;

  • 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础。

配置从节点

# ------ 命令模式 -------
#在从服务器执行下面的命令成为或取消成为某节点的从节点
#slaveof  主服务器的IP  端口号
slaveof  host port
 
#取消成为任何服务器的从服务器
slaveof no one
 
#从服务器只读(推荐配置)
config set slave-read-only yes
 
#查看主从信息
info replication
 
#配置主节点ACL账号密码(Redis6开启ACL的情况)
config set masteruser username
?
# ------ 配置文件模式 -------
#slaveof 主节点地址 主节点端口
slaveof  host port
 
#从服务器只读(推荐配置)
slave-read-only yes

使用ACL用户同步

#命令行模式
#在从节点配置主节点ACL账号密码(Redis6开启ACL的情况)
config set masteruser default
config set masterauth wyk123456
 
#在从节点查看主节点的ACL用户密码
config get master*
 
#配置文件模式 redis.conf
#在从节点配置主节点ACL账号密码(Redis6开启ACL的情况)
masteruser default
masterauth wyk123456

主从模式

一主一从:主节点负责处理写请求,从节点负责处理读请求,主节点使用RDB持久化模式,从节点使用AOF持久化模式

一主多从:一个主节点可以有多个从节点,但每个从节点只能有一个主节点。一主多从适用于写少读多的场景,多个从节点可以分担读请求负载,提升并发

树状从:上面的一主多从可以实现读请求的负载均衡,但当从节点数量多的时候,主节点的同步压力也是线性提升的,因此可以使用树状主从来分担主节点的同步压力

复制原理

主从复制过程大体可以分为3个阶段:连接建立阶段(即准备阶段)、数据同步阶段、命令传播阶段。

在从节点执行 slaveof 命令后,复制过程便开始按下面的流程运作:

  1. 保存主节点信息:配置slaveof之后会在从节点保存主节点的信息。

  2. 主从建立socket连接:定时发现主节点以及尝试建立连接。

  3. 发送ping命令:从节点定时发送ping给主节点,主节点返回PONG。若主节点没有返回PONG或因阻塞无法响应导致超时,则主从断开,在下次定时任务时会从新ping主节点。

  4. 权限验证:若主节点开启了ACL或配置了requirepass参数,则从节点需要配置masteruser和masterauth参数才能保证主从正常连接。

  5. 同步数据集:首次连接,全量同步。

  6. 命令持续复制:全量同步完成后,保持增量同步

哨兵

哨兵(sentinel),用于对主从结构中的每一台服务器进行监控,当主节点出现故障后通过投票机制来挑选新的主节点,并且将所有的从节点连接到新的主节点上

  • 监控:监控主从节点运行情况。

  • 通知:当监控节点出现故障,哨兵之间进行通讯。

  • 自动故障转移:当监控到主节点宕机后,断开与宕机主节点连接的所有从节点,然后在从节点中选取一个作为主节点,将其他的从节点连接到这个最新的主节点。最后通知客户端最新的服务器地址。

哨兵也是一台redis服务器,只是不对外提供任何服务,redis的bin目录下的redis-sentinel其实就是redis-server的软连接。

哨兵节点最少三台且必须为单数。如果是双数,在选举的时候就会出现平票的情况,所以必须是三台及以上的单数。

配置

# ../bin/sentinel.conf
#端口
port 26379 
#后台启动
daemonize yes
#运行时PID文件
pidfile /var/run/redis-sentinel.pid 
#日志文件(绝对路径)
logfile "/opt/app/redis6/sentinel.log" 
#数据目录
dir /tmp/sentinel_26379
 
#监控的节点名字可以自定义,后边的2代表的:如果有俩个哨兵判断这个主节点挂了那这个主节点就挂了,通常设置为哨兵个数一半加一
sentinel monitor mymaster 127.0.0.1 6379 2
 
#哨兵连接主节点多长时间没有响应就代表主节点挂了,单位毫秒。默认30000毫秒,30秒。
sentinel down-after-milliseconds mymaster 30000
 
#在故障转移时,最多有多少从节点对新的主节点进行同步。这个值越小完成故障转移的时间就越长,这个值越大就意味着越多的从节点因为同步数据而暂时阻塞不可用
sentinel parallel-syncs mymaster 1
 
#在进行同步的过程中,多长时间完成算有效,单位是毫秒,默认值是180000毫秒,3分钟。
sentinel failover-timeout mymaster 180000
 
#禁止使用SENTINEL SET设置notification-script和client-reconfig-script
sentinel deny-scripts-reconfig yes

启动哨兵服务:redis-sentinel sentinel.conf,一般用一个哨兵服务监控一个节点服务

原理

哨兵之间会有通讯,哨兵和主从节点之间也有监控,基于这些信息同步和状态监控实现Redis的故障转移

  • 哨兵和哨兵之间以及哨兵和Redis主从节点之间每隔一秒发送ping监控它们的健康状态;

  • 哨兵向Redis主从节点每隔10秒发送一次info保存节点信息;

  • 哨兵向Redis主节点每隔2秒发送一次hello,直到哨兵报出sdown,代表主节点失联,然后通知其余哨兵尝试连接该主节点;

    • 主观下线(sdown):单独一个哨兵发现master故障了。 客观下线(odown):半数哨兵都认为master节点故障就会触发故障转移。

哨兵Leader选举:

一般情况下当哨兵发现主节点sdown之后,该哨兵节点会成为领导者负责处理主从节点的切换工作:

  1. 哨兵A发现Redis主节点失联;

  2. 哨兵A报出sdown,并通知其他哨兵,发送指令sentinel is-master-down-by-address-port给其余哨兵节点;

  3. 其余哨兵接收到哨兵A的指令后尝试连接Redis主节点,发现主节点确实失联;

  4. 哨兵返回信息给哨兵A,当超过半数的哨兵认为主节点下线后,状态会变成odown

  5. 最先发现主节点下线的哨兵A会成为哨兵领导者负责这次的主从节点的切换工作

故障转移:

当哨兵发现主节点下线之后经过哨兵选举机制,选举出本次故障转移工作的哨兵节点完成本次主从节点切换的工作:

  1. 哨兵Leader 根据一定规则从各个从节点中选择出一个节点升级为主节点;

  2. 其余从节点修改对应的主节点为新的主节点;

  3. 当原主节点恢复启动的时候,变为新的主节点的从节点

python访问哨兵模式的接口:from redis.sentinel import SentinelConnectionPool

集群

集群中的节点分为主节点和从节点,只有主节点负责读写请求和集群信息的维护,从节点只进行主节点数据和状态信息的复制(客户端不不能在从节点处读)(主节点可多个)

  • 数据分区:突破单机的存储限制,将数据分散到多个不同的节点存储;

  • 负载均衡:每个主节点都可以处理读写请求,提高了并发能力;

  • 高可用:集群有着和哨兵模式类似的故障转移能力,提升集群的稳定性;

原理

衡量数据分区方法的标准有两个重要因素:

  1. 是否均匀分区

  2. 增减节点对数据分布的影响;

哈希算法具有随机性,可以保证数据均匀分布

数据分区原则:一致性哈希

将hash值区间抽象为一个环形,节点均匀分布在该环形之上,然后根据数据的key计算hash值,在该hash值所在的圆环上的位置延顺时针行走找到的第一个节点的位置,该数据就放在该节点之上。相比于哈希取余,一致性哈希分区将增减节点的影响限制为相邻节点。

Redis采用的方案,在一致性哈希基础之上,引入虚拟节点的概念,虚拟节点被称为槽(slot)。Redis集群中,槽的数量为16384

  • 槽介于数据和节点之间,将节点划分为一定数量的槽,每个槽包含哈希值一定范围内的数据。由原来的hash-->node 变为 hash-->slot-->node

  • 当增删节点时,该节点所有拥有的槽会被重新分配给其他节点,可以避免在一致性哈希分区中由于某个节点的增删造成数据的严重分布不均。

通信机制

在上面的哨兵方案中,节点被分为数据节点和哨兵节点,哨兵节点也是redis服务,但只作为选举监控使用,只有数据节点会存储数据。而在Redis集群中,所有节点都是数据节点,也都参与集群的状态维护。

在Redis集群中,数据节点提供两个TCP端口,在配置防火墙时需要同时开启下面两类端口:

  • 普通端口:即客户端访问端口,如默认的6379;

  • 集群端口:普通端口号加10000,如6379的集群端口为16379,用于集群节点之间的通讯;

集群的节点之间通讯采用Gossip协议,节点根据固定频率(每秒10次)定时任务进行判断,当集群状态发生变化,如增删节点、槽状态变更时,会通过节点间通讯同步集群状态,使集群收敛。

集群间发送的Gossip消息有下面五种消息类型:

  • MEET:在节点握手阶段,对新加入的节点发送meet消息,请求新节点加入当前集群,新节点收到消息会回复PONG消息;

  • PING:节点之间互相发送ping消息,收到消息的会回复pong消息。ping消息内容包含本节点和其他节点的状态信息,以此达到状态同步;

  • PONG:pong消息包含自身的状态数据,在接收到ping或meet消息时会回复pong消息,也会主动向集群广播pong消息;

  • FAIL:当一个主节点判断另一个主节点进入fail状态时,会向集群广播这个消息,接收到的节点会保存该消息并对该fail节点做状态判断;

  • PUBLISH:当节点收到publish命令时,会先执行命令,然后向集群广播publish消息,接收到消息的节点也会执行publish命令;

访问集群
Dummy客户端

使用redis-cli客户端连接集群被称为dummy客户端,只会在执行命令之后通过MOVED错误重定向找到对应的节点,我们可以使用redis-cli -c命令进入集群命令行,当查看或设置key的时候会根据上面提到的CRC16算法计算key的hash值找到对应的槽slot,然后重定向到对应的节点之后才能操作,我们也使用cluster keyslot命令查看key所在的槽solt

#使用-c进入集群命令行模式
redis-cli -c -p 6381
 
#使用命令查看key所在的槽
cluster keyslot key1
smart客户端(Redis6)

相比于dummy客户端,smart客户端在初始化连接集群时就缓存了槽slot和节点node的对应关系, 在连接任意节点后执行cluster slots

搭建(Redis5)

三主三从的例子

#1.首先复制Redis目录出三个:A,B,C是主,AA,BB,CC分别是它们的从
cp -r /opt/app/redis6A /opt/app/redis6AA
cp -r /opt/app/redis6B /opt/app/redis6BB
cp -r /opt/app/redis6C /opt/app/redis6CC
 
#2.分别修改6个目录中的redis.conf文件,主要开启集群以及修改端口和文件路径,下面以A为演示,其余略过:
vim /opt/app/redis6A/bin/redis.conf
--------------------------------------------
port 6381
daemonize yes
pidfile "/var/run/redisA_6381.pid"
logfile "/opt/app/redis6A/redis_6381.log"      #需要手动touch文件
dir "/opt/app/redis6A/data"                    #需要手动先mkdir文件夹
cluster-enabled yes                            # 启用集群模式
cluster-node-timeout 15000                     # 设置当前节点连接超时毫秒数
cluster-config-file node_6381.conf             #设置当前节点集群配置文件路径
 
--------------------------------------------
 
#3.在6个目录下分别创建log文件和目录:
mkdir /opt/app/redis6A/data 
touch /opt/app/redis6A/redis_6381.log
?
#4.创建集群,这里的--cluster-replicas表示每个主节点有几个副本节点
../bin/redis-cli --cluster create 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6391 127.0.0.1:6392 127.0.0.1:6393 --cluster-replicas 1
?
# 查询集群的命令
../bin/redis-cli --cluster help

限制

由于Redis集群中数据分布在不同的节点上,因此有些功能会受限:

  • 集群模式下只有一个db0

  • 集群模式下只允许单层复制结构

  • 只有当操作的key都在同一个节点上才可以在集群下使用事务或lua脚本(Hash Tag可以解决)

  • 只有当操作的key都在同一个节点上才可以执行(Hash Tag可以解决)

  • 只会在该节点之上进行操作,不会对集群的其他节点进行操作

Hash Tag:

当key包含{}的时候,不会对整个key做hash,只会对{}包含的部分做hash然后分配槽slot;因此我们可以让不同的key在同一个槽内,这样就可以解决key的批量操作和事务及lua脚本的限制了;

但由于hash tag会将不同的key分配在相同的slot中,如果使用不当,会造成数据分布不均的情况,需要注意。

参数优化

cluster_node_timeout:默认值为15s。

影响ping消息接收节点的选择,值越大对延迟容忍度越高,选择的接收节点就越少,可以降低带宽,但会影响收敛速度。应该根据带宽情况和实际要求具体调整。

影响故障转移的判定,值越大越不容易误判,但完成转移所消耗的时间就越长。应根据网络情况和实际要求具体调整。

cluster-require-full-coverage

为了保证集群的完整性,只有当16384个槽slot全部分配完毕,集群才可以上线,但同时,若主节点发生故障且故障转移还未完成时,原主节点的槽不在任何节点中,集群会处于下线状态,影响客户端的使用。

该参数可以改变此设定:

  • no: 表示当槽没有完全分配时,集群仍然可以上线;

  • yes: 默认配置,只有槽完全分配,集群才可以上线;