Redis 缓存热点引发的思考
01—背景
一开始并没有打算梳理 redis 的相关内容,因为在一篇文章中看到关于热点问题的处理,心中有一些疑惑,内容如下:
缓存热点:
对于特别热的数据,如果大部分甚至所有的业务都命中同一份缓存数据,则这份数据所在缓存服务器压力就很大,例如,某明星微博发布“我们”来宣告恋爱了,则短时间内有成千上万的用户都来围观。
缓存热点解决方案:
就是复制多份缓存,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力,以新浪微博为例,对于粉丝超过 100 万的明星,每一条微博都可以生成 100 分缓存,缓存的数据都是一样的,通过缓存 key 里面加编号进行分区,每次读缓存都随机读取其中某份缓存。
解决思路是没有问题的,是通过备份相同的数据到多台缓存服务器中,缓解分散单台服务器的压力,我的疑惑是"通过缓存 key 里面加编号进行分区"这种方式是怎么确保使需要保存的数据都分散到不同的服务器呢?redis 集群根据 key 进行 hash 散列算法,最终映射的是槽位,不同的缓存服务器分别管理的是一些列表槽位,怎么保证通过 key 计算出来的槽位值正好不在同一台机器上呢?
带着这个疑问点,开始了 redis 复制,哨兵,集群内容的整理。
02—内容梳理
复制
在分布式系统中,为了解决单点问题,通常会把数据复制多个副本部署到其他的机器,满足故障恢复和负载均衡的需求。哨兵和集群模式都是在复制的基础上实现的高可用。
redis 复制拓扑极其应用
拓扑结构分为一主一从,一主多从,树状多从结构。
1. 一主一从结构
应用场景:主节点出现宕机时,从节点提供故障转移支持。
当写命令并发量较高并且需要持久化时,可以在从节点开启 AOF,这样既保证了数据的安全也避免了持久化对于主节点的影响。
注意:
当主节点关闭持久化功能,在从节点实现的时候,如果主节点脱机要避免主节点自动重启,因为主节点没有开启持久化功能,自动重启后数据集为空,这时如果从节点继续复制主节点会导致从节点数据也被清空。
解决方案:
在从节点执行 slaveof no one 断开与主节点的复制关系,再重启主节点避免这种情况。
2. 一主多从结构
利用多个节点实现读写分离,适用于读占比大的场景,把读命令发送到从节点来分担主节点压力。
日常开发中,执行一些比较耗时的读命令,可以在其中的一个从节点上进行,防止慢查询对主节点造成阻塞
3. 树状主从结构
从节点不但可以复制主节点的数据,同时可以作为其他从节点的主节点继续向下层复制。
有效的降低主节点的负载和需要传送给从节点的数据量,降低主节点压力。
复制模式的注意事项:
1. 复制的数据流是单向的,只能从主节点复制到从节点。
2. 从节点默认使用 slave-read-only=yes 配置为只读模式,对于从节点的任何修改主节点是无感知的,修改从节点会造成主从数据不一致,所以建议不要修改从节点的只读模式。
3. 读写分离面临的问题
对读占比高的场景,把一部分读流量分摊到从节点来减轻主节点压力,永远只对主节点执行写操作。
当使用从节点影响请求时,业务端可能面临的问题:
复制数据延迟
刚在主节点写入数据后立刻在从节点读取可能读取不到,需要业务场景允许短时间内数据的延迟。
当无法容忍延迟场景,可以编写外部监控程序,监听主从节点的复制偏移量,当延迟较大时触发报警或者通知客户端避免读取延迟过高的从节点。
监控程序定期检查主从节点偏移量,当延迟字节过高时,例如超过 10MB,监控程序触发报警通知客户端从节点延迟过高,客户端接收到从节点过高延迟通知后,修改读命令路由到其他的从节点或者主节点上,当延迟恢复后,再次通知客户端,恢复从节点的读命令请求。
读到过期数据
Redis 内部维护过期数据删除策略,删除策略一共有两种,惰性删除和定期删除。
惰性删除:主节点每次处理读请求命令时,都会检查键是否超时,如果超时,则执行 del 命令删除键对象,之后 del 命令也会异步发送给从节点,为了保证数据一致性,从节点本身永远不会主动删除超时数据。
如果在从节点读取数据,则获取到的数据就有可能是已经过期的数据了,因为此时读请求没有发送到主节点,主节点不会检查键是否过期也不会发送 del 命令给从节点。
定期删除:Redis 主节点内部定时任务会循环采样一定数量的键,当发现采样的键过期时执行 del 命令,之后同步给从节点。
如果此时数据大量超时,主节点采样的速度跟不上过期的速度且主节点没有读取过期键操作,那么从节点将无法收到 del 命令,这时从节点可能读取到已经超时的数据。
定时删除和惰性删除命令都会有读取到过期数据的问题,在 Rdis3.2 版本之后,从节点读取数据之前会检查键的过期时间来决定是否返回数据,规避了读取超时数据的问题。
从节点故障
对于从节点故障,需要在客户端维护可用从节点列表,当从节点故障时立刻切换到其他从节点或主节点上,类似于延迟过高的监控处理。
哨兵
redis 主从复制模式下,一旦主节点由于故障不能提供服务,需要人工将从节点晋升为主节点,同时还要通知应用方更新主节点地址,为了解决人工处理问题,Redis Sentinel(哨兵)架构解决了这个问题。
Sentinel 是 Redis 的高可用的解决方案,由一个或多个 Sentinel 实例组成 Sentinel 系统可以监视任意多个主服务器以及这些主服务器下的所有从服务器,并且在被监视的主服务器进入下线状态时,自动将下线主服务器下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。
当主节点出现故障时,Redis Sentinel 能自动完成故障发现和故障转移,并通知应用方,从而实现真正的高可用。
高可用读写分离思路:
从节点的作用:
第一,当主节点出现故障时,作为主节点的后备"顶"上来,实现故障转移,Redis Sentinel 实现了该功能的自动化,实现了真正的高可用。第二,扩展主节点的读能力,尤其是在读多写少的场景,非常适用。
上述的模型中,从节点不是高可用的,如果 slave-1 节点出现故障,首先客户端 client-1 将与其失联,其次 Sentinel 节点只会对该节点做主观下线,因为 Redis Sentinel 的故障转移时针对主节点的。
很多时候 Redis Sentinel 中的从节点仅仅是作为主节点的一个热备,不让它参与客户端的读操作,就是为了保证整体的高可用,但实际上这种使用方法还是有一些浪费,尤其是在有很多从节点或者确实需要读写分离的场景,所以如何实现从节点的高可用是非常有必要的。
设计 Redis Sentinel 从节点的高可用,只要能够掌握所有的从节点的状态,把所有的从节点看做是一个资源池,无论是上线还是下线一个从节点,客户端都能及时感知到(将其从资源池中添加或者删除),这样就实现了从节点的高可用
集群
Redis cluster 是 Redis 的分布式解决方案,当遇到单机内存,并发,流量等瓶颈时,可以采用 Cluster 架构方案达到负载均衡的目的。
数据分布理论
分布式数据库首要解决的是把整个数据集按照分区规则映射到多个节点的问题。把数据集划分到多个节点,每个节点负责整个数据的一个子集。
数据分区规则有两种:哈希分区和顺序分区两种。
哈希分区:离散度好,数据分布业务无关,无法顺序访问。
顺序分区:离散度易倾斜,数据分布业务相关,可顺序访问。
Redis Cluster 使用的是哈希分区规则,哈希分区规则有以下几种:
节点取余分区
使用特定的数据,如 Redis 的键或用户 ID,再根据节点数量 N 使用公式:
hash(key) % N 计算出哈希值,用来决定数据映射到哪一个节点。
问题:
当节点数量发送变化时,如扩容或收缩节点,数据节点映射关系需要重新计算,会导致数据的重新迁移。
使用场景:
常用于数据库的分库分表规则,一般采用预分区的方式,提前根据数据量规划好分区数,比如划分 512 或 1024 张表,保证支撑未来一段时间的数据量,再根据负载情况将表迁移到其他的数据库中。
扩容时通常采用翻倍扩容,避免数据映射全部打乱导致全量迁移的情况。
一致性哈希分区
优点:相比于节点取余的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他的节点没有影响。
问题:
加减节点会造成哈希环中部分数据无法命中,需要手动处理或忽略这部分数据,所以一致性哈希常用于缓存场景。
当使用少量节点时,节点变化将大范围影响哈希环中数据映射。一致性哈希不适合少量数据节点的分布式方案。
普通的一致性哈希分区在增减节点时,需要增加一倍或者减少一半的节点才能够保证数据和负载的均衡。
虚拟槽分区
虚拟槽分区巧妙的使用了哈希空间,使用分散度良好的哈希函数把所有的数据映射到一个固定范围的整数集合中,整数定义为槽(slot),整个范围一般远远大于节点数量,Redis Cluster 槽范围是 0-163843,槽是集群内数据管理和迁移的基本单位,采用大范围槽的主要目的是为了方便数据拆分和集群扩展。
每一个节点会负责一定数量的槽。
Redis Cluster 使用的虚拟槽分区,所有的键根据哈希函数映射到 0-16383 整数槽内,计算公式:slot=CRC16(key) & 16383,每一个节点负责维护一部分槽以及槽所映射的键值数据。
Redis 虚拟槽分区的特点:
解耦数据和节点之间的关系,简化了节点扩容和缩容的难度。
节点自身维护槽的映射关系,不需要客户端或代理服务器维护槽分区元数据。
支持节点,槽,键之间的映射查询,用于数据路由,在线伸缩等场景。
集群功能的限制:
key 批量操作支持有限,如 mset,mget 目前只支持具有相同 slot 值的 key 执行批量操作(相同的槽位 slot 可能会存放多个 key,根据公式可知:slot=CRC16(key) & 16383)。
key 事务操作支持有限,只支持多 key 在同一个节点上的事务操作。
key 作为数据分区的最小粒度,不能将一个大的键值对象如 hash,list 等映射到不同的节点。
单机下 Redis 可以支持 16 个数据库,集群模式下只能使用一个数据库空间,即 db0。
复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。
集群伸缩原理:
在不影响集群对外提供服务的情况下,可以为集群添加节点进行扩容,也可以下线部分节点进行缩容
Redis 集群对节点灵活上下线控制,其原理可抽象为槽和对应的数据在不同节点之间灵活的移动。
三个节点分别维护自己负责的槽和对应的数据,如果希望加入一个节点实现集群扩容时,需要通过相关命令把一部分槽和数据迁移到新节点。
图中每一个节点把槽和数据迁移到新节点 6385 上,每个节点负责的槽和数据相比之前变少了,从而达到了集群扩容的目的。
集群水平伸缩的上层原理:集群伸缩=槽和数据在节点之间的移动。
03—解决方案
再回到开始自己的疑问,我理解我自己的想法是没有错的,不能够完全保证可以通过 key 来进行分区,因为生成的 key 通过 hash 算法最终获取到的是一个槽位,并不能确保新生成的槽位分配在不同的机器上,一般情况下每一个节点对应的是多个槽位。
在已知热点 key 的情况下推荐使用内存存放热 key 来解决。
针对这种热 key 请求,会直接从 jvm 中获取,避免走 redis 层,假设此时有 10 万请求针对同一个 key 请求过来,如果没有本地缓存,这十万个请求直接到了 redis,会将 redis 服务器弄挂,如果应用服务器有 50 台,同时热数据在 jvm 中已经存储了,这十万请求可以平均分散开来,每个机器 2000 请求,直接从 jvm 中取值,然后返回数据
为什么阿里巴巴的程序员成长速度这么快,看完他们的内部资料我懂了
看完三件事❤️
如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
关注公众号 『 Java 斗帝 』,不定期分享原创知识。
同时可以期待后续文章 ing🚀
评论