Redis
2024-12-17 17:17:33 0 举报
AI智能生成
Redis是一个开源、先进的键值存储系统,采用C语言编写,具有强大的处理速度和稳定性。它支持丰富的数据类型,如字符串、哈希、列表、集合和有序集合,并提供了诸多实用的功能,如缓存、消息队列、分布式锁等。其高性能和灵活性使它成为许多企业级应用的首选。无论是作为独立的存储服务,还是配合其他组件构建大型系统,Redis都是一款强大的工具。
作者其他创作
大纲/内容
Redis 分布式锁
在多台机器提供服务的情况下,想要加锁不能使用 Sychronized 关键字,因为不能隔离不同机器中线程 id 相同的两个线程
自己实现一个简单的分布式锁:使用 setnx
key 的设计:使用 uuid + threadid
uuid:用来区分不同的 JVM 服务
threadid:用来区分同一台机器的不同线程
使用 setnx 设计的分布式锁不是可重入锁,已经获取锁的对象不能再获取到这个锁
Redis 提供了 Redission,这就是一个可重入锁
使用两个参数:一个是 uuid+threadId,另一个是重入次数
所以 Redission 使用的数据结构是 hash,filed 存的是 uuid+threadId,value 存的是重入次数
Redission 帮我们设置了默认的过期时间 30s,看门狗机制每过 10s 都会将 过期时间重置为 30s;看门狗会判断当前线程状态,如果线程异常,将会释放锁,这样就防止了在使用锁的过程中出现故障,导致锁不能释放
限流器
当突然遇到流量剧增的情况下,可能导致服务器宕机,为了保护上下游服务,可以采用限流的方法,防止 QPS 超过系统的上限
请求阈值:单位时间内允许请求的最大请求数
拒绝策略:处理超过阈值的请求方法,常见的拒绝策略包括直接拒绝和等待
直接拒绝:直接返回客户端(提示:抢购人数太多)
等待:将请求放入到队列中,按照一定的规则处理
限流算法
固定窗口限流(计数器限流):将时间划分为固定大小的窗口
将时间划分为固定大小的窗口,比如每秒一个
在每一个时间窗口内,记录请求数量
当窗口有请求,请求数 +1
当请求数超过阈值,执行拒绝策略
当前窗口结束后,重置请求数
缺点
请求不均匀
请求突刺
滑动窗口限流:将时间按照一定比例分片,相比于固定滑动窗口限流粒度更小
漏桶限流算法:将请求存储在一个漏桶中,无论以任意速率访问请求,漏桶始终以固定的速率流出
令牌桶限流算法:和漏桶算法相似,只不过在桶里面装的是令牌,请求必须获得令牌才能进行处理
Redis 基础
Redis 是什么
Redis 是一种基于内存的存储的非关系型数据库
为什么使用 Redis
Redis 中的数据因为都是存放在内存中,所以对于数据的读取比使用 MySQL 速度要快,我们就可以使用 Redis 来作为缓存
Redis 支持的最大 QPS 比 MySQL 的高很多,使系统的并发能力变的更高
Redis 提供了很多种的数据类型,针对于不同的场景,使用不同的数据结构进行存储
Redis 除了可以做分布式缓存,还可以用来做分布式锁、限流器、消息队列、延时队列
Redis 为什么快
Redis 是基于内存进行数据的存储,内存的访问速度比磁盘快很多
Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用
IO
BIO(Blocking IO)同步阻塞式 IO
应用程序发起 Read 调用后,会一直阻塞,直到把内核的数据拷贝到用户空间
缺点:当有十万甚至百万的连接,传统的 BIO 会无能为力
NIO(Non-Blocking IO)同步非阻塞式 IO
应用程序不断进行 Read 调用,查看内核是否把数据准备好,等准备好的时候,将数据从内核拷贝到用户空间,这个数据拷贝的过程是阻塞的。
缺点:虽然相比于传统的 BIO 有很大的改进,避免的一直阻塞,但是应用程序不断的进行 IO 系统调用是很耗费资源的
IO 多路复用
线程通过 select/poll/epoll 进行调用,询问内核数据是否准备就绪,等内核把数据准备好,把数据从内核拷贝到用户空间
有一个 Selector 选择器,通过使用一个线程或者多个线程就可以管理多个客户端的连接
select调用,缺点
文件描述符集合其实就是一个数组,操作系统限制了数组的大小,所以导致接收的文件描述符的个数受限
每次进行 select 调用,都需要将文件描述符集合从用户态拷贝到内核态
因为文件描述符的本质是数组,内核对文件描述符集合进行遍历扫描判断有多少时间就绪的时间复杂度是 O(n),当随着监控文件的个数变多,性能也就下降
select 调用返回的是 int 值,表示的有多少个就绪事件准备好,导致每次返回之后还需要遍历查看有哪些就绪事件
poll 调用,缺点
poll 调用其实并没有本质的提升,只不过不在使用数组来存储文件描述符,使用链表,没有上限。但是select 调用的其他缺点,poll 调用也存在
epoll 调用,优点
使用两个数据结构来存储,一个用来存储要监控的所有文件描述符,一个用来存储已经就绪的文件描述符
在用户态不用遍历所有的文件描述符查看哪些事件就绪,直接遍历已经就绪的文件描述符即可
用红黑树来存储所有要监听的文件描述符,查询时间复杂度是 O(log n),不会随着文件描述符的增多导致时间花费的时间线性增加,更高效
Redis 提供了很多优化后的数据结构,非常高效
String
底层实现使用不是 C 语言的字符串,自己使用 C 语言编写了 SDS(Simple Dynamic String,简单动态字符串)作为底层实现
优点
因为 C 语言中的字符串是以 \0 作为结尾的,所以不能存储一些像音频、图片之类的二进制文件,使用 SDS 通过 len 属性来判断字符串是否结束
C 语言中获取字符串的长度的时间复杂度是 O(n),SDS 可以直接读取 len 属性的值,时间复杂度是O(1)
C 语言中的字符串被修改时,一旦没有分配足够长的内存空间,会造成缓冲区溢出。使用 SDS,通过 alloc - len 检查空间是否足够,如果不够,自动扩充到需要的大小
应用场景
存储 Json 字符串等
进行计数
设计一个简单的分布式锁(不可重入的)
List
一个简单的字符串列表,按照插入顺序排序,可以从头部或者尾部向 List 列表添加元素
Hash
Hash 是一个键值对集合,比较适合存储对象
应用场景
购物车信息、文章信息、商品信息、用户信息等
Set
无序并唯一的键值集合,他的存储顺序不会按照插入的先后顺序进行存储
应用场景
共同好友、共同关注、共同喜好等功能
Sort Set (ZSet)
ZSet 相比于 Set 多了一个排序属性值(score)
底层实现
当有序集合的元素个数小于 128 个,并且每个元素的值小时 64 字节时,使用 listpack
否则使用跳表
跳表和平衡树相比
平衡树的插入和删除比较耗时,需要保证二叉树的平衡
跳表和红黑树相比
红黑树的插入、删除和查询的时间复杂度与跳表一样都是 O(log n),但是红黑树的实现相对复杂,区间查找比跳表慢
跳表和 B+ 树相比
使用跳表实现 ZSet 时相较 B+ 树更简单一些,在插入时只需要通过索引找到合适的位置再随机维护一定的高度即可,也不需要像 B+ 树那样插入时发现失衡还需要对节点分裂合并
应用场景
排行榜之类的需要排序的
BitMap
底层实现
用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型
应用场景
适合二值状态统计的场景,比如签到统计之类的
HyperLogLog
用于百万级的网页 UV 计数
Geo
用到经纬度,可以考虑使用
Stream·
用于实现消息队列
其他的分布式缓存
Memcached:也是基于 Key-Value 型的非关系型数据库,性能与 Redis 相比差不多。但是存在一些缺点
不支持持久化:数据全部放在内存之中,Memcached 重启或者挂掉之后,数据就没了
数据类型简单:Memcached 只支持最简单的 key-value 类型
Memcached 没有原生的集群模式,需要依靠客户端来实现往集群分片中写入数据
Redis 线程模式
对于读写命令,Redis 一直都是单线程模式,不过在 Redis 4.0 之后引入了多线程异步的删除大键值对,在 Redis 6.0 之后引入多线程来处理 IO 网络请求
Redis 6.0 之前为什么不引入多线程
单线程编程更容易维护
多线程就可能会造成死锁、线程上下文切换等问题,降低性能
关键是 Redis 的瓶颈不在 CPU,主要是内存和网络
Redis 6.0 之后为什么引入多线程
引入多线程主要是为了提高网络 IO 读写性能,执行命令依然是采用单线程执行
Redis 持久化
Redis 的读写操作都是在内存中,当 Redis 重启之后,内存数据就会丢失,那为了保证内存中的数据不会丢失,Redis 实现了数据持久化的机制,将数据持久化到磁盘中,等 Redis 重启之后就可以从磁盘中恢复原有的数据
AOF 日志:每执行一条写操作命令,会把该命令以追加的方式写入到一个文件中
问题
数据丢失:当执行完写操作命令的时候,在命令还没有写到日志的时候 Redis 发生了宕机,就会存在数据丢失问题
主进程阻塞:因为将命令写入到日志的这个操作是在主进程完成的,操作命令也是在主进程。如果在将日志内容写入到硬盘时,服务器的硬盘 I/O 压力太大,就会导致写硬盘的速度很慢,发生阻塞,也就会导致后续的命令无法执行
针对这个问题,有三种写回策略,但无论使用哪种策略,都无法同时解决数据丢失和主进程阻塞两个问题,因为两个问题是对立的
Always:每次写操作命令执行完,同步将 AOF 日志数据写回到硬盘
Everysec:每次写操作命令执行完后,先将命令写入到内核缓冲区,每隔一秒将缓冲区的的内容写入到磁盘
No:不由 Redis 控制写回硬盘的时机,转交给操作系统控制
随之执行写命令越来越多,AOF 日志文件的大小也会越来越大,如果日志文件过大,在 Redis 重启恢复数据的时候执行 AOF 日志文件中的指令就会花费很长的时间。所以提供了 AOF 机制,当 AOF 文件的大小超过设定的阈值时,Redis 就启用 AOF 重写机制压缩日志文件。读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到新的 AOF 日志文件中,最后将旧的 AOF 日志文件替换掉。
流程:创建一个后台子进程 bgwriteaof ,子进程不会阻塞主进程执行命令;操作系统把主进程的页表复制一份给子进程,同时将页表对应的页表项属性标记为只读。这样可以节省内存空间
问题:如果在进行 AOF 重写的时候,主进程对共享数据(已经存在与 Redis 中的键值对)进行修改,就会导致主进程的内存数据和子进程的内存数据不一致。
解决:写时复制
Redis 设置了一个 AOF 重写缓冲区,当主进程执行一条写命令之后,会将命令写入到 AOF 缓冲区和 AOF 重写缓冲区,当写命令执行结束,将 AOF 重写缓冲区中的数据追加到新的 AOF 日志文件中
RDB 快照就是记录某一时刻的内存数据,记录的是实际数据,RDB 文件的数据是二进制数据,恢复数据的时候比 AOF 效率高
save 命令生成 RDB 快照:在主线程中生成文件,和执行操作的命令在一个线程,如果时间比较长,会造成阻塞
bgsave 命令生成 RDB 快照:在进程中生成文件,避免了阻塞主进程
问题:生成快照如果太频繁,可能对 Redis 的性能产生影响;如果频率太低,出现服务器故障,会丢失很多数据
问题:使用 bgsave 命令生成文件的时候,如果主线程对共享数据进行修改,同样采用写时复制技术;但是这次对共享数据的修改没办法同步到 RDB 文件中,只能等下一次生成 RDB 文件。
RDB 快照:将某一时刻的内存数据以二进制的方式写入到磁盘
混合持久方式:集成了 AOF 和 RDB 的优点(AOF 丢失数据少,RDB 恢复数据快)
在进行 AOF 重写文件的时候,先将共享数据以 RDB 的方式写入到 AOF 文件中,这期间主线程执行的命令会记录在重写缓冲区,最后将这写命令以 AOF 的方式写入到 AOF 文件,替换旧的 AOF 文件
Redis 内存管理
如何判断数据过期:在查询一个数据的时候,Redis 先根据这个 key 查看是否在在过期字典表中,如果不在,直接返回数据;如果在的话需要判断数据是否过期,过期的话直接删除 key 然后返回 null
过期淘汰策略
惰性删除:只会在每次查询到时候对 Key 进行检查,对 CPU 友好
定期删除:周期性的随机从设置了过期时间的 key 中抽查一批,然后检查是否过期,过期则删除(不一定会删除)
为什么有些过期的 key 没有被删除:因为 Redis 的定期删除受执行时间和过期的 key 的比例的影响
如果删除的执行时间超过了阈值,就会中断这次定期删除,防止删除 key 占用太多的时间
如果这一批过期 key 的比例超过阈值,就会重复进行定期删除,直到超过执行时间或者过期 key 的比例小于阈值
延迟队列:将设置了过期时间的 key 放在一个队列中,过期了就删除
定时删除:每个设置了过期时间的 key 在过期之后立刻删除
Redis 使用的是惰性删除+定期删除,惰性删除对 CPU 友好,定期删除对内存友好
内存淘汰策略:在 Redis 的运行内存达到了某个阈值,就会触发内存淘汰机制
不进行数据淘汰
noeviction:当运行内存超过阈值,不淘汰任何数据,也不再提供服务,直接报错
进行数据淘汰
对设置过期 key 的数据进行淘汰
volatile-ttl:优先淘汰最早过期的键值
volatile-random:对设置过期时间的 key 随机进行淘汰
volatile-lru:对设置过期时间的 key 选择最近最久未使用的进行淘汰
volatile-lfu:对设置过期时间的 key 选择最近最不经常使用的进行淘汰
对所有数据进行淘汰
allkeys-random
allkeys-lru
allkeys-lfu
算法分析
lru
传统的 lru 算法中,是通过链表实现的,链表中的元素通过操作顺序从前往后排序,每当对一个元素进行一个操作,就将这个元素移动到链表的头部位置
Redis 实现的 lru并没有使用链表,为了节省空间,它的实现是在 Redis 对象中添加一个字段用于记录最后一次访问的时间
缺点:无法解决缓存污染问题;当用大量的数据被访问,但是这些数据只是被访问了一次,那么这些数据在内存中就会存活很久
lfu:解决了缓存污染的问题
Redis 缓存设计
缓存雪崩
当大量数据在同一时间内过期,如果这时候有大量的用户请求,这些请求会直接访问数据库,导致数据库压力剧增,可能会发生宕机
设置随机的失效时间:防止同一时间内大量的数据过期
持久缓存策略:对于某些关键性的或者变化不频繁的数据可以不设置过期时间
提前预热:针对热点数据可以提前预热,比如秒杀业务,在活动结束之前,数据不过期
限流
缓存击穿
当缓存中某个热点数据过期,此时大量的请求访问该热点数据,导致请求直接打在数据库上,可能冲垮数据库
热点数据不设置过期时间
提前预热:对热点数据提前预热,保证在活动结束之前热点数据不会过期
互斥锁
缓存穿透
当用户访问的数据即不在缓存,也不在数据库,如果有大量这样的请求,也会导致数据库压力剧增,可能导致数据库宕机
布隆过滤器
组成:一个二进制向量(位数组)和多个映射函数(哈希函数)
原理
使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值,将位数组中对应的值变为 1
判断一个元素是不是存在:通过布隆过滤器的哈希函数对元素进行计算,查看在位数组的对应下标的值是否都为 1
特点:布隆过滤器判断存在,不一定真的存在;但是布隆过滤器判断不存在,一定不存在
缓存无效key
对用户或者 IP 进行限流
设置黑名单,对频繁异常访问的用户或者ip加入黑名单
Redis 数据一致性
通常情况下我们都是用旁路缓存(cache aside),对于写操作,先更新数据库,再删除缓存
异常情况:可能更新数据库成功,但是删除缓存出现问题;导致了数据库与缓存的数据不一致
使用消息队列
基于 canal 订阅数据库 binlog 的异步方案
canal 的原理是基于 MySQL 的主从同步来实现的,canal 就是将自己伪装成 MySQL 的一个 slave 节点,从而监听 master 节点的 binlog 变化,再把得到的变化信息通知给 canal 客户端,进而完成对其他数据库的同步
异常情况:在读写分离 + 主从复制延迟的情况下,对主库修改之后,删除缓存,但是从库还没有同步更新到主库的修改
解决办法:延迟删除缓存,比如使用消息队列中的延时队列,达到延迟删除缓存。目的就是让从库同步更新完主库的内容,再删除缓存
Redis 集群
主从复制:面对读多写少的业务,通过主从复制,让读数据的压力分摊到多台 Redis 服务器上,可以在主服务器上进行读写,从服务器进行读
第一次同步
在第一次复制的时候,从服务器 执行 replicaof 主服务器ip 端口号,尝试与主服务器建立连接
第一阶段
向主服务器发送 psync ? -1 命令(?表示主服务器的 RunId,-1表示复制进度)
主服务收到命令,告诉从服务器要你全量复制的方式进行数据同步
第二阶段
主服务器进行执行 bgsave 生成 RDB 文件,并且把 RDB 文件发送给从服务器
从服务器收到 RDB 文件后载入数据
第三阶段
在第二阶段主服务器执行的写命令会保存到复制缓冲区
当从服务器将 RDB 文件载入完成,主服务器将复制缓冲区里的写命令发送给从服务器
从服务器执行这些命令,达到主从一致
命令传播
在主从第一次完成同步之后,双方会维护一个 TCP 连接,后续主服务器的所有写命令都会同步到从服务器
增量复制
在第一次同步之后,主从突然断开连接,一段时间之后回复连接,需要再次保证主从一致
Redis 2.8 之前
采用全量复制的方法,但这样开销很大
Redis 2.8 之后
从服务器向主服务器发送 psync 命令,带有参数 offset(复制进度),这时的 offset 就不是 -1
主服务器判断 offset 表示的数据是否在环形缓冲区,如果在,就采用增量复制的方法;如果不在,就采用全量复制的方法
如果采用增量复制,就将从服务器断开连接期间主服务器执行的写命令全部发送给从服务器;如果采用全量复制,主服务器就生成 RDB 文件发送给从服务器
哨兵:在主从架构中,如果主节点挂了,那么将没有主节点来服务客户端的写操做请求,也没有主节点给从节点进行数据同步;在 Redis 2.8 之后,提供哨兵机制,实现主从节点故障转移
监控
哨兵会每隔 1 秒想所有的主从节点发送 ping 命令,当主从节点收到 ping 命令之后,会发送一个响应命令给哨兵,表示正常运行
当有主节点没有给哨兵返回响应命令,认为这个主节点主观下线;当前哨兵会通知其他哨兵也对这个主节点进行检测,如果有超过 quorum(配置文件中的值) 的值认为主节点下线,则认为这个主节点客观下线
选主
当前哨兵向其他哨兵发送命令,希望自己来执行主从切换的操作
故障转移
选取一个网络稳定、合适的从节点作为主节点
让旧主节点所属的从节点修改复制目标,修改为新主节点
新主节点通过 发布者/订阅者机制将自己的 IP 和端口号通知给客户端
哨兵继续检测旧主节点,当旧主节点回复连接,将旧主节点设置为从节点
分片集群:当 Redis 要存储大量数据,或者说读并发比较高的时候,可以考虑使用 分片集群
集群中有多个 master,每个 master 保存不同数据,每个master 都可以有多个从节点
Redis Cluster 采用哈希槽的方式来处理数据和节点之间的映射关系,一个分片集群有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根绝它的 key,被分配到一个哈希槽中
根据键值对的 key,按照 CRC16 算法计算一个 16 bit 的值
再用 16 bit 值对 16384 取模,得到对应的哈希槽
0 条评论
下一页