Redis(2):性能优化、集群
1 Redis事务
Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。
-
不满足原子性和持久性,事务中的每条命令都会与 Redis 服务器进行网络交互
-
Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 事务是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性的。
-
AOF 持久化的
fsync
策略为 no、everysec 时都会存在数据丢失的情况 。always 下可以基本是可以满足持久性要求的,但性能太差,实际开发过程中不会使用。因此,Redis 事务的持久性也是没办法保证的。
除了不满足原子性和持久性之外,事务中的每条命令都会与 Redis 服务器进行网络交互,这是比较浪费资源的行为。明明一次批量执行多个命令就可以了,这种操作实在是看不懂。
Redis 可以通过 MULTI
,EXEC
,DISCARD
和 WATCH
等命令来实现事务(Transaction)功能。
MULTI
命令后可以输入多个命令,Redis 不会立即执行这些命令,而是将它们放到队列,当调用了 EXEC
命令后,再执行所有的命令
这个过程是这样的:
-
开始事务(
MULTI
); -
命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行);
-
执行事务(
EXEC
)。
你也可以通过 DISCARD
命令取消一个事务,它会清空事务队列中保存的所有命令。
2 Redis阻塞
-
O(n)命令
-
SAVE 创建 RDB 快照
-
AOF刷盘阻塞
-
AOF重写阻塞
-
删除大key
3 Redis性能优化
3.1 使用批量操作减少网络传输
-
发送命令
-
命令排队
-
命令执行
-
返回结果其中,第 1 步和第 4 步耗费时间之和称为 Round Trip Time (RTT,往返时间) ,也就是数据在网络上传输的时间。
使用批量操作可以减少网络传输次数,进而有效减小网络开销,大幅减少 RTT。
-
原生批量操作:Redis原生批量操作命令
-
pipeline:将一批 Redis 命令封装成一组,这些 Redis 命令会被一次性提交到 Redis 服务器,只需要一次网络传输
-
Lua脚本:一段 Lua 脚本可以视作一条命令执行,可以看作是 原子操作 。
3.2 大量key集中过期问题
定期任务线程在Redis主线程中执行,在遇到大量过期key时,客户端需等待定期清理过期key任务线程执行完成。导致客户端请求没办法被及时处理,响应速度会比较慢。
-
key设置随机过期时间
-
开启惰性删除,异步删除
3.3 Bigkey
key对应的value所占用的内存比较大,或者元素比较多
-
分割 bigkey:将一个 bigkey 分割为多个小 key。例如,将一个含有上万字段数量的 Hash 按照一定策略(比如二次哈希)拆分为多个 Hash。
-
手动清理:Redis 4.0+ 可以使用
UNLINK
命令来异步删除一个或多个指定的 key。Redis 4.0 以下可以考虑使用SCAN
命令结合DEL
命令来分批次删除。DEL
命令删除会阻塞主线程。 -
采用合适的数据结构:例如,文件二进制数据不使用 String 保存、使用 HyperLogLog 统计页面 UV、Bitmap 保存状态信息(0/1)。
-
开启 lazy-free(惰性删除/延迟释放) :lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
3.3.1 影响
-
客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
-
引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
-
阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
-
内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大。
3.3.2 解决办法
-
分批次删除
-
异步删除(Redis 4.0版本以上)
3.4 Hotkey(–hotkeys)
一个key的访问次数比较多并且明显多于其他key
hotkey 出现的原因主要是某个热点数据访问量暴增,如重大的热搜事件、参与秒杀的商品。
-
读写分离:主节点处理写请求,从节点处理读请求。
-
使用 Redis Cluster:将热点数据分散存储在多个 Redis 节点上。
-
二级缓存:hotkey 采用二级缓存的方式进行处理,将 hotkey 存放一份到 JVM 本地内存中(可以用 Caffeine)。
3.5 慢查询命令
Redis 慢查询统计的是命令执行这一步骤的耗时,慢查询命令也就是那些命令执行时间较长的命令。
-
发送命令
-
命令排队
-
命令执行
-
返回结果
在 redis.conf
文件中,我们可以使用 slowlog-log-slower-than
参数设置耗时命令的阈值,并使用 slowlog-max-len
参数设置耗时命令的最大记录条数。
当 Redis 服务器检测到执行时间超过 slowlog-log-slower-than
阈值的命令时,就会将该命令记录在慢查询日志(slow log) 中,这点和 MySQL 记录慢查询语句类似。当慢查询日志超过设定的最大记录条数之后,Redis 会把最早的执行命令依次舍弃。
4 多机数据库(集群,高可用)
要想设计一个高可用的 Redis 服务,一定要从 Redis 的多服务节点来考虑,比如 Redis 的主从复制、哨兵模式、切片集群。
4.1 主从复制
主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令。
复制功能分为同步sync和命令传播(command propagate)两个操作
-
同步操作由从服务器请求,将数据库状态更新至主服务器数据库状态
-
命令传播:
4.1.1 同步(全量复制)
-
从服务器向主服务器发送SYNC命令
-
主服务器收到命令,执行BGSAVE命令,再后台生成RDB文件,并将之后执行的命令记录到缓冲区
-
从服务器收到并载入RDB文件,将自己的数据库状态更新至主服务器执行BGSAVE 命令时的数据库状态
-
主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令
4.1.2 基于长连接的命令传播
写命令会造成服务器状态不一致,主服务器将执行的写命令发送给从服务器执行
旧版复制功能再主从服务器断联,并重新连接后,会再次执行SYNC命令,而不仅仅是将断联期间的命令传播给从服务器,要进行一次完整重同步。
PSYNC命令具有完整重同步( full resynchronization) 和部分重同步(partial resynchronization)两种模式:
-
完整重同步用于初次复制
-
部分重同步用于断线重连
执行SYNC 命令需要生成、传送和载入整个RDB 文件, 而部分重同步只需要将从服务器缺少的写命令发送给从服务器执行就可以了。
4.1.3 部分重同步(增量复制)
-
主服务器的复制偏移量和从服务器的复制偏移量
-
主服务器的复制积压缓冲区(缓冲区会记录复制偏移量,从服务器断线重连,如果从缓冲区取得缺失命令,执行部分重同步,否则执行完整重同步)
-
服务器运行ID(标识主服务器,从服务器重连后连接另一个主服务器,则执行完整重同步)
-
当主服务器的写入速度远超于从服务器的读取速度,缓冲区的数据一下就会被覆盖。此时需要进行全量同步。增量复制就与
repl_backlog_size
大小有关系,如果它配置的过小,主从服务器网络恢复时,可能发生「从服务器」想读的数据已经被覆盖了,那么这时就会导致主服务器采用全量复制的方式。所以为了避免这种情况的频繁发生,要调大这个参数的值,以降低主从服务器断开后全量同步的概率。
4.1.4 心跳检测
命令传播截断。从服务器向主服务器发送心跳
-
检测主从服务器的网络连接状态:主服务器可以知道主从连接是否正常
-
辅助实现m i n-slaves 选项。
-
检测命令丢失:主服务器命令传播丢失,通过心跳的偏移量可以检测到这一问题
主服务器通过向从服务器传播命令来更新从服务器的状态,保持主从服务器一致,而从服务器则通过向主服务器发送命令来进行心跳检测,以及命令丢失检测。
-
Redis 主节点默认每隔 10 秒对从节点发送 ping 命令,判断从节点的存活性和连接状态,可通过参数repl-ping-slave-period控制发送频率。
-
Redis 从节点每隔 1 秒发送 replconf ack{offset} 命令,给主节点上报自身当前的复制偏移量,目的是为了:
- 实时监测主从节点网络状态;
- 上报自身复制偏移量, 检查复制数据是否丢失, 如果从节点数据丢失, 再从主节点的复制缓冲区中拉取丢失数据。
4.1.5 数据丢失
5.1.5.1 异步复制同步丢失
主节点将写请求同步给从节点时宕机
Redis 配置里有一个参数 min-slaves-max-lag,表示一旦所有的从节点数据复制和同步的延迟都超过了 min-slaves-max-lag 定义的值,那么主节点就会拒绝接收任何请求。
那么对于客户端,当客户端发现 master 不可写后,我们可以采取降级措施,将数据暂时写入本地缓存和磁盘中,在一段时间(等 master 恢复正常)后重新写入 master 来保证数据不丢失,也可以将数据写入 kafka 消息队列,等 master 恢复正常,再隔一段时间去消费 kafka 中的数据,让将数据重新写入 master 。
5.1.5.2 集群产生脑裂数据丢失
由于网络问题,集群节点之间失去联系。主从数据不同步;重新平衡选举,产生两个主服务。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,由于会从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。
4.1.6 主从故障切换
Redis 哨兵机制就登场了,哨兵在发现主节点出现故障时,由哨兵自动完成故障发现和故障转移,并通知给应用方,从而实现高可用性。
4.2 哨兵模式(Sentinel,高可用)
选主、故障转移、通知
实现主从节点故障转移。它会监测主节点是否存活,如果发现主节点挂了,它就会选举一个从节点切换为主节点,并且把新主节点的相关信息通知给从节点和客户端。
4.2.1 判断主节点故障
哨兵会每隔 1 秒给所有主从节点发送 PING 命令,当主从节点收到 PING 命令后,会发送一个响应命令给哨兵,这样就可以判断它们是否在正常运行。
如果主节点或者从节点没有在规定的时间内响应哨兵的 PING 命令,哨兵就会将它们标记为「主观下线」。这个「规定的时间」是配置项 down-after-milliseconds
参数设定的,单位是毫秒。
哨兵部署多个节点,构成哨兵集群,降低误判概率。当一个哨兵判断主节点为「主观下线」后,就会向其他哨兵发起命令,其他哨兵收到这个命令后,就会根据自身和主节点的网络状况,做出赞成投票或者拒绝投票的响应。当超过半数的哨兵判断主节点主观下线后,则主节点客观下线。
4.2.2 选举领头Sentinel
当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个Sentinel 会进行协商,选举出一个领头Sentinel , 并由领头Sen tinel 对下线主服务器执行故障转移操作。
4.2.3 故障转移
-
在已下线主服务器属下的所有从服务器里面,挑选出一个从服务器,并将其转换为主服务器。
-
让已下线主服务器属下的所有从服务器改为复制新的主服务器。
-
将已下线主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器。
-
选出新主节点
-
将从节点指向新主节点
-
通过Redis的发布者/订阅者机制通知客户的主节点已更换
-
将旧主节点变为从节点
4.3 切片集群
clusterState.slots数组记录了集群中所有槽的指派信息,而clusterNode.slots数组只记录了clusterNode结构所代表的节点的槽指派信息,这是两个slots数组的关键区别所在。
当 Redis 缓存数据量大到一台服务器无法缓存时,就需要使用 Redis 切片集群(Redis Cluster )方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。
Redis Cluster 方案采用哈希槽(Hash Slot),来处理数据和节点之间的映射关系。在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中,具体执行过程分为两大步:
-
根据键值对的 key,按照 CRC16 算法 (opens new window)计算一个 16 bit 的值。
-
再用 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。
接下来的问题就是,这些哈希槽怎么被映射到具体的 Redis 节点上的呢?有两种方案:
-
平均分配: 在使用 cluster create 命令创建 Redis 集群时,Redis 会自动把所有哈希槽平均分布到集群节点上。比如集群中有 9 个节点,则每个节点上槽的个数为 16384/9 个。
-
手动分配: 可以使用 cluster meet 命令手动建立节点间的连接,组成集群,再使用 cluster addslots 命令,指定每个节点上的哈希槽个数。
$$
\begin{aligned}
\frac{16384}{8}=2048B=2KB\
2^{16}=65536=16384\times 4
\end{aligned}
$$
Redis Cluster没有使用一致性哈希,采用的是哈希槽分区,每一个键值对都属于一个 hash slot(哈希槽) 。当客户端发送命令请求的时候,需要先根据 key 通过上面的计算公示找到的对应的哈希槽,然后再查询哈希槽和节点的映射关系,即可找到目标 Redis 节点。
5 参考
主从复制是怎么实现的? | 小林coding (xiaolincoding.com)
为什么要有哨兵? | 小林coding (xiaolincoding.com)
【原创】为什么Redis集群有16384个槽 - 孤独烟 - 博客园 (cnblogs.com)
美团二面:说说redis主从的脑裂行为 (qq.com)
Redis Sentinel:
-
什么是 Sentinel? 有什么用?
-
Sentinel 如何检测节点是否下线?主观下线与客观下线的区别?
-
Sentinel 是如何实现故障转移的?
-
为什么建议部署多个 sentinel 节点(哨兵集群)?
-
Sentinel 如何选择出新的 master(选举机制)?
-
如何从 Sentinel 集群中选择出 Leader ?
-
Sentinel 可以防止脑裂吗?
Redis Cluster:
-
为什么需要 Redis Cluster?解决了什么问题?有什么优势?
-
Redis Cluster 是如何分片的?
-
为什么 Redis Cluster 的哈希槽是 16384 个?
-
如何确定给定 key 的应该分布到哪个哈希槽中?
-
Redis Cluster 支持重新分配哈希槽吗?
-
Redis Cluster 扩容缩容期间可以提供服务吗?
-
Redis Cluster 中的节点是怎么进行通信的?