13_Redis
2021-08-25 09:02:50 2 举报
AI智能生成
Redis的一些知识梳理
作者其他创作
大纲/内容
应用篇
聊聊 Redis 的淘汰机制
缓存淘汰过程
第一步,根据一定的策略,筛选出对应用访问来说“不重要”的数据
第二步,将这些数据从缓存中删除,为新来的数据腾出空间
对于 Redis 来说,它决定被淘汰的数据后,会把它们删除,即使淘汰的数据是脏数据,Redis 也不会把它们写回数据库
如果数据被修改,需要在数据修改时就将它写回数据库,否则,这个脏数据被淘汰时,会被 Redis 删除,而数据库里也没有最新的数据
内存淘汰策略
noeviction 策略(不进行数据淘汰)
一旦缓存被写满了,再有写请求来时,Redis 不再提供服务,而是直接返回错误
进行数据淘汰的策略
在设置了过期时间的数据中进行淘汰
volatile-ttl
根据过期时间的先后进行删除,越早过期的越先被删除
volatile-random
随机删除设置过期时间的键值对
volatile-lru
使用 LRU 算法筛选设置了过期时间的键值对
volatile-lfu
使用 LFU 算法选择设置了过期时间的键值对
在所有数据范围内进行淘汰
allkeys-random
从所有键值对中随机选择并删除数据
allkeys-lru
使用 LRU 算法在所有数据中进行筛选
allkeys-lfu
使用 LFU 算法在所有数据中进行筛选
优先使用 allkeys-lru 策略
聊聊分布式锁
分布式锁的应用场景
分布式锁的演进
如何解决缓存和数据库的数据的不一致问题
聊聊 Redis 的缓存穿透、缓存击穿和缓存雪崩
缓存雪崩
大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增
第一个原因,缓存中有大量数据同时过期,导致大量请求无法得到处理
方案一,避免给大量的数据设置相同的过期时间,用 EXPIRE 命令给每个数据设置过期时间时,给这些数据的过期时间增加一个较小的随机数
方案二,服务降级,当业务应用访问的是非核心数据时,暂时停止从缓存中查询这些数据,而是直接返回预定义信息、空值或是错误信息
第二个原因,Redis 缓存实例发生故障宕机,无法处理请求,这就会导致大量请求一下子积压到数据库层,从而发生缓存雪崩
建议一:在业务系统中实现服务熔断或请求限流机制
建议二,事前预防,构建 Redis 缓存高可靠集群
缓存击穿
针对某个访问非常频繁的热点数据的请求,无法在缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增
解决方案:对于访问特别频繁的热点数据,不设置过期时间,对热点数据的访问请求,都可以在缓存中进行处理
缓存穿透
访问的数据既不在 Redis 缓存中,也不在数据库中,请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据
这样一来,缓存也就成了“摆设”,如果应用持续有大量请求访问数据,就会同时给缓存和数据库带来巨大压力
可能产生的原因
业务层误操作:缓存中的数据和数据库中的数据被误删除了,所以缓存和数据库中都没有数据
恶意攻击:专门访问数据库中没有的数据
应对方案
方案一:缓存空值或缺省值
方案二:使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据是否存在,减轻数据库压力
方案三:请求入口的前端进行请求检测,过滤恶意请求
入门篇
简单介绍一下 Redis
简介
使用 C 语言开发的数据库,与传统的数据库不同,Redis 的数据存储在内存中,读写速度非常快,因此,Redis 广泛应用于缓存方向
也经常被用来做分布式锁,甚至是消息队列
Redis 提供多种数据类型来支持不同的业务场景
Redis 还支持事务、持久化、Lua 脚本、多种集群方案
Redis 是一个基于键值对的 NoSQL 数据库
特性
速度快
丰富的功能、丰富的数据结构
简单稳定
客户端语言多
持久化机制
主从复制
高可用和分布式
分布式缓存常见的技术选型
分布式缓存主要解决的是单机缓存的容量受服务器限制并且无法保存通用信息的问题,本地缓存只在当前服务有效,服务之间无法共享
使用的比较多的主要是 Memcached 和 Redis,不过,现在基本没有项目使用 Memcached 来做缓存,都是直接用 Redis
介绍一下 Redis 和 Memcached
为什么要用缓存
简单来说,使用缓存主要是为了提升用户体验以及应对更多的用户请求
高性能
操作缓存相当于直接操作内存,响应速度非常快
高并发
操作缓存能够承受的请求数量远远大于直接访问数据库
项目中如何使用缓存以及缓存使用不当可能会带来什么问题
缓存与数据库双写不一致
缓存穿透和缓存雪崩
缓存并发竞争
聊聊 Redis 的数据结构
全局哈希表
为了实现从键到值的快速访问,Redis 使用了一个哈希表来保存所有键值对
一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶,哈希桶中的元素保存的并不是值本身,而是指向具体值的指针
哈希桶中的 entry 元素中保存了*key和*value指针,分别指向了实际的键和值,这样一来,即使值是一个集合,也可以通过*value指针被查找到
O(1) 的时间复杂度来快速查找到键值对,只需要计算键的哈希值,就可以知道它所对应的哈希桶位置,然后就可以访问相应的 entry 元素
哈希冲突问题
往哈希表中写入更多数据时,哈希冲突是不可避免的问题
Redis 解决哈希冲突的方式,就是链式哈希,很容易理解,就是指同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接
哈希冲突可能也会越来越多,这就会导致某些哈希冲突链过长,进而导致这个链上的元素查找耗时长,效率降低
对于追求“快”的 Redis 来说,不太能接受
rehash的过程
Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2,一开始,刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间
随着数据越来越多,开始rehash过程
第一步,给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍
第二步,把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中
第三步,释放哈希表 1 的空间
但是第二步涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都迁移完,会造成 Redis 线程阻塞,无法服务其他请求
渐进式rehash
巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问
第一步,为 ht[1] 分配空间,Redis 同时持有 ht[0] 和 ht[1] 两个哈希表
第二步,Redis 中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始
第三步,rehash 进行期间,每次执行添加、删除、查找或者更新操作,会将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1]
第四步,rehash 工作完成之后, 程序将 rehashidx 属性的值加1
第五步,随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1]
第六步,程序将 rehashidx 属性的值设为 -1, 表示 rehash 操作全部完成
渐进式 rehash 的过程中,字典会同时使用 ht[0] 和 ht[1] 两个哈希表,渐进式 rehash 期间,字典的删除、查找、更新操作会在两个哈希表进行
要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找,如果没找到的话, 就会继续到 ht[1] 里面进行查找, 诸如此类
渐进式 rehash 的过程中,添加操作只会被保存到 ht[1] 哈希表, 而 ht[0] 则不再进行任何添加操作,最终变成空表
rehash 被触发后,Redis 会按一定频率(例如每 100ms/ 次)执行 rehash 操作,即使没有收到新请求,Redis 也会定时执行一次 rehash 操作
聊聊 Redis 的应用场景
string
常用命令, set,get,strlen,exists,decr,incr,setex 等;
一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等
list
常用命令,rpush,lpop,lpush,rpop,lrange,llen 等
发布与订阅或者说消息队列、慢查询
hash
常用命令,hset,hmset,hexists,hget,hgetall,hkeys,hvals 等
系统中对象数据的存储
set
常用命令,sadd,spop,smembers,sismember,scard,sinterstore,sunion 等
需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景
sorted list
常用命令,zadd,zcard,zscore,zrange,zrevrange,zrem 等
需要对数据根据某个权重进行排序的场景,比如直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜)等信息
Redis 为什么使用单线程
Redis 单线程指的是 Redis 的网络 IO 和键值对读写是由一个线程来完成,这也是 Redis 对外提供键值存储服务的主要流程
持久化、异步删除、集群数据同步等,其实是由额外的线程执行,所以,严格来说,Redis 并不是单线程
单线程编程容易并且更容易维护
Redis 的性能瓶颈不再是 CPU ,主要在内存和网络
多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能
Redis 的单线程为什么这么快
Redis 的大部分操作在内存上完成
Redis 采用高效的数据结构,例如哈希表和跳表
Redis 采用多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率
Redis 6.0 为什么引入多线程
为了提高网络 IO 读写性能
Redis 的多线程只是应用在网络数据的读写这类耗时操作,执行命令仍然是单线程顺序执行,不需要担心线程安全问题
进阶篇
聊聊 Redis 的持久化机制
一旦服务器宕机,内存中的数据将全部丢失
后端数据库恢复这些数据的方式存在两个问题
第一,需要频繁访问数据库,会给数据库带来巨大的压力
第二,数据是从慢速数据库中读取出来的,性能肯定比不上从 Redis 中读取,导致使用这些数据的应用程序响应变慢
实现数据的持久化,避免从后端数据库中进行恢复,至关重要
AOF日志
实现原理
写后日志
第一步,先执行命令,把数据写入内存
第二步,记录日志,记录每一条命令,文本形式保存
好处
第一,避免出现记录错误命令,命令执行成功才会记录日志
第二,命令执行后才记录日志,不会阻塞当前写操作
风险
第一,刚执行完一个命令,还没有来得及记日志就宕机,那么这个命令和相应的数据就有丢失的风险
第二,AOF 日志也是在主线程中执行,日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行
两个风险都是和 AOF 写回磁盘的时机相关
三种写回策略
AOF 配置项 appendfsync 的三个可选值
Always
同步写回:每个写命令执行完,立马同步地将日志写回磁盘
基本不丢数据,但是它在每一个写命令后都有一个慢速的落盘操作,不可避免地会影响主线程性能
No
操作系统控制写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
落盘的时机已经不在 Redis 手中,只要 AOF 记录没有写回磁盘,一旦宕机对应的数据就丢失
Everysec
每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘
避免了“同步写回”的性能开销,减少了对系统性能的影响,但是如果发生宕机,上一秒内未落盘的命令操作仍然会丢失
根据系统对高性能和高可靠性的要求,来选择使用哪种写回策略
AOF重写
AOF文件过大怎么办
第一,操作系统对文件大小有限制,超过则无法继续写入
第二,文件太大,写入的效率也会变低
第三,文件太大,恢复数据也很耗时
AOF重写机制
重写的时候,是根据这个键值对当前的最新状态,为它生成对应的写入命令,虽然 AOF 重写后,日志文件会缩小,但是,要把整个数据库的最新数据的操作日志都写回磁盘,仍然是非常耗时的过程,AOF重写机制怎么解决这个问题的呢?简单来说,一个拷贝,两处日志,主线程不会阻塞
重写的过程
简单来说,一个拷贝,两处日志
一个拷贝
首先,主线程 fork 出后台的 bgrewriteaof 子进程,fork 会把主线程的内存拷贝一份给 bgrewriteaof 子进程,里面就包含数据库的最新数据
然后,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志
正常 AOF 日志
主线程未阻塞,仍然可以处理新来的操作
重写的时候,如果有写操作,Redis 会把这个操作写到它的缓冲区,即使宕机, AOF 日志的操作仍然是齐全的,可以用于恢复
新的 AOF 重写日志
此时,如果有写操作,也会被写到重写日志的缓冲区,重写日志也不会丢失最新的操作
等到拷贝数据的所有操作记录重写完成后,重写日志记录的这些最新操作也会写入新的 AOF 文件,此时,就可以用新的 AOF 文件替代旧文件了
AOF 方法进行故障恢复的时候,需要逐一把操作日志都执行一遍,如果操作日志非常多,Redis 就会恢复得很缓慢,影响到正常使用
RDB快照
内存快照,就是指内存中的数据在某一个时刻的状态记录,实现类似照片记录效果的方式,就是把某一时刻的状态以文件的形式写到磁盘
快照文件就称为 RDB 文件,其中,RDB 就是 Redis DataBase 的缩写
实现原理
全量快照
把内存中的所有数据都记录到磁盘
save:在主线程中执行,会导致阻塞
bgsave:创建一个子进程,专门用于写入 RDB 文件,避免主线程的阻塞,默认配置
借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作
bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据
bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件
如果主线程对这些数据也都是读操作,那么,主线程和 bgsave 子进程相互不影响
如果主线程要修改一块数据,那么,复制一份该数据的副本,然后,主线程修改副本,同时,bgsave 子进程继续把原来的数据写入 RDB 文件
频繁地执行全量快照,也会带来两方面的开销
一方面,频繁将全量数据写入磁盘,会给磁盘带来很大压力
一方面,bgsave 子进程需要通过 fork 操作从主线程创建出来,fork 这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长
增量快照
混合使用 AOF 日志和内存快照
内存快照以一定的频率执行
快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响
两次快照之间,使用 AOF 日志记录这期间的所有命令操作
AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作,因此,就不会出现文件过大的情况,避免重写开销
数据尽量少丢失
聊聊 Redis 的主从同步
服务尽量少中断
增加副本冗余量,将一份数据同时保存在多个实例,即使有一个实例出现了故障,需要过一段时间才能恢复,其他实例也可以对外提供服务
Redis 提供主从库模式,以保证数据副本的一致,主从库之间采用的是读写分离的方式
读操作:主库、从库都可以接收
写操作:首先到主库执行,然后,主库将写操作同步给从库
主从同步
全量复制
启动多个 Redis 实例,相互之间通过 replicaof(Redis 5.0 之前使用 slaveof)命令形成主库和从库的关系
第一阶段,主从库间建立连接、协商同步的过程,为全量复制做准备
从库和主库建立起连接,并告诉主库即将进行同步,主库确认回复后,主从库间就可以开始同步
从库给主库发送 psync 命令,表示要进行数据同步,主库根据命令的参数来启动复制,psync 命令包含主库的 runID 和复制进度 offset 两个参数
runID,Redis 实例启动时都会自动生成的一个随机 ID,唯一标记这个实例,从库和主库第一次复制时,不知道主库的 runID,将 runID 设为 ?
offset,此时设为 -1,表示第一次复制
主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset,返回给从库
从库收到响应后,会记录下这两个参数
FULLRESYNC 响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库
第二阶段,主库将所有数据同步给从库,从库收到数据后,在本地完成数据加载
主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库
从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件,避免之前数据的影响
为了保证主从库的数据一致性,主库会在内存中用专门的 replication buffer,记录 RDB 文件生成后收到的所有写操作
第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库
主库完成 RDB 文件发送后,会把 replication buffer 中的修改操作发给从库,从库再重新执行这些操作
“主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库
基于长连接的命令传播
一旦主从库完成全量复制,它们之间就会一直维护一个网络连接,主库会通过这个连接将后续陆续收到的命令操作再同步给从库
避免频繁建立连接的开销
网络断连或阻塞
增量复制
repl_backlog_buffer 环形缓存区
聊聊 Redis 的哨兵机制
哨兵其实就是一个运行在特殊模式下的 Redis 进程,主从库实例运行的同时,它也在运行
哨兵主要负责的就是三个任务:监控、选主(选择主库)和通知
监控
选主
通知
聊聊 Redis 的集群模式
收藏
收藏
0 条评论
下一页