导图社区 大厂SSPoffer 复习资料 - Redis
博主获取多个大厂包括 腾讯,字节,B站,超参数等ssp的redis复习资料,希望这份资料能够为求职者提供一个全面的学习框架,帮助大家面试中脱颖而出,成功获得心仪的offer。
编辑于2024-12-08 18:00:29博主获取多个大厂包括 腾讯,字节,B站,超参数等ssp的mysql复习资料,希望这份资料能够为求职者提供一个全面的学习框架,帮助大家面试中脱颖而出,成功获得心仪的offer。
博主获取多个大厂包括 腾讯,字节,B站,超参数等ssp的redis复习资料,希望这份资料能够为求职者提供一个全面的学习框架,帮助大家面试中脱颖而出,成功获得心仪的offer。
博主获取大厂 腾讯,美团,字节,B站,超参数等大厂ssp的计算机网络复习资料,希望这份资料能够为求职者提供一个全面的学习框架,帮助大家面试中脱颖而出,成功获得心仪的offer。
社区模板帮助中心,点此进入>>
博主获取多个大厂包括 腾讯,字节,B站,超参数等ssp的mysql复习资料,希望这份资料能够为求职者提供一个全面的学习框架,帮助大家面试中脱颖而出,成功获得心仪的offer。
博主获取多个大厂包括 腾讯,字节,B站,超参数等ssp的redis复习资料,希望这份资料能够为求职者提供一个全面的学习框架,帮助大家面试中脱颖而出,成功获得心仪的offer。
博主获取大厂 腾讯,美团,字节,B站,超参数等大厂ssp的计算机网络复习资料,希望这份资料能够为求职者提供一个全面的学习框架,帮助大家面试中脱颖而出,成功获得心仪的offer。
Redis
Redis是什么
Redis 是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景。
Redis 提供了多种数据类型来支持不同的业务场景,并对数据类型的操作是原子性的,执行命令为单线程,不存在并发竞争
Redis 和 Memcached
Redis 支持的数据类型更丰富(String、Hash、List、Set、ZSet),而 Memcached 只支持最简单的 key-value 数据类型;
Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 没有持久化功能,数据全部存在内存之中,Memcached 重启或者挂掉后,数据就没了;
Redis 原生支持集群模式,Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;
Redis作为Mysql的缓存
高性能
用户第一次访问 MySQL 中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据缓存在 Redis 中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了,操作 Redis 缓存
高并发
Redis 的 QPS(Query Per Second,每秒钟处理完请求的次数) 是 MySQL 的 10 倍
查询过程
为什么用Redis
加速数据读取
缓存热点数据:Redis 作为一个高性能的内存数据库,非常适合用来缓存经常被查询的数据(热点数据)。这样可以显著减少对 MySQL 数据库的直接访问,从而提高应用的响应速度。
降低数据库负载:通过将读操作卸载到 Redis,可以显著减轻 MySQL 的负载,尤其是在读多写少的应用场景中。
实现复杂数据结构
支持复杂的数据结构:Redis 支持诸如列表、集合、哈希表等复杂的数据结构,这些在传统的关系型数据库中难以高效实现。这使得在某些特定的用例(如实时计数、排行榜、会话存储等)中,Redis 比 MySQL 更加高效。
提高可扩展性和高可用性
易于水平扩展:Redis 的数据模型使其更容易进行水平扩展。这对于需要处理大量数据和高流量的应用尤其重要。
支持高可用性和持久化:使用 Redis 集群可以实现高可用性。Redis 还支持数据持久化,确保数据安全。
实现快速会话存储
会话缓存:在 Web 应用中,Redis 常被用来存储会话数据,因为它提供了比基于数据库的会话存储更快的访问速度。
支持发布/订阅模式
消息队列和实时消息传递:Redis 的发布/订阅模式支持消息队列和实时消息传递功能,这在 MySQL 中不是原生支持的。
Redis 数据结构
String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
String 类型的底层的数据结构实现主要是 SDS(简单动态字符串)。 SDS 和我们认识的 C 字符串不太一样,之所以没有使用 C 语言的字符串表示,因为 SDS 相比于 C 的原生字符串:
SDS 不仅可以保存文本数据,还可以保存二进制数据。因为 SDS 使用 len 属性的值而不是空字符来判断字符串是否结束,并且 SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在 buf[] 数组里的数据。所以 SDS 不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据。
SDS 获取字符串长度的时间复杂度是 O(1)。因为 C 语言的字符串并不记录自身长度,所以获取长度的复杂度为 O(n);而 SDS 结构里用 len 属性记录了字符串长度,所以复杂度为 O(1)。
Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。因为 SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。
List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。
以前有双向链表构成或者压缩列表
现在有quicklist构成,一个有ziplist组成的双向链表
Hash 类型:缓存对象、购物车等。
如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;
如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的底层数据结构。
Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;
如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。
Zset 类型:排序场景,比如排行榜、电话和姓名排序等。
Zset 类型的底层数据结构是由压缩列表或跳表实现的:
如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
tot-bytes: 整个结构的字节数量,包括头部以及尾部,占4个字节。
num-elements:元素的数量,占2个字节,最大表示65535个,超过则需要遍历获取长度。
entry-N:具体的每个元素。
0xFF:结尾标志,占1个字节,全是1。
它的特点就是用一块连续的内存空间来紧凑地保存数据,同时为了节省内存空间,listpack 列表项使用了多种编码方式,来表示不同长度的数据,这些数据包括整数和字符串。
后续增加的四种
BitMap(2.2 版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
HyperLogLog(2.8 版新增):海量数据基数统计的场景,比如百万级网页 UV 计数等;
GEO(3.2 版新增):存储地理位置信息的场景,比如滴滴叫车;
Stream(5.0 版新增):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。
Redis为什么不用B+树而是跳表?
B+树在数据写入时,存在拆分和合并数据页的开销,目的是为了保持树的平衡。
跳表在数据写入时,只需要通过随机函数生成当前节点的层数即可,然后更新每一层索引,往其中加入一个节点,相比于B+ tree而言,少了旋转平衡带来的开销。
由于跳表的查询复杂度在O(logn),因此redis中zset数据类型底层结合使用skiplist和hash,用空间换时间,利用跳表支持范围查询和有序查询,利用hash支持精确查询。
Redis线程模式
Redis单线程那么快
Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了;
Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。
Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,
Redis初始化
首先,调用 epoll_create() 创建一个 epoll 对象和调用 socket() 创建一个服务端 socket
然后,调用 bind() 绑定端口和调用 listen() 监听该 socket;
然后,将调用 epoll_ctl() 将 listen socket 加入到 epoll,同时注册「连接事件」处理函数。
初始化之后的事件循环
首先,先调用处理发送队列函数,看是发送队列里是否有任务,如果有发送任务,则通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。
接着,调用 epoll_wait 函数等待事件的到来:
如果是连接事件到来,则会调用连接事件处理函数,该函数会做这些事情:调用 accpet 获取已连接的 socket -> 调用 epoll_ctl 将已连接的 socket 加入到 epoll -> 注册「读事件」处理函数;
如果是读事件到来,则会调用读事件处理函数,该函数会做这些事情:调用 read 获取客户端发送的数据 -> 解析命令 -> 处理命令 -> 将客户端对象添加到发送队列 -> 将执行结果写到发送缓存区等待发送;
如果是写事件到来,则会调用写事件处理函数,该函数会做这些事情:通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会继续注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。
引入多线程是为了处理多个网络IO,这样可以提高网络处理并发度
对于命令执行Redis使用的还是单线程
Redis持久化
AOF 日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里;
写回策略
AOF过大触发AOF重写
RDB 快照:将某一时刻的内存数据,以二进制的方式写入磁盘;
RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。
这里提一点,Redis 的快照是全量快照,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。所以执行快照是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响。如果频率太低,服务器故障时,丢失的数据会更多
产生RDB快照
执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程;
执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞
混合持久化方式:Redis 4.0 新增的方式,集成了 AOF 和 RBD 的优点;
RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。
AOF 优点是丢失数据少,但是数据恢复不快。
混合持久化工作在 AOF 日志重写过程
AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
大Key的影响
主要是对持久化的影响
产生的问题
AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 fork() 函数创建一个子进程来处理任务。 在创建子进程的过程中,操作系统会把父进程的「页表」复制一份给子进程,这个页表记录着虚拟地址和物理地址映射关系,而不会复制物理内存,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。
创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
创建完子进程后,如果子进程或者父进程修改了共享数据,就会发生写时复制,这期间会拷贝物理内存,如果内存越大,自然阻塞的时间也越长;
写时复制(Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变
客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大
解决方法
将大key拆成小key
定时检查 Redis 是否存在大 key ,如果该大 key 是可以删除的,不要使用 DEL 命令删除,因为该命令删除过程会阻塞主线程,而是用 unlink 命令(Redis 4.0+)删除大 key,因为该命令的删除过程是异步的,不会阻塞主线程。
热Key的问题
请求分散化
多级缓存(Multi-level Cache)
:将热键数据在多个缓存层级进行缓存。例如,在应用程序层引入本地缓存(如Guava Cache),减少对远程缓存(如Redis)的请求。
请求合并(Request Coalescing)
:当多个请求同时访问相同的热键时,可以通过合并请求的方式,减少实际到达缓存系统的请求次数。这个策略可以显著降低对缓存的压力。
缓存预热
:在系统启动或缓存失效之前,预先加载热键数据,以减少缓存失效时对后端数据库的直接访问。
数据复制
:在分布式缓存系统中,可以将热键数据复制到多个节点上,这样可以分散读请求,减少单个节点的负载。
一致性哈希与虚拟节点
:通过一致性哈希算法对数据进行分片,并使用虚拟节点技术,使得数据更加均匀地分布在缓存集群中,防止某些节点成为热点。
Redis过期key的应对策略
定时删除;
在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行 key 的删除操作。
优点
可以保证过期 key 会被尽快删除,也就是内存可以被尽快地释放。因此,定时删除对内存是最友好的。
缺点
在过期 key 比较多的情况下,删除过期 key 可能会占用相当一部分 CPU 时间,在内存不紧张但 CPU 时间紧张的情况下,将 CPU 时间用于删除和当前任务无关的过期键上,无疑会对服务器的响应时间和吞吐量造成影响。所以,定时删除策略对 CPU 不友好。
惰性删除;
不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
惰性删除策略的优点:
因为每次访问时,才会检查 key 是否过期,所以此策略只会使用很少的系统资源,因此,惰性删除策略对 CPU 时间最友好。
惰性删除策略的缺点:
如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费。所以,惰性删除策略对内存不友好。
定期删除;
每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
优点
通过限制删除操作执行的时长和频率,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。
缺点
内存清理方面没有定时删除效果好,同时没有惰性删除使用的系统资源少。
难以确定删除操作执行的时长和频率。如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好;如果执行的太少,那又和惰性删除一样了,过期 key 占用的内存不会及时得到释放
Redis 选择「惰性删除+定期删除」这两种策略配和使用
Redis内存淘汰策略
不进行数据淘汰的策略
超过内存直接报错
进行数据淘汰
在过期key里
随机淘汰设置了过期时间的任意键值;
优先淘汰更早过期的键值
淘汰所有设置了过期时间的键值中,最久未使用的键值;
淘汰所有设置了过期时间的键值中,最少使用的键值;
不在过期key里
随机淘汰任意键值;
淘汰整个键值中最久未使用的键值;
淘汰整个键值中最少使用的键值。
CAP定理
一致性(所有节点在同一时间返回相同的数据)
可用性 (保证对每个客户端请求无论成功与否都有响应)
分区容忍性(系统中任意信息的丢失或失败不会影响系统的继续运行)
为什么不能同时满足
① 假设有两个数据分区DB1和DB2,存储着相同的一个数据,都是Version0。
② 有写请求进来,修改了DB1中的数据到Version1,正常情况下需要将修改同步到DB2,但是由于之间通信故障,DB2数据没能成功修改。
③ 当有读请求进来,请求DB1,返回正确数据Version1,请求DB2,由于数据没有成功修改,要么牺牲一致性,返回Version0,要么牺牲可用性,等故障恢复后再返回数据,阻塞掉请求。
Redis的高可用
主从复制+哨兵
内容
读操作通过多台服务器进行分摊查询压力
写操作同步给从服务器,所有的数据修改只在主服务器上进行,然后将最新的数据同步给从服务器
无法实现强一致性保证(主从数据时时刻刻保持一致),数据不一致是难以避免的。
在从服务器上进行,从服务器上一般是只读
同步过程
第一步建立连接 使用replicaof确认主机
replicaof <主服务器 的 IP 地址> <主服务器 的 Redis 端口号>
执行了 replicaof 命令后,从服务器就会给主服务器发送 psync 命令,表示要进行数据同步。
psync 命令包含两个参数,分别是主服务器的 runID 和复制进度 offset。
runID,每个 Redis 服务器在启动时都会自动生产一个随机的 ID 来唯一标识自己。当从服务器和主服务器第一次同步时,因为不知道主服务器的 run ID,所以将其设置为 "?"。
offset,表示复制的进度,第一次同步时,其值为 -1。
主服务器收到 psync 命令后,会用 FULLRESYNC 作为响应命令返回给对方。
这个响应命令会带上两个参数:主服务器的 runID 和主服务器目前的复制进度 offset。从服务器收到响应后,会记录这两个值。
FULLRESYNC 响应命令的意图是采用全量复制的方式,也就是主服务器会把所有的数据都同步给从服务器。
第二阶段:主服务器同步数据给从服务器
会执行 bgsave 命令来生成 RDB 文件,然后把文件发送给从服务器。
第三阶段:主服务器发送新写操作命令给从服务器
命令传播
主从服务器在完成第一次同步后,双方之间就会维护一个 TCP 连接。
长连接
分摊主服务器的压力
问题
由于是通过 bgsave 命令来生成 RDB 文件的,那么主服务器就会忙于使用 fork() 创建子进程,如果主服务器的内存数据非大,在执行 fork() 函数时是会阻塞主线程的,从而使得 Redis 无法正常处理请求;
传输 RDB 文件会占用主服务器的网络带宽,会对主服务器响应命令请求产生影响。
解决方法
我们可以把拥有从服务器的从服务器当作经理角色,它不仅可以接收主服务器的同步数据,自己也可以同时作为主服务器的形式将数据同步给从服务器,
增量复制
保证主从服务器的数据一致性呢
Redis 2.8 开始,网络断开又恢复后,从主从服务器会采用增量复制的方式继续同步,也就是只会把网络断开期间主服务器接收到的写操作命令,同步给从服务器。
repl_backlog_buffer,是一个「环形」缓冲区,用于主从服务器断连后,从中找到差异的数据;
replication offset,标记上面那个缓冲区的同步进度,主从服务器都有各自的偏移量,主服务器使用 master_repl_offset 来记录自己「写」到的位置,从服务器使用 slave_repl_offset 来记录自己「读」到的位置。
主从如何做到故障自动切换?
使用 Redis 哨兵机制,哨兵在发现主节点出现故障时,由哨兵自动完成故障发现和故障转移,并通知给应用方,从而实现高可用性。
脑裂
哨兵实现主从切换
场景
如果主节点的网络突然发生了问题,它与所有的从节点都失联了,但是此时的主节点和客户端的网络是正常的,这个客户端并不知道 Redis 内部已经出现了问题,还在照样的向这个失联的主节点写数据(过程A),此时这些数据被主节点缓存到了缓冲区里,因为主从节点之间的网络问题,这些数据都是无法同步给从节点的。这时,哨兵也发现主节点失联了,它就认为主节点挂了(但实际上主节点正常运行,只是网络出问题了),于是哨兵就会在从节点中选举出一个 leeder 作为主节点,这时集群就有两个主节点了 —— 脑裂出现了。这时候网络突然好了,哨兵因为之前已经选举出一个新主节点了,它就会把旧主节点降级为从节点(A)
问题:会丢失数据
因为第一次同步是全量同步的方式,此时的从节点(A)会清空掉自己本地的数据,然后再做全量同步。所以,之前客户端在过程 A 写入的数据就会丢失了,也就是集群产生脑裂数据丢失的问题。
解决方法
当主节点发现从节点通信数量小于阈值时,那么禁止主节点进行写数据,直接把错误返回给客户端。
原主库就会被限制接收客户端写请求,客户端也就不能在原主库中写入新数据了。 等到新主库上线时,就只有新主库能接收和处理客户端请求,此时,新写的数据会被直接写到新主库中。而原主库会被哨兵降为从库,即使它的数据被清空了,也不会有新数据丢失。
redis集群
当 Redis 缓存数据量大到一台服务器无法缓存时,就需要使用 Redis 切片集群(Redis Cluster )方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。
一个切片集群共有 16384 个哈希槽
根据键值对的 key,按照 CRC16 算法 (opens new window)计算一个 16 bit 的值。
再用 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。
分配节点
平均分配: 在使用 cluster create 命令创建 Redis 集群时,Redis 会自动把所有哈希槽平均分布到集群节点上。比如集群中有 9 个节点,则每个节点上槽的个数为 16384/9 个。
手动分配: 可以使用 cluster meet 命令手动建立节点间的连接,组成集群,再使用 cluster addslots 命令,指定每个节点上的哈希槽个数。
为什么是16384
是因为Redis 的作者认为这个数量能够在保证负载均衡的同时,又能够避免槽位数量过多带来的额外开销。 此外,16384 这个数是一个2 的14 次方,
Redis事务回滚
不支持事务回滚
原因
官方认为Redis 事务的执行时,错误通常都是编程错误造成的,这种错误通常只会出现在开发环境中,而很少会在实际的生产环境中出现,所以他认为没有必要为 Redis 开发事务回滚功能;
不支持事务回滚是因为这种复杂的功能和 Redis 追求的简单高效的设计主旨不符合。
缓存雪崩,击穿,穿透
雪崩
大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。
因为sql被冲烂了
解决方法
大量数据过期
设置随机的过期时间
互斥锁
当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存
双Key策略
主Key设置过期时间,备用Key永久
后台更新缓存
让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。 事实上,缓存数据不设置有效期,并不是意味着数据一直能在内存里,因为当系统内存紧张的时候,有些缓存数据会被“淘汰”
redis故障
服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误,
主从节点的方式构建 Redis 缓存高可靠集群。
击穿
某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题
单纯是因为redis中数据过期了用不了,但这个是单一数据过期且被大量访问
解决方法
互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间
穿透
当用户访问的数据,既不在缓存中,也不在数据库中,没办法构建缓存数据,那么就变成了缓存穿透
原因
业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
黑客恶意攻击,故意大量访问某些读取不存在数据的业务;
方案
第一种方案,非法请求的限制;
第二种方案,缓存空值或者默认值;
第三种方案,使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在;
熔断限流降级
熔断(Circuit Breaking)
目的:防止系统过载,保护系统免受连锁故障的影响。
工作原理:熔断机制类似于电路中的断路器。当一个服务出现问题(如超时、错误率过高等)时,熔断器会“断开”,阻止进一步的访问,从而避免进一步加剧系统负载和故障。
恢复:在断开一段时间后,熔断器会进入半开状态,允许有限的访问。如果这些访问成功,熔断器会完全“闭合”,恢复正常访问。
应用:常见于微服务架构中,用于防止服务间的连锁故障。
当依赖的第三方服务出现不稳定的情况时,例如三方服务器过载,会导致服务自身调用第三方服务的响应时间也变长,甚者形成级联效应。这样一来,服务自身的线程可能会积压,最终可能耗尽业务自身的线程池,导致服务本身变得不可用。
限流(Rate Limiting)
目的:控制访问速率,防止资源被过度使用。
工作原理:限流通过控制系统接收或处理的请求速率来防止系统过载。这可以通过限制每秒请求的数量、每个用户的请求速率等方式实现。
无论服务器的硬件多么强大,总归也是有限的资源只能处理有限的请求;简单理解限流(Rate Limit)的话,在有限时间内请求数量超过服务的处理数量,自动丢弃新来的请求从而保障有限的请求高可用。用日常例子来类比说明就很好理解,一个餐厅只有 5 张四人桌,理想坐满的情况也也就是 20 个人;而如果涌进来 50 个人都点单,那么每个人都无法正常的用餐。因此,要限制进来的人数是保障正常用餐。 同样的对于系统而言,一次性接受超出硬件承载的资源,就会导致资源的剧烈竞争从而 导致请求服务的延迟、异常等等,无法提供到高可用的服务。为此,对服务进行限流保护是提供高可用的重要策略了。
限流算法
固定窗口计数限流算法(Fixed Window Counter):
在固定的时间窗口内,限制请求的数量。例如,在 1 秒内最多允许处理 10 个请求,当窗口满时,后续请求将被拒绝。
滑动窗口计数限流算法(Sliding Window Counter):
设置一个滑动时间窗口,计算在该时间窗口内的请求数量,并限制其在指定范围内。与固定窗口计数算法相比,滑动窗口算法允许更加灵活的流量控制。
令牌桶算法(Token Bucket):
1. 令牌桶算法通过将请求放入令牌桶中来控制流量。每个请求需要从令牌桶中获取令牌,如果桶中没有足够的令牌,则请求被拒绝。令牌桶算法允许突发流量一定程度的处理,并平滑了请求的速率。
2. 令牌桶是一个固定容量的桶,它以恒定的速率产生令牌(即令牌产生速率),并将其放入桶中。
3. 桶中最大可以保存的令牌数量为桶的容量,当桶满时,多余的令牌会被丢弃。
4. 每当有请求到达时,如果令牌桶中有足够的令牌,该请求会获取一个令牌,并被处理。如果桶中没有令牌可用,该请求将被延迟或丢弃。
5. 令牌桶可以应用于固定窗口计数限流算法和滑动窗口计数限流算法。在固定窗口计数限流中,令牌桶以固定速率产生令牌,而在滑动窗口计数限流中,令牌桶按照滑动时间窗口的速率产生令牌。
令牌桶算法的优点在于,它可以平滑地处理突发流量,即使在短时间内有大量请求到达,令牌桶算法仍然能够保持相对稳定的速率来处理这些请求。此外,令牌桶算法还可以允许一定程度的突发流量,因为桶中积累的令牌可以处理突发的请求。令牌桶算法的缺点是在某些情况下可能会导致请求的延迟。如果请求到达时桶中没有足够的令牌,该请求将被延迟等待令牌,可能会导致响应时间增加。
漏桶算法
1. 将请求放入一个漏桶中,请求以恒定的速率从漏桶中流出。如果漏桶已满,则多余的请求将被拒绝。漏桶算法可以用于平滑流量,防止突发请求造成的资源浪费。
2. 漏桶是一个固定容量的桶,它有一个漏口。桶底的漏口以固定的速率(即令牌产生速率)漏水。
3. 每个请求都会向漏桶中添加一个令牌。如果漏桶已满(即桶内令牌数量达到了最大容量),则新的令牌会被丢弃。
4. 当请求到达时,如果漏桶中有可用的令牌,则请求被处理,且漏桶中的令牌数量减少一个。如果漏桶中没有足够的令牌,则请求被丢弃或延迟处理。
漏桶算法的优点在于,它能够以固定的速率来处理请求,从而平滑流量,防止突发请求对系统造成过大的压力。此外,漏桶算法还能够控制流出速率,避免资源的浪费;然而,漏桶算法的缺点是对于突发流量的处理相对较差。如果漏桶中的令牌数量耗尽,那么突发流量的请求会被丢弃,可能会导致某些请求的延迟。
降级(Degradation)
目的:在系统压力过大时降低服务质量,保证核心功能的运行。
工作原理:当系统资源紧张时,降级可以通过关闭或简化某些非核心功能来减轻负载。例如,降低数据质量、简化页面元素、关闭某些服务等。
应用:在系统负载高峰期或部分组件不可用时保持核心服务的运行。
服务降级往往指在面对系统过载、资源不足、有计划的大型活动(双十一),有意识地降低系统的部分功能或服务质量,以保证系统的核心功能和关键服务仍能继续正常运行。
三者关系
降级和熔断是不是一回事?熔断后执行策略就相当于降级?
纠结于这些问题的本身,还是要回到文章开头三大策略提出所面对解决的高可用问题。熔断是针对防止故障扩散所进行的策略设计,而 降级面对的是特殊场景的 服务功能/质量的调整策略。
数据库和缓存的最终一致性
为何出现数据不一致
由于网络延迟,先删除缓存结果更新数据库延迟了,线程2进来了查询数据库,因为没有更新数据库,并写回数据2到redis,最后线程1将老数据更新到数据库中了,后续读取redis一直都是旧数据
先更新数据库,再更新缓存
先更新缓存,再更新数据库
先删除缓存,再更新数据库
先更新数据库,再删除缓存\
因为缓存的写入通常要远远快于数据库的写入
实际中很难出现请求 B 已经更新了数据库并且删除了缓存,请求 A 才更新完缓存的情况。
「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。
有可能删除缓存失败
解决方法
删除重试机制
如果应用删除缓存失败,发送删除失败的KEY给MQ,线程监听MQ,监听到了之后,重新进行删除
如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。
如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。
订阅 MySQL binlog,再操作缓存
阿里巴巴开源的 Canal 中间件就是基于这个实现的。
Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。
基于MQ异步多写
定时任务
效率最高,延迟最高
MYSQL要有last-update字段
延时双删
先删缓存,再更新数据库,再删缓存
第一次删缓存然后网络延迟了,现在进程2进来,完成了数据库的读取,写回,但这时候数据库更新的操作到了,延迟100毫秒后再删出缓存(防止立即删除,读取的数据库数据覆盖了删除的缓存)
分区 分库 分表 分片
分库
分到不同的数据库中
分表
通过配置中间件,自动路由到不同的表上进行工作
mysql的分表是真正的分表,一张表分成很多表后,每一个小表都是完正的一张表
分区是对外暴露一个接口,但其实内部是根据某些规则物理拆开了表
比如将23年数据存一个表 24年数据存另个表
物理上将表分开逻辑上还是一张表
分区则把一张表的数据分成N多个区块,这些区块可以在同一个磁盘上,也可以在不同的磁盘上
分片
跨多个数据库实例和服务器的数据分布,每个分片包含整个数据库架构的一部分数据
水平扩展(Horizontal Scaling):当单个数据库的负载过高时,分片允许将数据和负载分散到多个服务器或实例,从而提高整体性能和存储能力。
高可用性和容错:分片可以在不同的物理服务器上进行,从而提高系统的可用性和容错性。
地理分布:为了减少延迟,可以将用户数据分片到离用户更近的服务器上。
分布式锁
为什么需要分布式锁
锁的作用域问题:因为在分布式的场景下,不能通过单体架构的锁来锁住分布式进场,普通锁只能锁住当前服务器的进程
Redis使用setNX实现
使用 SETNX 设置一个 Key,如果 Key 不存在,则操作成功(返回 true),这表示锁被当前进程获得。如果 Key 已存在,则返回 false,表示锁已被占用
这个Key是设为UUID保证每个key绑定的是自己的线程
如果返回false表示被占用 不能操作
要设置占用时间,不然可能挂掉,其他服务器的请求就会阻塞
持有时间过短,可能这么操作没执行完就释放了锁,其他进程就会进入
解决方案加一个子线程,每十秒确认主线程是否在线
如果在线则重新设置持有时间
为锁加上UUID
保证自己的锁绑定的是自己的线程不会释放其他线程的锁
加锁时,节点将锁与一个唯一的 UUID 关联起来。当尝试解锁时,节点会检查锁是否与其 UUID 匹配。如果不匹配,这意味着锁可能已经被其他节点获取(由于超时或其他原因),因此当前节点不应该解锁。
锁被别人释放问题
释放别人的锁的问题,关键在于身份鉴权,每个客户端在释放锁时,并没有检查这把锁是否还归自己持有,所以就会发生释放别人锁的风险,这样的解锁流程,很不严谨。
客户端在加锁时,设置一个只有自己知道的唯一标识进去,可以是自己的线程 ID,也可以是一个 UUID。
使用GET+DEL可能会遇到原子性问题
1. 客户端 1 执行 GET,判断锁是自己的
2. 客户端 1 执行 GET 结束后,这个锁刚好超时自动释放
3. 此时,恰好客户端 2 又获取到了这个锁
4. 之后,客户端 1 在执行 DEL 时,释放的却是客户端 2 的锁
解决办法:可以把这个逻辑,写成 Lua 脚本,让 Redis 来执行,在执行一个 Lua 脚本时,其它请求必须等待,直到这个 Lua 脚本处理完成,这样一来,GET + DEL 之间就不会插入其它命令了,
Redlock
在主从切换的时候如果master节点出现了故障,发生了了主从切换,就会出现锁丢失的情况:
1. 客户端 1 在主库上执行 SET 命令,加锁成功
2. 此时,主库异常宕机,SET 命令还未同步到从库上(主从复制是异步的)
3. 从库被提升为新主库,这个锁在新的主库上,丢失了
执行前提
1. 部署N个Redis master,这些节点完全互相独立,不存在主从复制或者其他集群协调机制
2. 主库要部署多个,官方推荐至少 5 个实例
具体操作
1. 客户端先获取当前时间戳T1
2. 客户端依次向这 5 个 Redis 实例发起加锁请求(用前面讲到的 SET 命令),且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁
3. 如果客户端从 >=3 个(大多数)以上 Redis 实例加锁成功,则再次获取当前时间戳T2,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败
4. 加锁成功,去操作共享资源(例如修改 MySQL 某一行,或发起一个 API 请求)
5. 加锁失败,向全部节点发起释放锁请求(前面讲到的 Lua 脚本释放锁)
基于ZooKeeper
客户端 1 和 2 都尝试创建临时节点,例如 /lock
假设客户端 1 先到达,则加锁成功,客户端 2 加锁失败
客户端 1 操作共享资源
客户端 1 删除 /lock 节点,释放锁
Zookeeper 不像 Redis 那样,需要考虑锁的过期时间问题,它是采用了临时节点,保证客户端拿到锁后,只要连接不断,就可以一直持有锁。
如果客户端 1 异常崩溃了,这个临时节点也会自动删除,保证了锁一定会被释放。
客户端 1 创建临时节点后,Zookeeper 是如何保证让这个客户端一直持有锁呢?
客户端 1 此时会与 Zookeeper 服务器维护一个 Session,这个 Session 会依赖客户端定时心跳来维持连接。
如果 Zookeeper 长时间收不到客户端的心跳,就认为这个 Session 过期了,也会把这个临时节点删除。
进程暂停
1. 客户端 1 创建临时节点 /lock 成功,拿到了锁
2. 客户端 1 发生长时间GC
3. 客户端 1 无法给 Zookeeper 发送心跳,Zookeeper 把临时节点删除
4. 客户端 2 创建临时节点 /lock 成功,拿到了锁
5. 客户端 1 GC 结束,它仍然认为自己持有锁(冲突)
即使是使用 Zookeeper,也无法保证进程 GC场景下的锁安全性
基于ETCD的分布式锁
1. 基于 Etcd 实现的分布式锁流程
客户端 1 创建一个 lease 租约(设置过期时间)
客户端 1 携带这个租约,创建 /lock 节点
客户端 1 发现节点不存在,拿锁成功
客户端 2 同样方式创建节点,节点已存在,拿锁失败
客户端 1 定时给这个租约续期,保持自己一直持有锁
客户端 1 操作共享资源
客户端 1 删除 /lock 节点,释放锁
2. 存在的问题
1. 客户端 1 创建节点 /lock 成功,拿到了锁
2. 客户端 1 发生长时间 GC
3. 客户端 1 无法向 Etcd 发请求给租约续期
4. 租约到期,Etcd 删除锁节点
5. 客户端 2 创建临时节点 /lock 成功,拿到了锁
6. 客户端 1 GC 结束,它仍然认为自己持有锁(冲突)
基于 Etcd 实现的分布锁,当拿到锁发生 GC问题时,依旧可能失效。 不过,上述 GC引起的问题,通常发生概率较低。得益于ZK和ETCD的易用性,目前仍然在业界得到了广泛的应用。
Redission
分布式事务
在分布式服务中,如何回滚不同服务器之间的事务
2PC
1. 事务协调者对参与者进行事务的通知
成功处理事务情况
这时候就会建立连接了
超时或者出问题的情况
一阶段
2. 通知所有参与者进行提交
如果进行提交
进行回滚
二阶段
缺点
导致全局阻塞
如果失败 导致其他链接资源浪费
3PC
比2PC多出来一个询问阶段
一阶段,但是不建立连接
二阶段,建立连接
不开启事务就不会导致资源浪费
TCC
表示
try
服务器自己先模拟操作,尝试能不能自己服务器有没有问题
提前准备好事务
返回确认
confirm
直接提交数据库
cancel
如果出问题了进入cancel
类似于2PC
区别
在TCC上是在代码层面修改
2PC是数据库层面
优点阻塞力度变小了
Try阶段: 这个阶段主要是对事务进行预留资源或检查约束,确保事务可以顺利完成。但此时并不真正执行事务的业务逻辑,仅仅是做准备。
Confirm阶段: 如果所有相关的Try操作都成功,那么进入Confirm阶段,这时会真正执行事务的业务逻辑,如修改数据。
Cancel阶段: 如果在Try阶段有任何一个操作失败,或者其他原因需要回滚,那么进入Cancel阶段,撤销在Try阶段做的所有操作。
只能达到最终一致性
Saga将长事务分成多个短事务
减少阻塞
无法保证隔离性
过程
直接开启事务
如果出现异常
发出反向回滚
使用消息队列完成事务一致性
1. 服务A开始一个本地事务,并执行必要的操作。
2. 事务成功,服务A将一个消息发送到消息队列,并等待确认。
3. 消息队列接收到消息并存储它,然后向服务A发送确认。
4. 服务A接收到确认后,提交本地事务。
5. 服务B从消息队列中接收消息,并在一个本地事务中执行相应的操作。
6. 服务B操作完成后,向消息队列发送处理完成的确认。
7. 如果服务B在处理过程中遇到错误,消息队列将根据重试策略重新投递消息。