
Redis进阶
这篇文章主要介绍了Redis的一些内存管理、优化以及Redis事务、Redis集群与高可用性,通过这些高级用法和策略,可以高效地管理 Redis 的数据存储,保证其性能和可靠性。
上一篇介绍Redis的内容中着重讲述了Redis的数据结构以及功能,了解了Redis的持久化机制以及Redis为什么这么快,这一篇博客将着重介绍Redis中的一些更加底层的构造以及更高级的用法。
如果Redis的内存达到设置的上限,Redis的写命令会返回错误信息,读命令还能够正常返回。这个时候我们可以配置内存淘汰机制,当Redis达到内存上限的时候冲刷掉旧的内容。
一般而言我们会使用expire和persist命令来对键的过期时间进行处理。
在单机版本的Redis中,主要存在两种删除的策略:
在主从复制的场景下,为了主从节点的数据一致性,从节点从来不会主动删除数据,而是由主节点控制节点中过期数据的删除。由于主节点的惰性删除和定期删除策略,都无法保证主节点及时对过期数据执行删除操作。因此当客户端通过Redis从节点读取数据的时候,很容易读取到已经过期的数据。而在Redis 3.2版本以后,从节点在读取数据的时候增加了对数据是否过期的判断。如果数据已经过期就不会返回给客户端。
Redis一共提供了八种淘汰的策略:
主要可以分为三种类型:
其一是不淘汰:
其二是针对所有键淘汰:
其三是针对设置了过期时间的键的淘汰策略:
Redis事务的本质就是一组命令的集合。事务支持一次执行多个命令,一个事务中的所有命令都会被序列化。在执行事务的过程中会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
MULTI:开启事务,Redis会将后续的命令逐个放入到队列中,然后使用EXEC命令来原子化执行该命令序列。EXEC:执行事务中的所有操作命令。DISCARD:取消事务,放弃执行事务块中的所有命令。WATCH:监视一个或多个KEY,如果事务在执行前,KEY被其他命令修改,事务就会被终端,不再指定事务中的任何命令。多个客户端使用WATCH可以保证事务的安全性。如有两个客户端执行如下命令,如果一个客户端发出的指令先执行了,另一个客户端就不会再执行。UNWATCH:取消WATCH中对KEY的监视。MULTI、EXEC等命令实现。当一个客户端切换到事务状态之后,服务器就会根据这个客户端发送的不同命令来执行不同的操作:如果客户端发送的是EXEC、DISCARD、WATCH、MULTI这四个命令之中的一个,那么服务器就会立即执行该命令,否则就会将命令放入事务队列中,并且向客户端发送QUEUED回复。Redis其实并没有回滚支持。Redis命令通常只会因为错误的语法而失败,或是命令用在了错误类型的键上面,也就是说从实用性的角度来说,失败的命令是由于变成编程错误造成的,而这些错误都应该在开发的过程中被发现,而不是出现在生产环境中。并且因为并不需要对回滚提供支持,所以Redis的内部可以保持简单和快速。
Redis集群是为了解决Redis单机容量有限、高并发场景下的性能瓶颈以及单点故障问题而提出的解决方案。它将数据分散在存储多个Redis节点上,实现了数据的分布式存储和高可用性。简单来说就是将多个Redis实例组织起来,共同对外提供服务。主要有三种模式:主从复制模式(Master-Slave)、哨兵模式(Sentinel)、Cluster模式。
主从复制,就是将一台Redis服务器的数据复制到其他的Redis服务器,前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能从主节点到从节点。
主从复制的作用主要包括:
主从库之间采取的是读写分离的模式:
读操作主库和从库都能够接收,而写操作则首先到主库执行,而后主库将写操作同步给从库。同步的方式主要有全量复制(第一次同步时)和增量复制(只会把主从库网络断连期间主库收到的命令同步给从库)。
全量复制的过程主要分为三个阶段:
psync命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync命令包含了主库的runID(每个Redis实例启动时自动生成,用于唯一标记该实例)和复制进度offset两个参数。当从库和主库第一次复制的时候,由于不知道主库的runID,所以暂时将其设定为“?”,而offset则设定为-1,表示第一次复制。主库收到psync命令之后,会使用fullresync响应命令并且带上主库的runID和主库当前的复制进度offset,返回给从库。从库在收到命令之后会记录下这两个参数。fullresync响应表示第一次采用的是全量复制。在Redis2.8之前,如果主从库在命令传播的时候出现了网络闪断,那么从库就会和主库重新进行一次全量复制,这会导致极大的开销。在Redis 2.8之后,网络断开并且重新连接之后,主从库会采用增量复制的方式继续同步。
增量复制主要涉及到两个概念:replication_buffer以及repl_backlog_buffer。
每个从库都有其独特的slave_replication_offset。当replication_backlog_size的环形缓冲区被填满,主库上对应slave_replication_offset位置的数据被覆盖后,从库将执行全量复制。如果数据未被覆盖,则执行增量复制。
当从库的数量比较多且都要和主库进行全量复制的话,就会导致主库忙于fork子进程生成RDB文件,进行数据全量复制,这个过程会对主线程造成阻塞,主线程无法处理正常请求。同时传输RDB文件也会占用主库的网络带宽,给主库的资源带来使用压力。这个时候可以通过“主-从-从”模式来分担主库的压力,将主库生成RDB和传输RDB的压力以级联的方式分散到从库上,即在部署主从集群的时候手动选择一个从库,用于级联其他的从库,然后再选择一些从库,使其和所选择的级联从库建立起主从同步,这样在执行同步的时候,从库就不再直接和主库进行交互了,而是和级联的从库进行写操作同步即可,减轻了主库的压力。
此外,Redis 还支持无磁盘复制模式,在此模式下,子进程会直接通过网络将 RDB 文件发送到服务器,而不是使用磁盘作为中转。这适用于磁盘速度慢而网络速度快的环境。
哨兵的核心功能是实现主节点的自动故障转移。
哨兵主要实现了以下功能:
哨兵集群主要是通过Redis的pub/sub机制实现的,即发布/订阅机制。在主从集群中,主库上会有一个名为__sentinel__:hello的频道,不同的哨兵就是通过其来相互发现从而实现互相通信的。哨兵通过把自己的ip和端口发布到该频道上,其他哨兵可以通过该频道获取,而后建立网络连接。
哨兵通过向主库发送INFO命令来监控Redis集群,主库接收到INFO命令之后,就会把从库列表返回给哨兵。接着哨兵就可以根据从库列表中的连接信息,和每一个从库建立连接,并且在这个连接上持续地对从库进行监控。
下线的方式主要有两种:主观下线和客观下线。
is-master-down-by-addr命令,而后其他哨兵就会根据自己和主库的连接情况,做出Y或者N的响应。quorum配置项,就认为主库客观下线了。当主库判断客观下线之后,会在过滤掉不健康的、没有回复过哨兵ping响应的从节点之后,选择salve-priority从节点优先级最高的以及复制偏移量最大的、复制最完整的从节点。
为了避免哨兵的单点故障,需要一个哨兵的分布式集群。分布式集群就必然涉及到共识问题,同时故障的转移和通知都只需要一个主哨兵节点即可。
哨兵的选举机制主要采用的是Raft选举算法:选举的票数大于等于nums(sentinel)/2 + 1的时候就能够成为领导者,如果没有超过的话就会继续选举。即任何一个想要成为Leader的哨兵,要满足两个条件:拿到半数以上的赞成票;拿到的票数要大于等于哨兵配置文件中的quorum值。
以Redis哨兵的图为例,假设判断主库客观下线了,同时选出了sentinel3为哨兵leader。那么此时哨兵集群就会选择一个最完整的节点,假设此时选出了slave-1为主节点,那么salve-1此时会执行slave of no one解除从节点的身份,变为新的master,slave-2会变成新的主节点的从节点,如果原主节点恢复也会变为新的主节点的从节点,最后通知应用程序新的主节点的地址。
Redis Cluster 通过分布式的方式将数据分布到多个节点上,能够自动地进行分片(Sharding)、容错处理和数据复制。它的设计目标是使 Redis 在集群模式下支持更多的数据量、负载均衡,并能保证高可用性和自动故障恢复。
Redis Cluster由多个Redis节点组成,和其他的方式一样,也分为主节点和从节点。但是其他的不一样的是,Redis Cluster的数据分布机制基于哈希槽,整个集群包含16384个哈希槽,这个数字的选择有其深层的原因:Redis Cluster的心跳包机制会定期发送集群状态信息,帮助集群中的节点保持同步。在心跳包中,Redis需要传递当前集群中每个节点所负责的哈希槽的状态。由于有16384个哈希槽,这些槽信息可以通过bitmap 压缩为一个 2KB 的二进制位图进行传输,从而显著减少网络传输的开销。具体而言,使用1位来表示每个哈希槽的状态(如节点是否持有该槽),共需 16384 位(即 2KB)。相较于如果选择更多的哈希槽(比如 65535 个槽,对应 65535 个哈希槽,需要使用 8KB 的心跳包),16384 个哈希槽使得心跳包的大小保持在一个合适的范围内,减少了网络负载。。每一个键都会根据哈希函数计算出一个哈希值,并且将该哈希值映射到16384哈希槽中的某一个槽,每个节点负责一部分哈希槽。客户端通过计算键的哈希值并且找到对应的哈希槽,将请求发送到对应的节点。
在使用Redis作为缓存的时候,我们需要考虑以下问题:
缓存穿透是指缓存和数据库中都不存在的数据,而用户不断发起请求,由于缓存是没有命中的时候被动写的,并且出于容错考虑,如果从存储层没有查询到数据就不写入缓存,这就导致了这个不存在的数据每次都要到存储层去查询,失去了缓存的意义,增大了存储层的负担。
解决方案:
缓存击穿是指当请求的数据在缓存中不存在时,缓存无法命中,而数据直接查询数据库,且在查询数据库时,数据是合法且存在的。在这种情况下,虽然缓存可以有效存储数据,但是由于缓存的过期、淘汰等原因,或者缓存未命中,导致每次查询都直接从数据库获取数据,从而给数据库带来较大的负担。
解决方案:
缓存雪崩是指缓存系统中,多个缓存项同时失效,导致大量请求直接打到数据库上,造成数据库压力激增,甚至可能导致数据库宕机或性能严重下降,通常这种情况发生在缓存的失效时间是相同或者非常接近的,或者缓存的内容批量失效时,所有缓存都会同时失效并且所有请求一并访问数据库,导致系统的负载急剧增加。
解决方案:
参考资料:
Java 全栈知识点问题汇总(上) | Java 全栈知识体系
Redis cluster specification | Docs
掌握 Redis 缓存 - 您需要了解的一切 [2024] --- Master Redis Cache - Everything You Need To Know [2024]